@mandujs/core 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/package.json +94 -69
  2. package/src/auth/__tests__/login.test.ts +419 -0
  3. package/src/auth/__tests__/password.test.ts +122 -0
  4. package/src/auth/__tests__/reset.test.ts +296 -0
  5. package/src/auth/__tests__/tokens.test.ts +274 -0
  6. package/src/auth/__tests__/verification.test.ts +274 -0
  7. package/src/auth/index.ts +76 -0
  8. package/src/auth/login.ts +225 -0
  9. package/src/auth/password.ts +120 -0
  10. package/src/auth/reset.ts +243 -0
  11. package/src/auth/tokens.ts +612 -0
  12. package/src/auth/verification.ts +253 -0
  13. package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -0
  14. package/src/bundler/__tests__/cold-start.test.ts +504 -0
  15. package/src/bundler/__tests__/csp-nonce.test.ts +278 -0
  16. package/src/bundler/__tests__/dev-reliability.test.ts +619 -0
  17. package/src/bundler/__tests__/extended-watch.test.ts +710 -0
  18. package/src/bundler/__tests__/fast-refresh.test.ts +596 -0
  19. package/src/bundler/__tests__/hdr.test.ts +353 -0
  20. package/src/bundler/__tests__/hmr-client.test.ts +532 -0
  21. package/src/bundler/__tests__/manifest-schema.test.ts +266 -0
  22. package/src/bundler/__tests__/prod-smoke.test.ts +138 -0
  23. package/src/bundler/__tests__/slot-dispatch.test.ts +573 -0
  24. package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -0
  25. package/src/bundler/__tests__/vendor-cache.test.ts +455 -0
  26. package/src/bundler/build.test.ts +8 -1
  27. package/src/bundler/build.ts +310 -18
  28. package/src/bundler/css.ts +326 -323
  29. package/src/bundler/dev.ts +1611 -59
  30. package/src/bundler/fast-refresh-plugin.ts +307 -0
  31. package/src/bundler/hmr-types.ts +252 -0
  32. package/src/bundler/manifest-schema.ts +301 -0
  33. package/src/bundler/safe-build.test.ts +128 -0
  34. package/src/bundler/safe-build.ts +77 -0
  35. package/src/bundler/scenario-matrix.ts +229 -0
  36. package/src/bundler/types.ts +11 -0
  37. package/src/bundler/vendor-cache-types.ts +130 -0
  38. package/src/bundler/vendor-cache.ts +526 -0
  39. package/src/client/router.ts +214 -56
  40. package/src/db/__tests__/db.test.ts +485 -0
  41. package/src/db/index.ts +513 -0
  42. package/src/db/migrations/__tests__/runner.test.ts +661 -0
  43. package/src/db/migrations/history-table.ts +345 -0
  44. package/src/db/migrations/lock.ts +269 -0
  45. package/src/db/migrations/runner.ts +633 -0
  46. package/src/desktop/__tests__/smoke.test.ts +100 -0
  47. package/src/desktop/__tests__/window.test.ts +172 -0
  48. package/src/desktop/__tests__/worker.test.ts +266 -0
  49. package/src/desktop/index.ts +43 -0
  50. package/src/desktop/types.ts +158 -0
  51. package/src/desktop/window.ts +492 -0
  52. package/src/desktop/worker.ts +180 -0
  53. package/src/email/__tests__/email.test.ts +355 -0
  54. package/src/email/index.ts +282 -0
  55. package/src/email/resend.ts +163 -0
  56. package/src/email/smtp.ts +64 -0
  57. package/src/filling/__tests__/session-sqlite.test.ts +454 -0
  58. package/src/filling/context.ts +72 -78
  59. package/src/filling/cookie-codec.ts +299 -0
  60. package/src/filling/deps.ts +25 -1
  61. package/src/filling/filling.ts +28 -3
  62. package/src/filling/session-sqlite.ts +617 -0
  63. package/src/filling/session.ts +265 -216
  64. package/src/guard/decision-memory.test.ts +52 -22
  65. package/src/id/__tests__/id.test.ts +120 -0
  66. package/src/id/index.ts +105 -0
  67. package/src/kitchen/index.ts +2 -2
  68. package/src/kitchen/kitchen-handler.ts +86 -0
  69. package/src/kitchen/stream/activity-sse.ts +2 -1
  70. package/src/middleware/csrf.ts +328 -0
  71. package/src/middleware/index.ts +40 -0
  72. package/src/middleware/oauth/__tests__/oauth.test.ts +574 -0
  73. package/src/middleware/oauth/index.ts +505 -0
  74. package/src/middleware/oauth/providers.ts +115 -0
  75. package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -0
  76. package/src/middleware/rate-limit/index.ts +522 -0
  77. package/src/middleware/rate-limit/sqlite-store.ts +382 -0
  78. package/src/middleware/secure/__tests__/secure.test.ts +360 -0
  79. package/src/middleware/secure/csp.ts +193 -0
  80. package/src/middleware/secure/index.ts +417 -0
  81. package/src/middleware/session.ts +174 -0
  82. package/src/observability/event-bus.ts +81 -79
  83. package/src/paths.ts +37 -0
  84. package/src/perf/hmr-markers.ts +215 -0
  85. package/src/perf/index.ts +104 -0
  86. package/src/resource/__tests__/generator.test.ts +603 -2
  87. package/src/resource/ddl/__tests__/diff.test.ts +639 -0
  88. package/src/resource/ddl/__tests__/emit.test.ts +799 -0
  89. package/src/resource/ddl/__tests__/snapshot.test.ts +499 -0
  90. package/src/resource/ddl/diff.ts +392 -0
  91. package/src/resource/ddl/emit.ts +548 -0
  92. package/src/resource/ddl/persistence-types.ts +218 -0
  93. package/src/resource/ddl/snapshot.ts +447 -0
  94. package/src/resource/ddl/type-map.ts +223 -0
  95. package/src/resource/ddl/types.ts +232 -0
  96. package/src/resource/generator-repo.ts +610 -0
  97. package/src/resource/generator-schema.ts +476 -0
  98. package/src/resource/generator.ts +117 -1
  99. package/src/resource/index.ts +17 -1
  100. package/src/resource/schema.ts +30 -0
  101. package/src/router/fs-scanner.ts +3 -0
  102. package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -0
  103. package/src/runtime/__tests__/hdr-client.test.ts +223 -0
  104. package/src/runtime/__tests__/http-errors.test.ts +117 -0
  105. package/src/runtime/__tests__/not-found.test.ts +152 -0
  106. package/src/runtime/boundary.tsx +21 -1
  107. package/src/runtime/fast-refresh-runtime.ts +322 -0
  108. package/src/runtime/fast-refresh-types.ts +128 -0
  109. package/src/runtime/hmr-client.ts +409 -0
  110. package/src/runtime/http-errors.ts +113 -0
  111. package/src/runtime/index.ts +6 -0
  112. package/src/runtime/logger.ts +678 -677
  113. package/src/runtime/not-found.ts +93 -0
  114. package/src/runtime/redirect.ts +133 -0
  115. package/src/runtime/server.ts +518 -20
  116. package/src/runtime/ssr.ts +340 -10
  117. package/src/runtime/streaming-ssr.ts +222 -19
  118. package/src/scheduler/__tests__/scheduler.test.ts +514 -0
  119. package/src/scheduler/index.ts +343 -0
  120. package/src/storage/s3/__tests__/s3.test.ts +479 -0
  121. package/src/storage/s3/index.ts +412 -0
  122. package/src/testing/index.ts +58 -0
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Phase 4c — Dialect-specific type mapping.
3
+ *
4
+ * Pure, stateless translation from Mandu's abstract `DdlFieldType` to the
5
+ * concrete SQL column type declaration for each supported provider.
6
+ *
7
+ * This module is intentionally tiny and has no dependency on `emit.ts`; it
8
+ * is imported by `emit.ts` and may be consumed independently by Agents
9
+ * D (generator) and F (QA) for table introspection / parity checks.
10
+ *
11
+ * ## Type map (v1 definitive)
12
+ *
13
+ * | Mandu `type` | Postgres | MySQL | SQLite |
14
+ * |---------------------------|-------------------|---------------|---------|
15
+ * | `string` (no maxLength) | `TEXT` | `VARCHAR(255)`| `TEXT` |
16
+ * | `string` (maxLength = N) | `VARCHAR(N)` | `VARCHAR(N)` | `TEXT` |
17
+ * | `number` | `DOUBLE PRECISION`| `DOUBLE` | `REAL` |
18
+ * | `boolean` | `BOOLEAN` | `TINYINT(1)` | `INTEGER`|
19
+ * | `date` | `TIMESTAMPTZ` | `DATETIME(6)` | `TEXT` |
20
+ * | `uuid` | `UUID` | `CHAR(36)` | `TEXT` |
21
+ * | `email` | `VARCHAR(320)` | `VARCHAR(320)`| `TEXT` |
22
+ * | `url` | `VARCHAR(2048)` | `VARCHAR(2048)`| `TEXT` |
23
+ * | `json` / `array` / `object`| `JSONB` | `JSON` | `TEXT` |
24
+ *
25
+ * Rationale for fixed lengths:
26
+ * - `email` 320 chars — RFC 5321 cap (64 local + 1 @ + 255 domain).
27
+ * - `url` 2048 chars — de facto HTTP URL upper bound used by most
28
+ * browsers and CDNs; shorter than many drivers' TEXT truncation.
29
+ * - `string` default on MySQL is `VARCHAR(255)` because MySQL without a
30
+ * length on `VARCHAR` fails to parse; `TEXT` on MySQL disallows indexes
31
+ * without a prefix spec which breaks `indexed: true`.
32
+ *
33
+ * SQLite notes:
34
+ * - SQLite uses dynamic type affinity; our mapping chooses the canonical
35
+ * affinity names (`TEXT`, `REAL`, `INTEGER`) over the richer PG types so
36
+ * `SELECT typeof(col)` returns the expected affinity in tests.
37
+ * - Booleans are `INTEGER` — SQLite has no native boolean; the driver
38
+ * stores `1`/`0`.
39
+ */
40
+ import type { DdlDefault, DdlFieldDef, DdlFieldType, SqlProvider } from "./types";
41
+
42
+ // =====================================================================
43
+ // Constants — exposed for testability and to document magic numbers.
44
+ // =====================================================================
45
+
46
+ /** Default VARCHAR length for MySQL when the `string` field omits `maxLength`. */
47
+ export const MYSQL_DEFAULT_STRING_LENGTH = 255;
48
+
49
+ /** RFC 5321 email upper bound. Enforced on MySQL/PG as a VARCHAR cap. */
50
+ export const EMAIL_MAX_LENGTH = 320;
51
+
52
+ /**
53
+ * Pragmatic HTTP URL cap — shorter than most TEXT truncation points
54
+ * (e.g. IE's historical 2083 limit) and long enough for OAuth redirects.
55
+ */
56
+ export const URL_MAX_LENGTH = 2048;
57
+
58
+ /** Fixed CHAR length for UUIDs on MySQL (36 = canonical hex-with-dashes). */
59
+ export const UUID_CHAR_LENGTH = 36;
60
+
61
+ // =====================================================================
62
+ // Static per-provider map for non-parameterized types.
63
+ // `string` is resolved dynamically because it depends on `maxLength`.
64
+ // =====================================================================
65
+
66
+ type StaticType = Exclude<DdlFieldType, "string">;
67
+
68
+ const TYPE_MAP: Record<SqlProvider, Record<StaticType, string>> = {
69
+ postgres: {
70
+ number: "DOUBLE PRECISION",
71
+ boolean: "BOOLEAN",
72
+ date: "TIMESTAMPTZ",
73
+ uuid: "UUID",
74
+ email: `VARCHAR(${EMAIL_MAX_LENGTH})`,
75
+ url: `VARCHAR(${URL_MAX_LENGTH})`,
76
+ json: "JSONB",
77
+ array: "JSONB",
78
+ object: "JSONB",
79
+ },
80
+ mysql: {
81
+ number: "DOUBLE",
82
+ boolean: "TINYINT(1)",
83
+ date: "DATETIME(6)",
84
+ uuid: `CHAR(${UUID_CHAR_LENGTH})`,
85
+ email: `VARCHAR(${EMAIL_MAX_LENGTH})`,
86
+ url: `VARCHAR(${URL_MAX_LENGTH})`,
87
+ json: "JSON",
88
+ array: "JSON",
89
+ object: "JSON",
90
+ },
91
+ sqlite: {
92
+ number: "REAL",
93
+ boolean: "INTEGER",
94
+ date: "TEXT",
95
+ uuid: "TEXT",
96
+ email: "TEXT",
97
+ url: "TEXT",
98
+ json: "TEXT",
99
+ array: "TEXT",
100
+ object: "TEXT",
101
+ },
102
+ };
103
+
104
+ // =====================================================================
105
+ // Public API
106
+ // =====================================================================
107
+
108
+ /**
109
+ * Dialect-specific NOW() expression. Used by `DdlDefault.kind === "now"`
110
+ * and by emit.ts when a field's default should map to "current timestamp".
111
+ *
112
+ * - Postgres & MySQL — `NOW()` (ANSI-ish, identical semantics).
113
+ * - SQLite — `CURRENT_TIMESTAMP` (SQLite has no `NOW()`).
114
+ */
115
+ export function nowExpr(provider: SqlProvider): string {
116
+ switch (provider) {
117
+ case "postgres":
118
+ case "mysql":
119
+ return "NOW()";
120
+ case "sqlite":
121
+ return "CURRENT_TIMESTAMP";
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Resolve a Mandu field to its dialect-specific SQL column type declaration.
127
+ *
128
+ * `maxLength` applies ONLY to `string` — other types have fixed widths
129
+ * documented in the module header.
130
+ *
131
+ * @example
132
+ * resolveColumnType({ type: "string", maxLength: 100, ... }, "postgres")
133
+ * → "VARCHAR(100)"
134
+ * resolveColumnType({ type: "string", ... }, "postgres")
135
+ * → "TEXT"
136
+ * resolveColumnType({ type: "string", ... }, "mysql")
137
+ * → "VARCHAR(255)"
138
+ */
139
+ export function resolveColumnType(field: DdlFieldDef, provider: SqlProvider): string {
140
+ if (field.type === "string") {
141
+ return resolveStringType(field.maxLength, provider);
142
+ }
143
+ // Narrowed above — the remaining types are StaticType.
144
+ return TYPE_MAP[provider][field.type];
145
+ }
146
+
147
+ /**
148
+ * Resolve a `DdlDefault` to the SQL literal that goes after `DEFAULT`.
149
+ *
150
+ * Returns JUST the expression — the caller prepends `DEFAULT ` when
151
+ * composing the column definition.
152
+ *
153
+ * Escaping rules:
154
+ * - `literal: string` — ANSI single-quote escape (`'` → `''`).
155
+ * - `literal: number` — emitted verbatim (already numeric).
156
+ * - `literal: boolean` — Postgres/MySQL `TRUE`/`FALSE`; SQLite `1`/`0`
157
+ * (no native boolean).
158
+ * - `sql: expr` — passed through unchanged. Caller is responsible
159
+ * for cross-dialect portability; this is the escape hatch for things
160
+ * like sequence defaults or Postgres-specific `gen_random_uuid()`.
161
+ */
162
+ export function resolveDefault(def: NonNullable<DdlFieldDef["default"]>, provider: SqlProvider): string {
163
+ switch (def.kind) {
164
+ case "now":
165
+ return nowExpr(provider);
166
+ case "null":
167
+ return "NULL";
168
+ case "sql":
169
+ return def.expr;
170
+ case "literal":
171
+ return formatLiteral(def.value, provider);
172
+ }
173
+ // Exhaustiveness check — if DdlDefault gains a variant, TS will force this
174
+ // function to be updated before the project type-checks.
175
+ const _exhaustive: never = def;
176
+ return _exhaustive;
177
+ }
178
+
179
+ // =====================================================================
180
+ // Internals
181
+ // =====================================================================
182
+
183
+ function resolveStringType(maxLength: number | undefined, provider: SqlProvider): string {
184
+ if (provider === "sqlite") return "TEXT"; // SQLite ignores length spec.
185
+ if (typeof maxLength === "number" && maxLength > 0) return `VARCHAR(${maxLength})`;
186
+ if (provider === "postgres") return "TEXT";
187
+ // MySQL default: we MUST pick a length because `VARCHAR` with no length is
188
+ // a parse error. 255 is the convention most ORMs settle on.
189
+ return `VARCHAR(${MYSQL_DEFAULT_STRING_LENGTH})`;
190
+ }
191
+
192
+ function formatLiteral(value: string | number | boolean, provider: SqlProvider): string {
193
+ if (typeof value === "string") {
194
+ return `'${value.replace(/'/g, "''")}'`;
195
+ }
196
+ if (typeof value === "number") {
197
+ if (!Number.isFinite(value)) {
198
+ throw new Error(`DDL default literal must be a finite number, got ${String(value)}`);
199
+ }
200
+ return String(value);
201
+ }
202
+ // boolean
203
+ if (provider === "sqlite") return value ? "1" : "0";
204
+ return value ? "TRUE" : "FALSE";
205
+ }
206
+
207
+ // Runtime sanity — exported for test use only.
208
+ export function _internal_allStaticTypes(): StaticType[] {
209
+ return [
210
+ "number",
211
+ "boolean",
212
+ "date",
213
+ "uuid",
214
+ "email",
215
+ "url",
216
+ "json",
217
+ "array",
218
+ "object",
219
+ ];
220
+ }
221
+
222
+ /** Re-export DdlDefault for ergonomic consumer imports. */
223
+ export type { DdlDefault };
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Phase 4c shared types — DDL / diff / migration runtime.
3
+ *
4
+ * This file is the CONTRACT between Agents A (DDL emit), B (diff engine),
5
+ * C (migration runtime), and downstream D (generator) / E (CLI) / F (QA).
6
+ * Do NOT add logic here — pure types only. Logic belongs next to each
7
+ * agent's module.
8
+ *
9
+ * Source of truth for:
10
+ * - SqlProvider, DdlFieldType, DdlDefault, DdlFieldDef, DdlIndex, DdlResource
11
+ * - Snapshot (serialized state of the schema at a point in time)
12
+ * - Change (discriminated union the diff engine emits)
13
+ * - PendingMigration / AppliedMigration (runtime plan + history records)
14
+ * - LockStrategy (per-dialect apply serialization)
15
+ *
16
+ * References:
17
+ * docs/bun/phase-4c-team-plan.md — team plan + I/O contracts
18
+ * docs/rfcs/0001-db-resource-layer.md — design decisions incl. Appendix D
19
+ */
20
+
21
+ // ========== Provider + field types ==========
22
+
23
+ /** Supported SQL providers. Drizzle/Atlas/sqldef all share this same set. */
24
+ export type SqlProvider = "postgres" | "mysql" | "sqlite";
25
+
26
+ /**
27
+ * DDL-relevant subset of Mandu's existing `FieldType`. v1 supports:
28
+ * string/number/boolean/date/uuid/email/url/json/array/object.
29
+ *
30
+ * `array` and `object` are persisted as JSON columns (JSONB on Postgres,
31
+ * JSON on MySQL, TEXT on SQLite). Users who need typed JSON fields should
32
+ * define a Zod schema in the contract layer; the DB only stores the blob.
33
+ */
34
+ export type DdlFieldType =
35
+ | "string"
36
+ | "number"
37
+ | "boolean"
38
+ | "date"
39
+ | "uuid"
40
+ | "email"
41
+ | "url"
42
+ | "json"
43
+ | "array"
44
+ | "object";
45
+
46
+ /** How a DEFAULT clause is represented. */
47
+ export type DdlDefault =
48
+ | { kind: "now" } // CURRENT_TIMESTAMP / NOW() — dialect-mapped
49
+ | { kind: "null" }
50
+ | { kind: "literal"; value: string | number | boolean }
51
+ | { kind: "sql"; expr: string }; // raw expression — caller responsible for portability
52
+
53
+ /**
54
+ * DDL-level field definition. Normalized form derived from
55
+ * ResourceField + ResourceDefinition.persistence.fieldOverrides at
56
+ * snapshot creation time (see `snapshotFromResources`).
57
+ */
58
+ export interface DdlFieldDef {
59
+ /** Column name in the DB. Derived from Mandu's field key (e.g. `passwordHash` → `password_hash` when `snake_case: true`). */
60
+ name: string;
61
+ /** Abstract Mandu field type. Maps to dialect-specific SQL type via the type map. */
62
+ type: DdlFieldType;
63
+ /** Whether NULL is allowed. Default: false (derived from `ResourceField.required === false`). */
64
+ nullable: boolean;
65
+ /** Primary key flag. Exactly one field per resource should have this set (composite keys are v2+). */
66
+ primary: boolean;
67
+ /** Unique constraint — emits `UNIQUE` on the column (standalone, not composite). */
68
+ unique: boolean;
69
+ /** Whether this field participates in a single-column index (non-unique). */
70
+ indexed: boolean;
71
+ /** DEFAULT clause. */
72
+ default?: DdlDefault;
73
+ /**
74
+ * For `string` type — VARCHAR length hint. Ignored by SQLite (TEXT is
75
+ * unbounded). Postgres prefers TEXT when undefined; MySQL emits
76
+ * VARCHAR(255) default when undefined.
77
+ */
78
+ maxLength?: number;
79
+ }
80
+
81
+ /** Multi-column index definition (composite). Single-column indexes live on `DdlFieldDef.indexed`. */
82
+ export interface DdlIndex {
83
+ name: string; // must be unique within the resource
84
+ fields: string[]; // field names (in order)
85
+ unique: boolean;
86
+ }
87
+
88
+ /**
89
+ * DDL-level resource — what actually reaches the emit / diff engines.
90
+ * Produced by `snapshotFromResources` from `ParsedResource[]`. Contains
91
+ * only the information the DB layer cares about.
92
+ */
93
+ export interface DdlResource {
94
+ /** Table name in the DB. Usually `pluralize(resourceName)` or explicit override. */
95
+ name: string;
96
+ fields: DdlFieldDef[]; // order-preserving; affects emit order
97
+ indexes: DdlIndex[]; // multi-column indexes only; single-column live on fields
98
+ }
99
+
100
+ // ========== Snapshots ==========
101
+
102
+ /**
103
+ * The full schema state at a point in time. Serialized to JSON and stored
104
+ * at `.mandu/schema/applied.json` after each successful apply.
105
+ * The diff engine compares an old snapshot (or null for first run) to a
106
+ * next snapshot computed from the current resource files.
107
+ */
108
+ export interface Snapshot {
109
+ /** Format version of this snapshot file. Bump on breaking schema changes. */
110
+ version: 1;
111
+ /** Which provider this snapshot was built for. Diffing across providers is an error. */
112
+ provider: SqlProvider;
113
+ /** Resources in deterministic order (sorted by name). */
114
+ resources: DdlResource[];
115
+ /** When this snapshot was computed. For provenance only — not used by diff. */
116
+ generatedAt: string; // ISO 8601
117
+ }
118
+
119
+ // ========== Changes ==========
120
+
121
+ /**
122
+ * Discriminated union emitted by the diff engine. Every `Change` is one
123
+ * "atomic" DDL operation. Emit order is deterministic so the generated
124
+ * migration SQL is stable across runs.
125
+ *
126
+ * Rename is NOT auto-detected — the diff engine always emits drop + add.
127
+ * The CLI layer (Agent E) asks the user whether consecutive drop+add are
128
+ * a rename and rewrites the Change list accordingly before SQL emit.
129
+ */
130
+ export type Change =
131
+ | { kind: "create-table"; resource: DdlResource }
132
+ | { kind: "drop-table"; resourceName: string }
133
+ | { kind: "add-column"; resourceName: string; field: DdlFieldDef }
134
+ | { kind: "drop-column"; resourceName: string; fieldName: string }
135
+ | {
136
+ kind: "alter-column-type";
137
+ resourceName: string;
138
+ fieldName: string;
139
+ fromType: DdlFieldType;
140
+ toType: DdlFieldType;
141
+ /** v1 emits a stub comment. User edits the migration manually. */
142
+ stub: true;
143
+ }
144
+ | { kind: "alter-column-nullable"; resourceName: string; fieldName: string; nullable: boolean }
145
+ | { kind: "alter-column-default"; resourceName: string; fieldName: string; default?: DdlDefault }
146
+ | { kind: "add-index"; resourceName: string; index: DdlIndex }
147
+ | { kind: "drop-index"; resourceName: string; indexName: string }
148
+ | {
149
+ kind: "rename-table";
150
+ oldName: string;
151
+ newName: string;
152
+ /** Emitted only after CLI user confirmation. Diff engine never emits directly. */
153
+ origin: "user-confirmed";
154
+ }
155
+ | {
156
+ kind: "rename-column";
157
+ resourceName: string;
158
+ oldName: string;
159
+ newName: string;
160
+ origin: "user-confirmed";
161
+ };
162
+
163
+ // ========== Migration runtime ==========
164
+
165
+ /** A migration file that exists in the migrations directory but has not yet been applied. */
166
+ export interface PendingMigration {
167
+ /** Zero-padded 4-digit sequence, e.g. "0001". Must sort lexicographically. */
168
+ version: string;
169
+ /** Filename relative to the migrations dir, e.g. "0001_create_users.sql". */
170
+ filename: string;
171
+ /** Full SQL text of the migration. */
172
+ sql: string;
173
+ /** SHA-256 of `sql` with `\r\n` normalized to `\n` — used by `__mandu_migrations` for tamper detection. */
174
+ checksum: string;
175
+ /** Filesystem mtime of the migration file. */
176
+ createdAt: Date;
177
+ }
178
+
179
+ /** A migration that has been applied — read from the `__mandu_migrations` history table. */
180
+ export interface AppliedMigration {
181
+ version: string;
182
+ filename: string;
183
+ checksum: string;
184
+ appliedAt: Date;
185
+ executionMs: number;
186
+ success: boolean;
187
+ }
188
+
189
+ /** Snapshot of the migration history at call time. */
190
+ export interface MigrationStatus {
191
+ applied: AppliedMigration[];
192
+ pending: PendingMigration[];
193
+ /** Migrations that exist in history but whose checksum no longer matches the file. */
194
+ tampered: Array<{ version: string; filename: string; storedChecksum: string; currentChecksum: string }>;
195
+ /** Migration files on disk that have no history row and don't match pending (shouldn't happen but guards against dir corruption). */
196
+ orphaned: Array<{ filename: string }>;
197
+ }
198
+
199
+ // ========== Lock strategy (per-dialect apply serialization) ==========
200
+
201
+ /**
202
+ * Single-process apply serialization. Multi-instance coordination is out of
203
+ * scope for v1 (RFC §8 non-goals).
204
+ *
205
+ * Defaults per provider (Agent C implements):
206
+ * - postgres → "pg_advisory_lock" (`pg_advisory_lock(bigint)` + `pg_advisory_unlock`)
207
+ * - mysql → "mysql_get_lock" (`GET_LOCK('mandu-migrations', 60)` + `RELEASE_LOCK`)
208
+ * - sqlite → "sqlite_immediate" (`BEGIN IMMEDIATE` for the apply transaction)
209
+ */
210
+ export type LockStrategy = "pg_advisory_lock" | "mysql_get_lock" | "sqlite_immediate" | "none";
211
+
212
+ // ========== Scope fences — what v1 does NOT cover ==========
213
+
214
+ /**
215
+ * v1 scope (enforced by Agent A/B — they should NOT handle these):
216
+ * - Foreign keys
217
+ * - CHECK constraints
218
+ * - ENUM types (custom Postgres ENUMs, MySQL ENUM columns)
219
+ * - Computed / GENERATED columns
220
+ * - Partitioning
221
+ * - Triggers, views, stored procedures
222
+ * - Alter column type (stub only — user edits manually)
223
+ * - Rename auto-detection (always drop+add unless CLI prompts user)
224
+ * - Multi-column primary key (composite)
225
+ * - Rollback / DOWN migrations
226
+ * - Repeatable migrations (Flyway `R__` style)
227
+ *
228
+ * Anything outside this list is intentionally out of scope for Phase 4c
229
+ * v1. Expansion lands in 4c.1 / 4c.2 patches after 4c merge.
230
+ */
231
+ export type Phase4cScopeMarker = typeof _PHASE_4C_V1_SCOPE;
232
+ const _PHASE_4C_V1_SCOPE = Symbol.for("@mandujs/core/resource/ddl/phase-4c-v1");