Skip to content

Wire up class-based migration.ts new/apply/plan CLI commands#354

Merged
wmadden merged 24 commits intomainfrom
tml-2219-b-class-flow-scaffolding
Apr 20, 2026
Merged

Wire up class-based migration.ts new/apply/plan CLI commands#354
wmadden merged 24 commits intomainfrom
tml-2219-b-class-flow-scaffolding

Conversation

@wmadden
Copy link
Copy Markdown
Contributor

@wmadden wmadden commented Apr 19, 2026

Summary

This is PR B in the stacked split of #349 (base: PR A — tml-2219-a-cli-emit-unification).

Introduces the class-flow migration authoring spine: the canonical
shape for migration files becomes a TypeScript class extending the
framework Migration base, with .operations, .describe(), and a
self-running Migration.run(import.meta.url, M) helper.

Highlights

  • Migration base class evolves from .plan() to a .operations
    getter + .describe() metadata. Migration.run(...) produces fully
    attested ops.json + migration.json artifacts when the migration
    file is run directly.
  • Scaffolding capability: targets opt into class-flow scaffolding by
    implementing MigrationPlanner.emptyMigration(...), returning a
    MigrationPlanWithAuthoringSurface that the CLI can serialize back to
    a migration.ts source file via renderTypeScript().
  • MongoDB target: ships PlannerProducedMongoMigration (extends
    Migration directly to avoid a cycle with family-mongo), and the
    authoring-surface renderTypeScript() for authored Mongo migration
    sources.
  • Postgres target: parallel scaffolding.ts + authoring-surface
    tests for the descriptor planner.
  • placeholder(...) utility (PN-MIG-2001): scaffolded sources mark
    unfilled slots with placeholder(...). Emit detects unfilled
    placeholders and surfaces PN-MIG-2001.
  • Framework/target import boundary lint added under scripts/.

The class-flow shape is now the single supported authoring surface;
the old .plan() API and existing example migrations are migrated.

Stacked PR context

This PR depends on the following stack:

  1. PR D — Docs & ADRs (#TBD)
  2. PR A — CLI emit unification (#TBD) ← base
  3. PR B (this PR) — Class-flow scaffolding spine
  4. PR C — Mongo data transforms (to follow)

Test plan

  • CI green (pnpm typecheck, pnpm lint, pnpm test)
  • Class-flow examples (examples/mongo-demo, examples/retail-store) run via direct tsx invocation
  • Scaffolded migration.ts for an empty Mongo plan compiles and runs
  • Unfilled placeholder produces PN-MIG-2001 from emit

Summary by CodeRabbit

  • New Features

    • CLI auto-selects descriptor vs class authoring, scaffolds editable/empty migrations, writes runnable migration scripts with shebangs, and copies contract files into migration packages.
    • Targets (Postgres, Mongo) expose authoring surfaces and TypeScript rendering.
  • Behavior Changes

    • Migrations now use an operations-based contract; planners accept an explicit fromHash and migrationId is persisted in migration.json.
    • Unfilled placeholder slots now fail with error PN-MIG-2001.
  • Documentation

    • CLI emit/new docs updated for new artifacts and failure modes.
  • Tests

    • Expanded coverage for authoring, emit, scaffolding, runtime detection, and error propagation.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces the Migration.plan() method with an abstract operations getter and required targetId; introduces authoring-capable plans (MigrationPlanWithAuthoringSurface), planner emptyMigration() scaffolding, a migration strategy selector, placeholder errors, new IO helpers (copyContractToMigrationDir, writeMigrationTs), runtime detection, and widespread CLI/target/test updates.

Changes

Cohort / File(s) Summary
Migration Base & API
packages/1-framework/3-tooling/migration/src/migration-base.ts, packages/1-framework/3-tooling/migration/src/migration-ts.ts
Refactor Migration API: remove plan() in favor of abstract get operations() and required describe()/targetId; always produce migration.json; add writeMigrationTs and remove scaffoldMigrationTs.
Framework Types & Exports
packages/1-framework/1-core/framework-components/src/control-migration-types.ts, packages/1-framework/1-core/framework-components/src/exports/control.ts
Add MigrationPlanWithAuthoringSurface (with renderTypeScript()), MigrationScaffoldContext, planner emptyMigration(), and include fromHash in planner inputs; re-export new types.
Errors & Placeholders
packages/1-framework/1-core/errors/src/control.ts, .../errors/src/migration.ts, .../errors/src/exports/migration.ts, tests
Add CliErrorDomain, new structured errors: errorUnfilledPlaceholder/placeholder (PN‑MIG‑2001), errorTargetHasIncompleteMigrationCapabilities (PN‑MIG‑2011), errorPlanDoesNotSupportAuthoringSurface (PN‑MIG‑2010); update messages to reference operations.
CLI: Strategy, Commands & Emit Flow
packages/1-framework/3-tooling/cli/src/lib/migration-strategy.ts, .../commands/migration-new.ts, .../commands/migration-plan.ts, .../commands/migration-emit.ts, .../lib/migration-emit.ts, .../utils/cli-errors.ts, README`
Introduce migrationStrategy() to pick descriptor vs class-based flows; migration new/plan/emit now use planner emptyMigration()/renderTypeScript() or renderDescriptorTypeScript() and persist migrationId in migration.json; propagate placeholder errors to CLI.
Migration I/O & Runtime Detection
packages/1-framework/3-tooling/migration/src/io.ts, .../migration-ts.ts, .../runtime-detection.ts, .../exports/*.ts
Add copyContractToMigrationDir() (copies contract.json + optional .d.ts); add detectScaffoldRuntime()/shebangLineFor() and ScaffoldRuntime; writeMigrationTs() writes file and sets executable when shebang present; update exports.
Planner Strategy & Tools Tests
packages/1-framework/3-tooling/cli/test/*, packages/1-framework/3-tooling/migration/test/*
Tests updated/added to cover migration strategy, placeholder propagation, writeMigrationTs, runtime detection, and the new migration/json output shape.
Mongo Family & Target
packages/2-mongo-family/9-family/src/core/mongo-emit.ts, packages/2-mongo-family/4-query/..., packages/3-mongo-target/1-mongo-target/src/core/*, .../exports/*, tests
mongoEmit and tests moved to read .operations (accept factory-returned MigrationPlan objects); added PlannerProducedMongoMigration implementing authoring surface, emptyMigration() support, renamed renderer to renderCallsToTypeScript (meta required), and propagate placeholder() errors; tests adapted.
Postgres Target: Scaffolding & Planner
packages/3-targets/3-targets/postgres/src/core/migrations/scaffolding.ts, .../planner.ts, .../exports/control.ts, tests
Add descriptor-based renderDescriptorTypeScript(), planner now returns a plan with an authoring-surface-shaped renderTypeScript() (throws when unsupported), implements emptyMigration() and added tests for authoring surface.
Migration Tools Re-exports & IO
packages/1-framework/3-tooling/migration/src/exports/migration-ts.ts, .../exports/io.ts, packages/1-framework/3-tooling/migration/src/exports/*
Update re-exports: remove scaffoldMigrationTs, add writeMigrationTs, export copyContractToMigrationDir, and export runtime-detection helpers.
Examples & Broad Tests
examples/*/migrations/*/migration.ts, examples/*/test/*, many package tests, integration/e2e tests
Example migrations updated to get operations() and describe(); numerous tests updated to read operations, include fromHash planner arg, assert new error codes/meta, and validate migration.json semantics.
Misc & Lint
scripts/lint-framework-target-imports.mjs, package.json changes
Add repo lint script to forbid framework→target imports; adjust workspace package dependencies where needed.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(0,120,200,0.5)
    participant CLI
    participant Planner
    participant TargetCapability
    participant FS
  end

  CLI->>Planner: plan(options { contract, fromHash, toHash, ... })
  Planner->>TargetCapability: inspect migrations capability
  alt descriptor strategy
    TargetCapability->>Planner: planWithDescriptors -> descriptors
    Planner->>TargetCapability: renderDescriptorTypeScript(descriptors, scaffoldCtx)
    TargetCapability->>FS: writeMigrationTs(packageDir, content)
  else class-based strategy
    TargetCapability->>Planner: emit() -> MigrationPlan (operations + targetId)
    Planner->>FS: copyContractToMigrationDir(packageDir, contractPath)
    Planner->>FS: write ops.json and migration.json (attest)
  end
  CLI->>FS: read/write migration.json / ops.json
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~110 minutes

Possibly related PRs

Poem

🐰 I hopped through code with nimble feet,

Swapped plan() for operations — tidy and sweet,
Shebangs and scaffolds, placeholders in line,
Planners choose routes, and files now shine,
A carrot for tests — the build looks fine! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: wiring up class-based migration commands for new/apply/plan CLI operations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2219-b-class-flow-scaffolding

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 19, 2026

Open in StackBlitz

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-runtime@354

@prisma-next/family-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-mongo@354

@prisma-next/sql-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-runtime@354

@prisma-next/family-sql

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-sql@354

@prisma-next/middleware-telemetry

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/middleware-telemetry@354

@prisma-next/mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo@354

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-paradedb@354

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-pgvector@354

@prisma-next/postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/postgres@354

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-orm-client@354

@prisma-next/sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sqlite@354

@prisma-next/target-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-mongo@354

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-mongo@354

@prisma-next/driver-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-mongo@354

@prisma-next/contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract@354

@prisma-next/utils

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/utils@354

@prisma-next/config

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/config@354

@prisma-next/errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/errors@354

@prisma-next/framework-components

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/framework-components@354

@prisma-next/operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/operations@354

@prisma-next/contract-authoring

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract-authoring@354

@prisma-next/ids

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/ids@354

@prisma-next/psl-parser

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-parser@354

@prisma-next/psl-printer

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-printer@354

@prisma-next/cli

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/cli@354

@prisma-next/emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/emitter@354

@prisma-next/migration-tools

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/migration-tools@354

prisma-next

npm i https://pkg.pr.new/prisma/prisma-next@354

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/vite-plugin-contract-emit@354

@prisma-next/runtime-executor

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/runtime-executor@354

@prisma-next/mongo-codec

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-codec@354

@prisma-next/mongo-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract@354

@prisma-next/mongo-value

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-value@354

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract-psl@354

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract-ts@354

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-emitter@354

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-schema-ir@354

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-query-ast@354

@prisma-next/mongo-orm

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-orm@354

@prisma-next/mongo-pipeline-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-pipeline-builder@354

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-lowering@354

@prisma-next/mongo-wire

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-wire@354

@prisma-next/sql-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract@354

@prisma-next/sql-errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-errors@354

@prisma-next/sql-operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-operations@354

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-schema-ir@354

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-psl@354

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-ts@354

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-emitter@354

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-lane-query-builder@354

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-relational-core@354

@prisma-next/sql-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-builder@354

@prisma-next/target-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-postgres@354

@prisma-next/target-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-sqlite@354

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-postgres@354

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-sqlite@354

@prisma-next/driver-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-postgres@354

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-sqlite@354

commit: 59bb1d9

@wmadden wmadden force-pushed the tml-2219-a-cli-emit-unification branch from 3bde981 to 53f8188 Compare April 19, 2026 11:41
@wmadden wmadden force-pushed the tml-2219-b-class-flow-scaffolding branch from d1c595a to 1e0bc67 Compare April 19, 2026 11:42
@wmadden wmadden force-pushed the tml-2219-a-cli-emit-unification branch from 53f8188 to ba88a32 Compare April 19, 2026 12:08
@wmadden wmadden force-pushed the tml-2219-b-class-flow-scaffolding branch from 1e0bc67 to f001281 Compare April 19, 2026 12:08
@wmadden wmadden changed the title feat(framework,target-mongo): add class-flow migration scaffolding spine feat(framework,target-mongo): add Mongo migration new/apply/plan with migration.ts files Apr 19, 2026
@wmadden wmadden changed the title feat(framework,target-mongo): add Mongo migration new/apply/plan with migration.ts files Wire up class-based migration.ts new/apply/plan CLI commands Apr 19, 2026
@wmadden wmadden force-pushed the tml-2219-a-cli-emit-unification branch 2 times, most recently from 50911ef to 75531bc Compare April 19, 2026 16:29
@wmadden wmadden force-pushed the tml-2219-b-class-flow-scaffolding branch from f001281 to 92a699e Compare April 19, 2026 16:36
@wmadden
Copy link
Copy Markdown
Contributor Author

wmadden commented Apr 19, 2026

Re-triggering CI after rebase

@wmadden wmadden closed this Apr 19, 2026
@wmadden wmadden reopened this Apr 19, 2026
@wmadden wmadden force-pushed the tml-2219-b-class-flow-scaffolding branch from 1690cff to 1f00d90 Compare April 19, 2026 16:58
Base automatically changed from tml-2219-a-cli-emit-unification to main April 19, 2026 18:20
wmadden added 2 commits April 19, 2026 20:27
Introduces the class-based migration authoring surface as the canonical
form for migration files:

- `Migration` base class evolves from a `.plan()` method to an
  `.operations` getter and `.describe()` metadata method, with a static
  `Migration.run(import.meta.url, M)` helper that produces fully-attested
  ops.json + migration.json artifacts when a migration file is run
  directly.
- Targets opt into class-flow scaffolding by implementing
  `MigrationPlanner.emptyMigration(...)`, returning a
  `MigrationPlanWithAuthoringSurface` that the CLI can serialize back to
  a `migration.ts` source file via `renderTypeScript()`.
- MongoDB target ships `PlannerProducedMongoMigration`, which extends the
  framework `Migration` base directly (avoiding a cycle with
  `family-mongo`) and renders authored MongoDB migration sources.
- Postgres target adds a parallel `scaffolding.ts` and
  authoring-surface tests for its descriptor planner.
- Adds the `placeholder(...)` utility (PN-MIG-2001) used by scaffolded
  migration sources to mark slots the user must fill in. Emit detects
  unfilled placeholders and surfaces a structured `PN-MIG-2001` error.
- Adds an internal lint script enforcing framework/target import
  boundaries for the new spine.

The class-flow shape is now the single supported authoring surface; the
old `.plan()` API and existing example migrations are migrated to the
new shape.
…adata

Adds a test exercising `PlannerProducedMongoMigration.renderTypeScript()` with
`kind` and `labels` set, which previously left the conditional spread branches
in `renderTypeScript` uncovered. Restores 100% branch coverage so the CI
coverage job stops failing for `3-mongo-target/1-mongo-target`.
@wmadden wmadden force-pushed the tml-2219-b-class-flow-scaffolding branch from eca86d0 to 3ca7de9 Compare April 19, 2026 18:30
Comment thread packages/3-targets/3-targets/postgres/src/core/migrations/scaffolding.ts Dismissed
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/1-framework/3-tooling/cli/test/lib/migration-emit.test.ts (1)

144-164: ⚠️ Potential issue | 🟡 Minor

Refactor error assertion to use async expectation pattern.

Replace manual try/catch with await expect(...).rejects.toMatchObject(...) for cleaner, more idiomatic error testing:

♻️ Suggested refactor
-    let thrown: unknown;
-    try {
-      await emitMigration(
-        DIR,
-        makeCtx({ targetId: 'test-target' }) as unknown as Parameters<EmitMigration>[1],
-      );
-    } catch (error) {
-      thrown = error;
-    }
-
-    expect(thrown).toMatchObject({
-      code: '2011',
-      message: 'Target migrations capability is incomplete',
-    });
-    expect((thrown as Error & { why: string }).why).toContain(
-      'implements neither `resolveDescriptors`',
-    );
+    await expect(
+      emitMigration(
+        DIR,
+        makeCtx({ targetId: 'test-target' }) as unknown as Parameters<EmitMigration>[1],
+      ),
+    ).rejects.toMatchObject({
+      code: '2011',
+      message: 'Target migrations capability is incomplete',
+      why: expect.stringContaining('implements neither `resolveDescriptors`'),
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/cli/test/lib/migration-emit.test.ts` around
lines 144 - 164, Replace the manual try/catch in this test with the async Jest
rejects pattern: call emitMigration once into a promise variable (e.g., const p
= emitMigration(DIR, makeCtx({ targetId: 'test-target' }) as unknown as
Parameters<EmitMigration>[1])) and then use await
expect(p).rejects.toMatchObject({ code: '2011', message: 'Target migrations
capability is incomplete' }); and also assert the why text with await
expect(p).rejects.toHaveProperty('why', expect.stringContaining('implements
neither `resolveDescriptors`')); this keeps the test concise and avoids running
emitMigration twice while preserving the same assertions on emitMigration and
the mocks.hasMigrationTs setup.
packages/1-framework/3-tooling/cli/src/commands/migration-new.ts (1)

233-246: ⚠️ Potential issue | 🟠 Major

Use CliStructuredError.is() type guard instead of manual property checks.

The code uses a weak defensive check that only verifies a string code property, but other errors (like Node.js system errors) may have string code properties without conforming to CliStructuredError shape. The proper pattern, established and used throughout the codebase (migration-emit.ts, migration-plan.ts, db-update.ts, contract-emit.ts, and others), is to use the CliStructuredError.is() type guard which validates the full error structure including name, domain, and toEnvelope function.

Correct pattern
   } catch (error) {
-    if (
-      error instanceof Error &&
-      'code' in error &&
-      typeof (error as { code?: unknown }).code === 'string'
-    ) {
-      return notOk(error as CliStructuredError);
+    if (CliStructuredError.is(error)) {
+      return notOk(error);
     }
     return notOk(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/cli/src/commands/migration-new.ts` around
lines 233 - 246, Replace the manual defensive check in the catch block with the
established type guard: use CliStructuredError.is(error) to detect structured
CLI errors (instead of checking for a string 'code' property) and return
notOk(error) when true; otherwise call notOk(errorUnexpected(...)) as before.
Update the catch handling around the existing notOk and errorUnexpected calls so
it references the CliStructuredError.is() guard and preserves the same error
message envelope behavior.
🧹 Nitpick comments (9)
packages/1-framework/3-tooling/cli/test/control-api/db-update.test.ts (1)

48-50: Optional cleanup: deduplicate the repeated renderTypeScript throw stub.

A shared helper (e.g., const unusedRenderTypeScript = ...) would reduce fixture noise and keep intent clearer.

♻️ Minimal refactor sketch
+const unusedRenderTypeScript = () => {
+  throw new Error('not used in db update tests');
+};

 // ...
-      renderTypeScript: () => {
-        throw new Error('not used in db update tests');
-      },
+      renderTypeScript: unusedRenderTypeScript,

Also applies to: 251-253, 376-378

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/cli/test/control-api/db-update.test.ts` around
lines 48 - 50, Multiple fixtures repeat the same renderTypeScript stub throwing
"not used in db update tests"; extract a single shared constant (e.g.,
unusedRenderTypeScript) at the top of the test file and replace all inline
occurrences of renderTypeScript: () => { throw new Error('not used in db update
tests'); } with renderTypeScript: unusedRenderTypeScript so the behavior is
deduplicated and intent is clearer; ensure the new constant is in scope for all
test fixtures that currently reference renderTypeScript.
packages/1-framework/3-tooling/migration/src/io.ts (1)

93-93: Guard .json suffix before deriving the .d.ts path.

Current slicing assumes contractJsonPath always ends with .json. A defensive check makes misuse fail fast and avoids malformed sibling paths.

♻️ Suggested hardening
-  const dtsPath = `${contractJsonPath.slice(0, -'.json'.length)}.d.ts`;
+  if (!contractJsonPath.endsWith('.json')) {
+    throw new Error(`Expected contract path to end with ".json": ${contractJsonPath}`);
+  }
+  const dtsPath = `${contractJsonPath.slice(0, -'.json'.length)}.d.ts`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/migration/src/io.ts` at line 93, The code
derives dtsPath from contractJsonPath by slicing off ".json" without checking
the suffix, which can produce incorrect paths; update the logic around the
dtsPath calculation (the line assigning dtsPath and any helper around it) to
first verify contractJsonPath endsWith ".json" and if not, throw a clear error
(or handle appropriately), then derive the .d.ts sibling (or use a safe
replacement approach) so misuse fails fast and malformed sibling paths are
avoided.
packages/1-framework/3-tooling/cli/test/lib/migration-strategy.test.ts (1)

52-61: Use direct throw matchers for the sync error path.

Line 53–57 can be simplified and made more expressive with toThrowError.

♻️ Suggested refactor
-    let caught: unknown;
-    try {
-      migrationStrategy(capability, 'broken');
-    } catch (error) {
-      caught = error;
-    }
-
-    expect(caught).toBeInstanceOf(CliStructuredError);
-    expect((caught as CliStructuredError).code).toBe('2011');
-    expect((caught as CliStructuredError).meta).toMatchObject({ targetId: 'broken' });
+    expect(() => migrationStrategy(capability, 'broken')).toThrowError(
+      expect.objectContaining({
+        code: '2011',
+        meta: expect.objectContaining({ targetId: 'broken' }),
+      }),
+    );

As per coding guidelines: Use expect(() => ...).toThrow(...) to assert errors instead of manual try/catch blocks in tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/cli/test/lib/migration-strategy.test.ts`
around lines 52 - 61, Replace the manual try/catch assertion with a direct throw
matcher: call migrationStrategy(capability, 'broken') inside an expect wrapper
and use toThrowError to assert it throws a CliStructuredError, then assert the
thrown error's .code equals '2011' and .meta matches { targetId: 'broken' }
using the matcher form (referencing migrationStrategy, CliStructuredError, code,
meta, and capability to locate the test).
packages/1-framework/3-tooling/cli/test/commands/migration-emit.test.ts (1)

134-141: Prefer matcher-based rejection assertion over manual try/catch.

Line 135–139 uses control-flow flags for expected failure. Use an async matcher for clearer intent and failures.

♻️ Suggested refactor
-      let thrown = false;
-      try {
-        await executeCommand(command, ['--dir', 'migrations/20260101_test', '--json']);
-      } catch {
-        thrown = true;
-      }
-
-      expect(thrown).toBe(true);
+      await expect(
+        executeCommand(command, ['--dir', 'migrations/20260101_test', '--json']),
+      ).rejects.toBeDefined();

As per coding guidelines: Use expect(() => ...).toThrow(...) to assert errors instead of manual try/catch blocks in tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/cli/test/commands/migration-emit.test.ts`
around lines 134 - 141, The test uses a manual try/catch with a thrown flag to
assert rejection when calling executeCommand(command, ['--dir',
'migrations/20260101_test', '--json']); replace that pattern with Jest's async
rejection matcher: await expect(executeCommand(command, ['--dir',
'migrations/20260101_test', '--json'])).rejects.toThrow(); remove the thrown
flag and try/catch block so the test uses the clearer matcher-based assertion on
executeCommand.
packages/3-targets/3-targets/postgres/test/migrations/planner.authoring-surface.test.ts (1)

72-82: Use expect().toThrow() instead of manual try/catch.

Per coding guidelines, prefer expect(() => ...).toThrow(...) for error assertions. This also provides cleaner failure messages.

♻️ Proposed refactor
-      let caught: unknown;
-      try {
-        success.plan.renderTypeScript();
-      } catch (err) {
-        caught = err;
-      }
-
-      expect(caught).toBeInstanceOf(CliStructuredError);
-      expect((caught as CliStructuredError).code).toBe('2010');
-      expect((caught as CliStructuredError).meta).toMatchObject({ targetId: 'postgres' });
+      expect(() => success.plan.renderTypeScript()).toThrow(
+        expect.objectContaining({
+          code: '2010',
+          meta: expect.objectContaining({ targetId: 'postgres' }),
+        }),
+      );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/3-targets/3-targets/postgres/test/migrations/planner.authoring-surface.test.ts`
around lines 72 - 82, Replace the manual try/catch block that calls
success.plan.renderTypeScript() and asserts on the caught value with Jest's
built-in throw matcher: call expect(() =>
success.plan.renderTypeScript()).toThrow(CliStructuredError) to assert the error
type, and if you still need to assert the error.code and error.meta, use
expect(() =>
success.plan.renderTypeScript()).toThrowError(expect.objectContaining({ code:
'2010', meta: expect.objectContaining({ targetId: 'postgres' }) })); this
removes the manual caught variable and leverages Jest's expect().toThrow APIs
while keeping references to success.plan.renderTypeScript and
CliStructuredError.
packages/2-mongo-family/9-family/test/mongo-emit.test.ts (1)

146-162: Use await expect(...).rejects.toThrow() for async error assertions.

Since mongoEmit is async, use rejects.toThrow() instead of manual try/catch for cleaner assertions per coding guidelines.

♻️ Proposed refactor
-      let thrown: unknown;
-      try {
-        await mongoEmit({ dir: pkgDir, frameworkComponents: [] });
-      } catch (error) {
-        thrown = error;
-      }
-
-      expect(CliStructuredError.is(thrown)).toBe(true);
-      expect(thrown).toMatchObject({
-        code: '2001',
-        domain: 'MIG',
-        meta: { slot: 'backfill-product-status:run' },
-      });
+      await expect(mongoEmit({ dir: pkgDir, frameworkComponents: [] })).rejects.toMatchObject({
+        code: '2001',
+        domain: 'MIG',
+        meta: { slot: 'backfill-product-status:run' },
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-mongo-family/9-family/test/mongo-emit.test.ts` around lines 146 -
162, Replace the manual try/catch used to capture errors from the async
mongoEmit call with Jest's async assertion helpers: remove the thrown variable
and try/catch and use await expect(mongoEmit({ dir: pkgDir, frameworkComponents:
[] })).rejects.toMatchObject({ code: '2001', domain: 'MIG', meta: { slot:
'backfill-product-status:run' } }); and add a separate await
expect(mongoEmit(...)).rejects.toSatisfy(err => CliStructuredError.is(err)) if
you still need to assert CliStructuredError.is; target the mongoEmit invocation
and the CliStructuredError.is check when making these replacements.
packages/3-mongo-target/1-mongo-target/test/migration-e2e.test.ts (1)

270-283: Silent skip when dist files are missing may mask CI failures.

The early return when dist files don't exist means this test silently passes if the build step was skipped. Consider using it.skipIf() or logging a warning to make the skip explicit.

♻️ Proposed alternative using explicit skip
-    it('runs via ./migration.ts on POSIX (or node migration.ts on Windows) and prints operations JSON', async () => {
+    it('runs via ./migration.ts on POSIX (or node migration.ts on Windows) and prints operations JSON', async ({ skip }) => {
       const distsExist = await Promise.all([
         stat(familyMongoDistMigration).then(
           () => true,
           () => false,
         ),
         stat(targetMongoDistMigration).then(
           () => true,
           () => false,
         ),
       ]);
       if (!distsExist.every(Boolean)) {
+        skip('dist files not built');
         return;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-mongo-target/1-mongo-target/test/migration-e2e.test.ts` around
lines 270 - 283, Instead of silently returning inside the test when the dist
files are missing, make the skip explicit: move the stat checks for
familyMongoDistMigration and targetMongoDistMigration out of the it(...) body
and compute distsExist before defining the test, then conditionally register the
test via test.skip(...) (or it.skip(...)) when !distsExist.every(Boolean);
alternatively, if you must keep the check inside the test, replace the early
return with a clear console.warn and call this.skip() / fail the test explicitly
so CI doesn't silently pass — reference the distsExist variable and the
familyMongoDistMigration and targetMongoDistMigration symbols to locate where to
change the logic.
packages/3-mongo-target/1-mongo-target/src/core/planner-produced-migration.ts (1)

45-52: Consider using ifDefined() utility for conditional spreads.

Per coding guidelines, prefer ifDefined() from @prisma-next/utils/defined for conditional object spreads:

♻️ Suggested refactor
+import { ifDefined } from '@prisma-next/utils/defined';
+
   renderTypeScript(): string {
     return renderCallsToTypeScript(this.calls, {
       from: this.meta.from,
       to: this.meta.to,
-      ...(this.meta.kind !== undefined ? { kind: this.meta.kind } : {}),
-      ...(this.meta.labels !== undefined ? { labels: this.meta.labels } : {}),
+      ...ifDefined(this.meta.kind, (kind) => ({ kind })),
+      ...ifDefined(this.meta.labels, (labels) => ({ labels })),
     });
   }

As per coding guidelines: "Use ifDefined() from @prisma-next/utils/defined for conditional object spreads instead of inline conditional spread patterns"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/3-mongo-target/1-mongo-target/src/core/planner-produced-migration.ts`
around lines 45 - 52, In renderTypeScript(), replace the inline conditional
spread expressions for meta.kind and meta.labels with the ifDefined() helper
from `@prisma-next/utils/defined`: import ifDefined and call
renderCallsToTypeScript(this.calls, { from: this.meta.from, to: this.meta.to,
...ifDefined(this.meta.kind, v => ({ kind: v })), ...ifDefined(this.meta.labels,
v => ({ labels: v })) }) so the conditional properties are added via ifDefined
rather than inline ternary spreads; update imports accordingly and keep function
name renderTypeScript and variables this.calls/this.meta unchanged.
packages/3-mongo-target/1-mongo-target/test/planner-produced-migration.test.ts (1)

84-97: Consider asserting optional metadata appears in rendered output.

This test verifies renderTypeScript() succeeds with optional kind and labels, but doesn't assert they appear in the output. If the authoring surface should embed this metadata, consider adding assertions:

     const source = migration.renderTypeScript();
 
     expect(source).toContain('class M extends Migration');
     expect(source).toContain(META.from);
     expect(source).toContain(META.to);
+    expect(source).toContain('baseline');
+    expect(source).toContain('initial');

If the metadata is intentionally omitted from the rendered TypeScript (only used at runtime), this test is fine as-is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/3-mongo-target/1-mongo-target/test/planner-produced-migration.test.ts`
around lines 84 - 97, The test currently builds a PlannerProducedMongoMigration
(using CreateIndexCall) and calls renderTypeScript but doesn't assert that the
optional metadata (kind and labels passed via META) are present in the rendered
output; update the test to assert that the rendered source contains the kind
string (e.g., 'baseline') and each label (e.g., 'initial', 'seed') so the test
verifies metadata propagation from
PlannerProducedMongoMigration.renderTypeScript; locate the test block that
constructs migration and source and add assertions that source.includes the kind
and labels.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/1-framework/1-core/errors/test/migration.test.ts`:
- Around line 24-39: The test replaced a try/catch with expect(() =>
...).toThrow() but lost validation of the thrown object's structure and the
.toEnvelope().code check; modify the test for placeholder('foo') to use
expect(() => { try { placeholder('foo'); } catch (e) { thrown = e as
CliStructuredError; throw e; } }).toThrow(expect.objectContaining({ code:
'2001', domain: 'MIG', meta: { slot: 'foo' } })), where thrown is declared as
CliStructuredError | undefined so you can afterwards assert
thrown?.toEnvelope().code === 'PN-MIG-2001'; keep references to placeholder,
CliStructuredError, and toEnvelope exactly as in the diff.

In `@packages/1-framework/3-tooling/migration/src/migration-base.ts`:
- Around line 201-205: The code only checks Array.isArray(ops) but doesn't
validate each operation object before persisting ops.json; update the logic that
handles instance.operations (ops) to iterate and validate every element has
required fields id, label, and operationClass (and that they are the expected
types/format), and throw a descriptive Error referencing "operations" if any
entry is missing/invalid so the write path is aborted; add the validation
immediately after const ops = instance.operations and before any file write to
ensure ops.json is never written with malformed entries.
- Around line 151-190: readExistingManifest currently swallows parse/hash
mismatches and allows stale/malformed migration.json fields (fromContract,
toContract, hints) to be reused; change it to fail fast: make
readExistingManifest throw on JSON parse errors instead of returning null, and
validate the manifest contents before returning—specifically verify
existing.fromContract?.storage?.storageHash === meta.from and
existing.toContract?.storage?.storageHash === meta.to (or otherwise validate the
stored contract bookends match the incoming meta) and that required
hints/authorship shapes exist; if any validation fails, throw a clear error so
the caller (which builds baseManifest and then calls computeMigrationId) cannot
silently reuse inconsistent fields. Ensure the calling code handles the thrown
error appropriately (propagates to the user) rather than treating it as “no
manifest.”

In `@scripts/lint-framework-target-imports.mjs`:
- Around line 72-73: The current comment-filter in
scripts/lint-framework-target-imports.mjs only checks trimmed.startsWith('*')
and trimmed.startsWith('//'), which misses block comment markers and allows
one-line block/JSDoc comments to be linted; update the conditional that skips
comment lines to also detect block comment starts and ends by adding checks for
trimmed.startsWith('/*') (including '/**') and trimmed === '*/' (or
trimmed.startsWith('*/')), so lines that begin a block comment or close one are
skipped by the same continue path (adjust the condition that uses trimmed to
include these additional cases).

---

Outside diff comments:
In `@packages/1-framework/3-tooling/cli/src/commands/migration-new.ts`:
- Around line 233-246: Replace the manual defensive check in the catch block
with the established type guard: use CliStructuredError.is(error) to detect
structured CLI errors (instead of checking for a string 'code' property) and
return notOk(error) when true; otherwise call notOk(errorUnexpected(...)) as
before. Update the catch handling around the existing notOk and errorUnexpected
calls so it references the CliStructuredError.is() guard and preserves the same
error message envelope behavior.

In `@packages/1-framework/3-tooling/cli/test/lib/migration-emit.test.ts`:
- Around line 144-164: Replace the manual try/catch in this test with the async
Jest rejects pattern: call emitMigration once into a promise variable (e.g.,
const p = emitMigration(DIR, makeCtx({ targetId: 'test-target' }) as unknown as
Parameters<EmitMigration>[1])) and then use await
expect(p).rejects.toMatchObject({ code: '2011', message: 'Target migrations
capability is incomplete' }); and also assert the why text with await
expect(p).rejects.toHaveProperty('why', expect.stringContaining('implements
neither `resolveDescriptors`')); this keeps the test concise and avoids running
emitMigration twice while preserving the same assertions on emitMigration and
the mocks.hasMigrationTs setup.

---

Nitpick comments:
In `@packages/1-framework/3-tooling/cli/test/commands/migration-emit.test.ts`:
- Around line 134-141: The test uses a manual try/catch with a thrown flag to
assert rejection when calling executeCommand(command, ['--dir',
'migrations/20260101_test', '--json']); replace that pattern with Jest's async
rejection matcher: await expect(executeCommand(command, ['--dir',
'migrations/20260101_test', '--json'])).rejects.toThrow(); remove the thrown
flag and try/catch block so the test uses the clearer matcher-based assertion on
executeCommand.

In `@packages/1-framework/3-tooling/cli/test/control-api/db-update.test.ts`:
- Around line 48-50: Multiple fixtures repeat the same renderTypeScript stub
throwing "not used in db update tests"; extract a single shared constant (e.g.,
unusedRenderTypeScript) at the top of the test file and replace all inline
occurrences of renderTypeScript: () => { throw new Error('not used in db update
tests'); } with renderTypeScript: unusedRenderTypeScript so the behavior is
deduplicated and intent is clearer; ensure the new constant is in scope for all
test fixtures that currently reference renderTypeScript.

In `@packages/1-framework/3-tooling/cli/test/lib/migration-strategy.test.ts`:
- Around line 52-61: Replace the manual try/catch assertion with a direct throw
matcher: call migrationStrategy(capability, 'broken') inside an expect wrapper
and use toThrowError to assert it throws a CliStructuredError, then assert the
thrown error's .code equals '2011' and .meta matches { targetId: 'broken' }
using the matcher form (referencing migrationStrategy, CliStructuredError, code,
meta, and capability to locate the test).

In `@packages/1-framework/3-tooling/migration/src/io.ts`:
- Line 93: The code derives dtsPath from contractJsonPath by slicing off ".json"
without checking the suffix, which can produce incorrect paths; update the logic
around the dtsPath calculation (the line assigning dtsPath and any helper around
it) to first verify contractJsonPath endsWith ".json" and if not, throw a clear
error (or handle appropriately), then derive the .d.ts sibling (or use a safe
replacement approach) so misuse fails fast and malformed sibling paths are
avoided.

In `@packages/2-mongo-family/9-family/test/mongo-emit.test.ts`:
- Around line 146-162: Replace the manual try/catch used to capture errors from
the async mongoEmit call with Jest's async assertion helpers: remove the thrown
variable and try/catch and use await expect(mongoEmit({ dir: pkgDir,
frameworkComponents: [] })).rejects.toMatchObject({ code: '2001', domain: 'MIG',
meta: { slot: 'backfill-product-status:run' } }); and add a separate await
expect(mongoEmit(...)).rejects.toSatisfy(err => CliStructuredError.is(err)) if
you still need to assert CliStructuredError.is; target the mongoEmit invocation
and the CliStructuredError.is check when making these replacements.

In
`@packages/3-mongo-target/1-mongo-target/src/core/planner-produced-migration.ts`:
- Around line 45-52: In renderTypeScript(), replace the inline conditional
spread expressions for meta.kind and meta.labels with the ifDefined() helper
from `@prisma-next/utils/defined`: import ifDefined and call
renderCallsToTypeScript(this.calls, { from: this.meta.from, to: this.meta.to,
...ifDefined(this.meta.kind, v => ({ kind: v })), ...ifDefined(this.meta.labels,
v => ({ labels: v })) }) so the conditional properties are added via ifDefined
rather than inline ternary spreads; update imports accordingly and keep function
name renderTypeScript and variables this.calls/this.meta unchanged.

In `@packages/3-mongo-target/1-mongo-target/test/migration-e2e.test.ts`:
- Around line 270-283: Instead of silently returning inside the test when the
dist files are missing, make the skip explicit: move the stat checks for
familyMongoDistMigration and targetMongoDistMigration out of the it(...) body
and compute distsExist before defining the test, then conditionally register the
test via test.skip(...) (or it.skip(...)) when !distsExist.every(Boolean);
alternatively, if you must keep the check inside the test, replace the early
return with a clear console.warn and call this.skip() / fail the test explicitly
so CI doesn't silently pass — reference the distsExist variable and the
familyMongoDistMigration and targetMongoDistMigration symbols to locate where to
change the logic.

In
`@packages/3-mongo-target/1-mongo-target/test/planner-produced-migration.test.ts`:
- Around line 84-97: The test currently builds a PlannerProducedMongoMigration
(using CreateIndexCall) and calls renderTypeScript but doesn't assert that the
optional metadata (kind and labels passed via META) are present in the rendered
output; update the test to assert that the rendered source contains the kind
string (e.g., 'baseline') and each label (e.g., 'initial', 'seed') so the test
verifies metadata propagation from
PlannerProducedMongoMigration.renderTypeScript; locate the test block that
constructs migration and source and add assertions that source.includes the kind
and labels.

In
`@packages/3-targets/3-targets/postgres/test/migrations/planner.authoring-surface.test.ts`:
- Around line 72-82: Replace the manual try/catch block that calls
success.plan.renderTypeScript() and asserts on the caught value with Jest's
built-in throw matcher: call expect(() =>
success.plan.renderTypeScript()).toThrow(CliStructuredError) to assert the error
type, and if you still need to assert the error.code and error.meta, use
expect(() =>
success.plan.renderTypeScript()).toThrowError(expect.objectContaining({ code:
'2010', meta: expect.objectContaining({ targetId: 'postgres' }) })); this
removes the manual caught variable and leverages Jest's expect().toThrow APIs
while keeping references to success.plan.renderTypeScript and
CliStructuredError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 356f6008-9bf4-49c5-941a-cb4815e48625

📥 Commits

Reviewing files that changed from the base of the PR and between 21a24dd and 3ca7de9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (61)
  • examples/mongo-demo/migrations/20260415_add-posts-author-index/migration.ts
  • examples/mongo-demo/test/manual-migration.test.ts
  • examples/retail-store/migrations/20260415_add-product-validation/migration.ts
  • examples/retail-store/test/manual-migration.test.ts
  • packages/1-framework/1-core/errors/src/control.ts
  • packages/1-framework/1-core/errors/src/exports/migration.ts
  • packages/1-framework/1-core/errors/src/migration.ts
  • packages/1-framework/1-core/errors/test/migration.test.ts
  • packages/1-framework/1-core/framework-components/src/control-migration-types.ts
  • packages/1-framework/1-core/framework-components/src/exports/control.ts
  • packages/1-framework/3-tooling/cli/README.md
  • packages/1-framework/3-tooling/cli/src/commands/migration-emit.ts
  • packages/1-framework/3-tooling/cli/src/commands/migration-new.ts
  • packages/1-framework/3-tooling/cli/src/commands/migration-plan.ts
  • packages/1-framework/3-tooling/cli/src/control-api/operations/db-init.ts
  • packages/1-framework/3-tooling/cli/src/control-api/operations/db-update.ts
  • packages/1-framework/3-tooling/cli/src/lib/migration-emit.ts
  • packages/1-framework/3-tooling/cli/src/lib/migration-strategy.ts
  • packages/1-framework/3-tooling/cli/src/utils/cli-errors.ts
  • packages/1-framework/3-tooling/cli/test/commands/migration-emit.test.ts
  • packages/1-framework/3-tooling/cli/test/commands/migration-plan-command.test.ts
  • packages/1-framework/3-tooling/cli/test/control-api/db-update.test.ts
  • packages/1-framework/3-tooling/cli/test/lib/migration-emit.test.ts
  • packages/1-framework/3-tooling/cli/test/lib/migration-strategy.test.ts
  • packages/1-framework/3-tooling/migration/src/exports/io.ts
  • packages/1-framework/3-tooling/migration/src/exports/migration-ts.ts
  • packages/1-framework/3-tooling/migration/src/io.ts
  • packages/1-framework/3-tooling/migration/src/migration-base.ts
  • packages/1-framework/3-tooling/migration/src/migration-ts.ts
  • packages/1-framework/3-tooling/migration/src/runtime-detection.ts
  • packages/1-framework/3-tooling/migration/test/io.test.ts
  • packages/1-framework/3-tooling/migration/test/migration-base.test.ts
  • packages/1-framework/3-tooling/migration/test/migration-ts.test.ts
  • packages/1-framework/3-tooling/migration/test/runtime-detection.test.ts
  • packages/2-mongo-family/4-query/query-ast/src/exports/control.ts
  • packages/2-mongo-family/4-query/query-ast/src/migration-operation-types.ts
  • packages/2-mongo-family/9-family/src/core/mongo-emit.ts
  • packages/2-mongo-family/9-family/src/core/mongo-target-descriptor.ts
  • packages/2-mongo-family/9-family/test/mongo-emit.test.ts
  • packages/3-mongo-target/1-mongo-target/package.json
  • packages/3-mongo-target/1-mongo-target/src/core/mongo-planner.ts
  • packages/3-mongo-target/1-mongo-target/src/core/planner-produced-migration.ts
  • packages/3-mongo-target/1-mongo-target/src/core/render-typescript.ts
  • packages/3-mongo-target/1-mongo-target/src/exports/control.ts
  • packages/3-mongo-target/1-mongo-target/src/exports/migration.ts
  • packages/3-mongo-target/1-mongo-target/test/migration-e2e.test.ts
  • packages/3-mongo-target/1-mongo-target/test/mongo-planner.test.ts
  • packages/3-mongo-target/1-mongo-target/test/planner-produced-migration.test.ts
  • packages/3-mongo-target/1-mongo-target/test/render-ops.test.ts
  • packages/3-mongo-target/1-mongo-target/test/render-typescript.test.ts
  • packages/3-mongo-target/2-mongo-adapter/test/mongo-runner.test.ts
  • packages/3-targets/3-targets/postgres/package.json
  • packages/3-targets/3-targets/postgres/src/core/migrations/scaffolding.ts
  • packages/3-targets/3-targets/postgres/src/exports/control.ts
  • packages/3-targets/3-targets/postgres/test/migrations/planner.authoring-surface.test.ts
  • packages/3-targets/3-targets/postgres/test/migrations/planner.contract-to-schema-ir.test.ts
  • packages/3-targets/3-targets/postgres/test/migrations/scaffolding.test.ts
  • scripts/lint-framework-target-imports.mjs
  • test/integration/test/mongo/migration-e2e.test.ts
  • test/integration/test/mongo/migration-m2-vocabulary.test.ts
  • test/integration/test/mongo/migration-psl-authoring.test.ts

Comment thread packages/1-framework/1-core/errors/test/migration.test.ts
Comment thread packages/1-framework/3-tooling/migration/src/migration-base.ts
Comment thread packages/1-framework/3-tooling/migration/src/migration-base.ts
Comment thread scripts/lint-framework-target-imports.mjs Outdated
@wmadden
Copy link
Copy Markdown
Contributor Author

wmadden commented Apr 19, 2026

Triage of CodeRabbit review #4136389683 (outside-diff + nitpicks)

Decomposed and triaged independently from the inline threads (already triaged separately). Summary of decisions on the 11 findings in the review body:

Will address in this PR

  • packages/1-framework/3-tooling/cli/test/lib/migration-emit.test.ts:144-164 — replace try/catch with await expect(...).rejects.toMatchObject(...).
  • packages/1-framework/3-tooling/cli/src/commands/migration-new.ts:233-246 — replace ad-hoc code in error check with CliStructuredError.is(error).
  • packages/1-framework/3-tooling/cli/test/lib/migration-strategy.test.ts:52-61 — switch to expect(() => ...).toThrowError(expect.objectContaining(...)).
  • packages/1-framework/3-tooling/cli/test/commands/migration-emit.test.ts:134-141 — switch to await expect(...).rejects.toBeDefined().
  • packages/3-targets/3-targets/postgres/test/migrations/planner.authoring-surface.test.ts:72-82 — switch to expect(() => ...).toThrow(expect.objectContaining(...)).
  • packages/2-mongo-family/9-family/test/mongo-emit.test.ts:146-162 — switch to await expect(...).rejects.toMatchObject(...).
  • packages/3-mongo-target/1-mongo-target/test/migration-e2e.test.ts:270-283 — make the dist-files-missing skip explicit (ctx.skip(...)/it.skipIf(...)) so silent passes do not mask CI gaps.
  • packages/3-mongo-target/1-mongo-target/src/core/planner-produced-migration.ts:45-52 — replace inline conditional spreads with ifDefined() from @prisma-next/utils/defined.
  • packages/3-mongo-target/1-mongo-target/test/planner-produced-migration.test.ts:84-97 — assert that kind / labels either appear (or deliberately do not appear) in the rendered TS source.

Will not address

  • packages/1-framework/3-tooling/cli/test/control-api/db-update.test.ts:48-50 (dedupe renderTypeScript throw stub) — the stub is intentionally inline at each fixture site so each fixture is self-contained and greppable; extracting a one-liner adds indirection without a readability win.
  • packages/1-framework/3-tooling/migration/src/io.ts:93 (guard .json suffix) — contractJsonPath originates from CLI scaffolding that always passes a .json path. Per prefer-assertions-over-defensive-checks we don't add runtime guards for impossible misuse.

Inline review threads have been triaged in their own threads (3 deferred to TML-2271 / TML-2272 and resolved; 2 acknowledged and left open for the implementer).

wmadden added 7 commits April 19, 2026 21:28
Replace manual try/catch in the placeholder() test with
expect(() => ...).toThrow(expect.objectContaining({...})) per the
prefer-to-throw rule. The toEnvelope() coverage is preserved by
asserting against errorUnfilledPlaceholder() (the underlying builder)
without re-invoking the throwing call twice.

Addresses CodeRabbit review action A01 on PR #354.
Extend the comment-skip filter in lint-framework-target-imports.mjs to
also match `/*`, `/**`, and `*/` line prefixes so single-line block
and JSDoc comments containing the forbidden `@prisma-next/target-*`
substring do not trigger false-positive failures.

Addresses CodeRabbit review action A05 on PR #354.
Replace manual try/catch in the
"throws errorTargetHasIncompleteMigrationCapabilities when neither
capability is present" test with await expect(...).rejects.toMatchObject
per the prefer-to-throw rule. Avoids running emitMigration twice and
preserves all assertions (code, message, why substring).

Addresses CodeRabbit review action A06a on PR #354.
Replace the manual instanceof Error + duck-typed code-string check in
migration-new.ts with CliStructuredError.is(error). Aligns with the
pattern used across migration-emit, migration-plan, db-update, and
contract-emit. Real correctness gain: Node system errors (e.g. ENOENT)
have string code properties without conforming to CliStructuredError,
so they now correctly fall through to errorUnexpected(...).

Addresses CodeRabbit review action A06b on PR #354.
Replace manual try/catch in the no-hooks-registered case with a single
expect(() => migrationStrategy(...)).toThrow(expect.objectContaining({
code, meta })) per the prefer-to-throw rule. Drops the unused
CliStructuredError import.

Addresses CodeRabbit review action A06e on PR #354.
Replace the let thrown = false / try { ... } catch { thrown = true; }
control-flow flag in the PN-MIG-2001 propagation test with
await expect(executeCommand(...)).rejects.toBeDefined() per the
prefer-to-throw rule.

Addresses CodeRabbit review action A06f on PR #354.
Replace manual try/catch in the descriptor-flow renderTypeScript()
test with expect(() => ...).toThrow(expect.objectContaining({ code,
meta })) per the prefer-to-throw rule. Drops the now-unused
CliStructuredError import.

Addresses CodeRabbit review action A06g on PR #354.
@wmadden
Copy link
Copy Markdown
Contributor Author

wmadden commented Apr 19, 2026

Iterate-loop #2 — A10a + A11/A12/A13 addressed

Following up on the previous iterate-loop summary. The 4 will address items from CodeRabbit re-review #4136463574 are landed:

  • A10a (outside-diff, migration-emit.test.ts): converted the four remaining try { await executeCommand(...) } catch {} blocks to await expect(...).rejects.toBeDefined() to match the PN-MIG-2001 case from A06f.
  • A11 (migration-new.ts): pass join(packageDir, 'contract.json') (the path copyContractToMigrationDir writes to) into planner.emptyMigration so the rendered scaffold imports the contract via ./contract.d instead of a workspace-relative path.
  • A12 (migration-e2e.test.ts): added readonly targetId = 'mongo'; to the three inline Migration fixture class strings.
  • A13 (migration-e2e.test.ts): switched the four ESM specifiers (migrationExport, factoryExport, family + target dist/migration.mjs) to pathToFileURL(join(...)).href so absolute import paths are valid file: URLs on Windows.

Per-thread Done replies + resolutions posted on A11/A12/A13. A10a is sourced from the review body and has no inline thread — this comment is the receipt.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/3-mongo-target/1-mongo-target/test/migration-e2e.test.ts (1)

164-178: ⚠️ Potential issue | 🟠 Major

Add readonly targetId = 'mongo'; to generated migration class.

The renderCallsToTypeScript() function omits the required abstract member readonly targetId from the generated Migration subclass. While tests pass because tsx executes without type checking, the generated scaffolds fail TypeScript compilation (tsc --noEmit). The tests at lines 164–178, 198–220, 236–248, and 289–302 only verify runtime execution via ops.json output; they do not validate that the generated source is valid TypeScript. Add readonly targetId = 'mongo'; to the generated class, and assert this field is present in the rendered output.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-mongo-target/1-mongo-target/test/migration-e2e.test.ts` around
lines 164 - 178, renderCallsToTypeScript currently emits a Migration subclass
that omits the required abstract member `readonly targetId`, causing generated
files to fail `tsc` type-checks; update the renderer (the function
renderCallsToTypeScript in the TS render module) to include `readonly targetId =
'mongo';` in the generated class body for the Migration subclass and update any
template or string generation that builds the class so the rendered source
contains that field; ensure the Migration subclass output (the string produced
by renderCallsToTypeScript) includes the `readonly targetId = 'mongo';` line so
compiled consumers pass `tsc --noEmit`.
🧹 Nitpick comments (1)
packages/1-framework/3-tooling/cli/test/commands/migration-emit.test.ts (1)

134-136: Also assert the failure exit code.

These cases now prove the JSON envelope, but not that the command exits non-zero. A regression that still prints the right error payload while calling process.exit(0) would pass here; please assert the mocked process.exit() call alongside the envelope checks.

As per coding guidelines, "packages/1-framework/3-tooling/cli/**/*.{test,spec}.ts: Test CLI commands by spying on process.exit() to verify correct exit codes are called."

Also applies to: 161-163, 179-181, 207-209, 224-226

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/cli/test/commands/migration-emit.test.ts`
around lines 134 - 136, The test currently only checks the JSON error envelope
returned by executeCommand(command, ['--dir', 'migrations/20260101_test',
'--json']) but does not assert the CLI exit code; update the test to spy on
process.exit (e.g., jest.spyOn(process, 'exit').mockImplementation(() => { throw
new Error('process.exit called'); })) before calling executeCommand, then assert
that process.exit was called with a non-zero code (e.g.,
expect(process.exit).toHaveBeenCalledWith(expect.any(Number)) or a specific
non-zero value) after the promise rejects; apply the same pattern to the other
failing-case blocks that use executeCommand/command in this file so each test
both verifies the JSON envelope and that process.exit was invoked with a
non-zero exit code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/1-framework/3-tooling/cli/src/commands/migration-new.ts`:
- Around line 205-209: The call to assertFrameworkComponentsCompatible(...) is
currently outside the try/catch and can throw a CliStructuredError that escapes
the Result flow; move the assertion call into the existing try block that
handles the rest of validation so its errors are caught and returned via the
notOk() path. Specifically, relocate the
assertFrameworkComponentsCompatible(config.family.familyId,
config.target.targetId, [config.target, config.adapter,
...(config.extensionPacks ?? [])]) invocation inside the try that wraps the
migration creation/validation logic and ensure no duplicate early-return logic
is introduced when the assertion fails.

---

Outside diff comments:
In `@packages/3-mongo-target/1-mongo-target/test/migration-e2e.test.ts`:
- Around line 164-178: renderCallsToTypeScript currently emits a Migration
subclass that omits the required abstract member `readonly targetId`, causing
generated files to fail `tsc` type-checks; update the renderer (the function
renderCallsToTypeScript in the TS render module) to include `readonly targetId =
'mongo';` in the generated class body for the Migration subclass and update any
template or string generation that builds the class so the rendered source
contains that field; ensure the Migration subclass output (the string produced
by renderCallsToTypeScript) includes the `readonly targetId = 'mongo';` line so
compiled consumers pass `tsc --noEmit`.

---

Nitpick comments:
In `@packages/1-framework/3-tooling/cli/test/commands/migration-emit.test.ts`:
- Around line 134-136: The test currently only checks the JSON error envelope
returned by executeCommand(command, ['--dir', 'migrations/20260101_test',
'--json']) but does not assert the CLI exit code; update the test to spy on
process.exit (e.g., jest.spyOn(process, 'exit').mockImplementation(() => { throw
new Error('process.exit called'); })) before calling executeCommand, then assert
that process.exit was called with a non-zero code (e.g.,
expect(process.exit).toHaveBeenCalledWith(expect.any(Number)) or a specific
non-zero value) after the promise rejects; apply the same pattern to the other
failing-case blocks that use executeCommand/command in this file so each test
both verifies the JSON envelope and that process.exit was invoked with a
non-zero exit code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 6cf4f798-5ec1-40b1-acbb-cfff14313683

📥 Commits

Reviewing files that changed from the base of the PR and between b555705 and 4f120d6.

📒 Files selected for processing (3)
  • packages/1-framework/3-tooling/cli/src/commands/migration-new.ts
  • packages/1-framework/3-tooling/cli/test/commands/migration-emit.test.ts
  • packages/3-mongo-target/1-mongo-target/test/migration-e2e.test.ts

Comment thread packages/1-framework/3-tooling/cli/src/commands/migration-new.ts Outdated
@wmadden
Copy link
Copy Markdown
Contributor Author

wmadden commented Apr 19, 2026

Re-triage of new review activity (iterate-loop #3)

Six new unresolved review threads + a third CodeRabbit review (#4136492857) + author self-comments came in after iterate-loop #2 (72093cb..4f120d6). Triaged below; on-disk plan in wip/reviews/prisma_prisma-next_pr-354/review-actions.{json,md} (entries A16–A24).

Action Decision Where Disposition
A16 will address packages/1-framework/3-tooling/cli/src/commands/migration-new.ts L205–209 Move assertFrameworkComponentsCompatible(...) inside the existing try block so its CliStructuredError flows through catchnotOk(error) instead of escaping the Result flow.
A17 will address packages/1-framework/1-core/errors/src/control.ts L135–141 Derive CliErrorDomain from the runtime CLI_ERROR_DOMAINS constant (literal tuple → (typeof X)[number]) so the type and the membership set can't drift.
A18 will address packages/2-mongo-family/9-family/src/core/mongo-migration.ts (+ example, + e2e fixtures) Declare readonly targetId = 'mongo' on MongoMigration; drop the override from the example user migration and the inline e2e fixture class strings. Subsumes A22a (CodeRabbit outside-diff on renderCallsToTypeScript() omitting targetId) — once MongoMigration provides it, the rendered scaffold inherits.
A19 defer → TML-2275 packages/1-framework/3-tooling/cli/src/control-api/operations/db-init.ts L86–88 Replace the fromHash: '' baseline sentinel with a typed nullable / discriminated origin in the planner input.
A20 defer → TML-2275 packages/1-framework/3-tooling/cli/src/control-api/operations/db-update.ts L86–88 Same fix; in addition, plumb the real origin contract hash through db update instead of the empty-string placeholder.
A21 defer → TML-2275 packages/1-framework/3-tooling/migration/src/migration-base.ts L58–62 Drop the from === '' ? null : ... consumer-side branch in Migration.origin once the producer side stops using the empty-string sentinel.
A22a will address (via A18) packages/3-mongo-target/1-mongo-target/src/core/render-typescript.ts (outside-diff) Generated class M extends Migration { ... } must satisfy abstract readonly targetId: string. Cleanest fix is putting targetId = 'mongo' on MongoMigration (A18) so the generated subclass inherits.
A22b won't address packages/1-framework/3-tooling/cli/test/commands/migration-emit.test.ts L134–226 (nitpick) These tests already use await expect(executeCommand(...)).rejects.toBeDefined() (per A06f / A10a); on the CLI surface that resolves only when the command exits non-zero, so the rejection itself is the exit-code signal in this harness. The process.exit spy guideline applies to tests that observe the CLI through stdout/stderr only — here the contract is observed via the throw + envelope assertion, which is strictly stronger.
A23 / A24 not actionable PR-level comments Author's own triage / iterate-loop summaries; not review findings.

Defers grouped under TML-2275 ("Model 'no origin contract' / baseline-origin properly across db-init, db-update, and Migration"). All three threads (A19/A20/A21) resolved with thumbs-down + ticket link.

Pending will-address for next implementer pass: A16, A17, A18, A22a (folded into A18). All four inline threads left unresolved with thumbs-up + ack reply.

Comment thread packages/1-framework/1-core/errors/src/control.ts Outdated
Comment thread examples/mongo-demo/migrations/20260415_add-posts-author-index/migration.ts Outdated
Comment thread packages/1-framework/3-tooling/migration/src/migration-base.ts
Comment thread packages/1-framework/3-tooling/migration/src/migration-base.ts Outdated
Comment thread packages/2-mongo-family/9-family/src/core/mongo-emit.ts Outdated
Comment thread packages/1-framework/3-tooling/cli/src/commands/migration-new.ts Outdated
Comment thread packages/3-targets/3-targets/postgres/src/exports/control.ts Outdated
wmadden added 3 commits April 19, 2026 22:43
…on new

Move the framework-components compatibility assertion inside the
existing try block in `migration new` so the `CliStructuredError` it
throws on incompatible components flows through the catch and is
returned via `notOk(error)` instead of crashing the command. Mirrors
the pattern used by `migration emit` and `migration plan` for the
same call.
Promote the runtime list of CLI error domains to a single
`as const` tuple and derive `CliErrorDomain` from it via
`(typeof CLI_ERROR_DOMAINS)[number]` so the type and the runtime
membership check can no longer drift apart. The membership Set is
constructed directly from the same tuple; `isCliErrorDomain` continues
to gate by Set lookup.
`MongoMigration` is the family-owned base class for class-flow Mongo
migrations, and the `targetId` is fixed at `mongo` for every subclass.
Declare it as `readonly targetId = "mongo" as const` on the base class
itself so:

- user-authored migrations (the mongo-demo + retail-store example
  migrations) no longer need to repeat the `override readonly targetId`
  line, and
- renderer-generated scaffolds emitted by `renderCallsToTypeScript`
  inherit `targetId` automatically, satisfying the abstract contract
  on `Migration` without the renderer having to embed the literal.

Removes the now-redundant override from the two example user
migrations and from the three inline `class M extends Migration { ... }`
fixture strings in target-mongo `migration-e2e.test.ts`. Also subsumes
the CodeRabbit outside-diff finding that flagged
`renderCallsToTypeScript()` for omitting `targetId`.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/1-framework/3-tooling/cli/src/commands/migration-new.ts (1)

212-225: Consider rendering before mutating disk to avoid partial scaffolds on planner failure.

Right now, package files are written before planner/render calls. If planner creation or renderTypeScript() fails, you can leave behind a partially scaffolded migration directory. Consider computing rendered source first (or writing to a temp dir then committing writes).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/cli/src/commands/migration-new.ts` around
lines 212 - 225, The current flow writes files (writeMigrationPackage,
copyContractToMigrationDir) before running planner/renderer, risking partial
scaffolds if migrations.createPlanner / planner.emptyMigration /
renderTypeScript() throws; change the sequence to produce the rendered
TypeScript first (call createControlStack, config.family.create,
migrations.createPlanner, planner.emptyMigration, then call
emptyPlan.renderTypeScript()) and only after successful render call
writeMigrationPackage/copyContractToMigrationDir/writeMigrationTs —
alternatively render and write into a temp directory and atomically move/commit
to packageDir after all steps succeed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/1-framework/3-tooling/cli/src/commands/migration-new.ts`:
- Around line 212-225: The current flow writes files (writeMigrationPackage,
copyContractToMigrationDir) before running planner/renderer, risking partial
scaffolds if migrations.createPlanner / planner.emptyMigration /
renderTypeScript() throws; change the sequence to produce the rendered
TypeScript first (call createControlStack, config.family.create,
migrations.createPlanner, planner.emptyMigration, then call
emptyPlan.renderTypeScript()) and only after successful render call
writeMigrationPackage/copyContractToMigrationDir/writeMigrationTs —
alternatively render and write into a temp directory and atomically move/commit
to packageDir after all steps succeed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 77a2ab7e-af0a-4023-b650-cc4f3cb1335d

📥 Commits

Reviewing files that changed from the base of the PR and between 4f120d6 and 5132b51.

📒 Files selected for processing (6)
  • examples/mongo-demo/migrations/20260415_add-posts-author-index/migration.ts
  • examples/retail-store/migrations/20260415_add-product-validation/migration.ts
  • packages/1-framework/1-core/errors/src/control.ts
  • packages/1-framework/3-tooling/cli/src/commands/migration-new.ts
  • packages/2-mongo-family/9-family/src/core/mongo-migration.ts
  • packages/3-mongo-target/1-mongo-target/test/migration-e2e.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/retail-store/migrations/20260415_add-product-validation/migration.ts

wmadden added 3 commits April 20, 2026 06:55
Replace the `...(existing?.authorship ? { authorship } : {})` ternary in
`buildAttestedManifest` with `ifDefined("authorship", existing?.authorship)`
to match the codebase convention for conditionally-included optional
properties under exactOptionalPropertyTypes.

Addresses review comment:
#354 (comment)
The preamble in `mongo-emit.ts` was duplicating the cross-cutting story
already documented in `@prisma-next/cli/lib/migration-emit` (CLI dispatch,
who attests `migration.json`, the relationship to `Migration.run`, why both
flows produce byte-identical artifacts). Two copies of the same narrative
will drift.

Trim the preamble to mongo-specific responsibilities only (accepted
authoring shapes, dynamic-import + structured error propagation, dispatch
on default-export shape, MigrationPlan-shape validation for the factory
form, ops.json persistence) and link to the framework-side helper for the
cross-cutting story.

Addresses review comment:
#354 (comment)
…ationPlanner directly

Drop the inline wrapper in `migrations.createPlanner` that bolted
`renderTypeScript()` onto plans and provided an out-of-band
`emptyMigration()`. Push both responsibilities into `PostgresMigrationPlanner`
itself so it implements `MigrationPlanner<"sql", "postgres">` directly:

  - `plan()` accepts the framework option shape (contract/schema as
    `unknown`, optional `fromHash`) and attaches the throwing
    `renderTypeScript()` stub to success plans, since Postgres is
    descriptor-flow and never authors plans back to TypeScript from a
    planner-produced plan.
  - `emptyMigration()` returns the descriptor-flow stub (a plan with no
    operations whose `renderTypeScript()` calls `renderDescriptorTypeScript`).
  - SQL-typed callers (notably this package`s unit tests) keep working
    because their stricter `SqlMigrationPlannerPlanOptions` payloads
    structurally satisfy the looser framework option shape, and the
    success plan still extends `SqlMigrationPlan<PostgresPlanTargetDetails>`.

`migrations.createPlanner` is now just `return createPostgresMigrationPlanner()`.

Addresses review comment:
#354 (comment)
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/1-framework/3-tooling/migration/src/migration-base.ts (1)

21-25: Use Schema.array() for the Arktype array field.

This schema is using the string DSL for an array where the repo standard is the typed .array() form.

♻️ Suggested change
 const MigrationMetaSchema = type({
   from: 'string',
   to: 'string',
   'kind?': "'regular' | 'baseline'",
-  'labels?': 'string[]',
+  'labels?': type('string').array(),
 });

As per coding guidelines, "Use Schema.array() for array definitions in Arktype (avoid tuple mistakes)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/migration/src/migration-base.ts` around lines
21 - 25, The MigrationMetaSchema currently defines the labels array using the
string DSL; change it to use Arktype's typed API by replacing the string-based
'labels?' definition with Schema.array(Schema.string()). Locate the
MigrationMetaSchema symbol in migration-base.ts and update the optional labels
field to Schema.array(Schema.string()) (or Schema.array(Schema.string(), {
optional: true }) depending on project convention) so the schema follows the
repo standard and avoids tuple/DSL pitfalls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/1-framework/3-tooling/migration/src/migration-base.ts`:
- Around line 161-167: The toContract stub is being force-cast to Contract
(toContract: Contract) while missing many required fields, causing
validateContract(migration.toContract) to fail at runtime; replace the blind
cast by either constructing a full minimal contract via the existing
minimalContract(...) helper (or analogous factory) and assign its result to
toContract, or define and use a dedicated ContractBookend/ContractStub type for
the migration bookend and change the toContract declaration to that type; update
usages that call validateContract to only validate when a real Contract is
present or adapt them to accept the new Bookend type (references: toContract,
Contract, validateContract, minimalContract).

---

Nitpick comments:
In `@packages/1-framework/3-tooling/migration/src/migration-base.ts`:
- Around line 21-25: The MigrationMetaSchema currently defines the labels array
using the string DSL; change it to use Arktype's typed API by replacing the
string-based 'labels?' definition with Schema.array(Schema.string()). Locate the
MigrationMetaSchema symbol in migration-base.ts and update the optional labels
field to Schema.array(Schema.string()) (or Schema.array(Schema.string(), {
optional: true }) depending on project convention) so the schema follows the
repo standard and avoids tuple/DSL pitfalls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: fa23396f-901a-4837-a07a-6ea0cd8493d3

📥 Commits

Reviewing files that changed from the base of the PR and between 5132b51 and adbf30b.

📒 Files selected for processing (4)
  • packages/1-framework/3-tooling/migration/src/migration-base.ts
  • packages/2-mongo-family/9-family/src/core/mongo-emit.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/planner.ts
  • packages/3-targets/3-targets/postgres/src/exports/control.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/2-mongo-family/9-family/src/core/mongo-emit.ts

Comment thread packages/1-framework/3-tooling/migration/src/migration-base.ts
…Schema labels

Replace the string-DSL `labels?: string[]` declaration with
`labels?: type("string").array()` per the repo arktype-usage rule
(prefer `Schema.array()` over the string form).

Behaviour-preserving; migration-tools typecheck + 152 tests pass.
@wmadden
Copy link
Copy Markdown
Contributor Author

wmadden commented Apr 20, 2026

Iterate-loop #4 follow-up on CodeRabbit review #4137274537 and #4136518291. Both findings were nitpicks attached to the review body (not to resolvable threads).

A27b — Arktype labels?: 'string[]' (migration-base.ts:21-25)

Done in 59bb1d928. MigrationMetaSchema now uses the chained factory form: 'labels?': type('string').array() per .cursor/rules/arktype-usage.mdc. @prisma-next/migration-tools typecheck + 152 tests pass.

A25a — migration new atomic scaffolding (render-before-write)

Valid atomicity concern, but an orthogonal hardening slice to the class-flow scaffolding this PR delivers. Deferred to TML-2278 so the render-first reorder (or staged temp-dir + atomic rename) lands with a partial-failure test.

A28 — migration-base.ts:161-167 as Contract cast

Already replied and resolved on the inline thread; deferred to TML-2271 with the other migration-base contract-bookend hardening.

@wmadden wmadden merged commit 1dbf4dc into main Apr 20, 2026
16 checks passed
wmadden added a commit that referenced this pull request Apr 20, 2026
Replace manual try/catch in the placeholder() test with
expect(() => ...).toThrow(expect.objectContaining({...})) per the
prefer-to-throw rule. The toEnvelope() coverage is preserved by
asserting against errorUnfilledPlaceholder() (the underlying builder)
without re-invoking the throwing call twice.

Addresses CodeRabbit review action A01 on PR #354.
wmadden added a commit that referenced this pull request Apr 20, 2026
Extend the comment-skip filter in lint-framework-target-imports.mjs to
also match `/*`, `/**`, and `*/` line prefixes so single-line block
and JSDoc comments containing the forbidden `@prisma-next/target-*`
substring do not trigger false-positive failures.

Addresses CodeRabbit review action A05 on PR #354.
wmadden added a commit that referenced this pull request Apr 20, 2026
Replace manual try/catch in the
"throws errorTargetHasIncompleteMigrationCapabilities when neither
capability is present" test with await expect(...).rejects.toMatchObject
per the prefer-to-throw rule. Avoids running emitMigration twice and
preserves all assertions (code, message, why substring).

Addresses CodeRabbit review action A06a on PR #354.
wmadden added a commit that referenced this pull request Apr 20, 2026
Replace the manual instanceof Error + duck-typed code-string check in
migration-new.ts with CliStructuredError.is(error). Aligns with the
pattern used across migration-emit, migration-plan, db-update, and
contract-emit. Real correctness gain: Node system errors (e.g. ENOENT)
have string code properties without conforming to CliStructuredError,
so they now correctly fall through to errorUnexpected(...).

Addresses CodeRabbit review action A06b on PR #354.
wmadden added a commit that referenced this pull request Apr 20, 2026
Replace manual try/catch in the no-hooks-registered case with a single
expect(() => migrationStrategy(...)).toThrow(expect.objectContaining({
code, meta })) per the prefer-to-throw rule. Drops the unused
CliStructuredError import.

Addresses CodeRabbit review action A06e on PR #354.
wmadden added a commit that referenced this pull request Apr 20, 2026
Replace the let thrown = false / try { ... } catch { thrown = true; }
control-flow flag in the PN-MIG-2001 propagation test with
await expect(executeCommand(...)).rejects.toBeDefined() per the
prefer-to-throw rule.

Addresses CodeRabbit review action A06f on PR #354.
wmadden added a commit that referenced this pull request Apr 20, 2026
Replace manual try/catch in the descriptor-flow renderTypeScript()
test with expect(() => ...).toThrow(expect.objectContaining({ code,
meta })) per the prefer-to-throw rule. Drops the now-unused
CliStructuredError import.

Addresses CodeRabbit review action A06g on PR #354.
wmadden added a commit that referenced this pull request Apr 20, 2026
Replace try/catch in the PN-MIG-2001 propagation test with
await expect(mongoEmit(...)).rejects.toMatchObject({ code, domain,
meta }) per the prefer-to-throw rule. Other tests in this file still
use the manual try/catch pattern (out of scope for this action) so
the CliStructuredError import is preserved.

Addresses CodeRabbit review action A06h on PR #354.
wmadden added a commit that referenced this pull request Apr 20, 2026
Use Vitest ctx.skip(reason) instead of an early return when the
family-mongo and target-mongo dist migration entrypoints are not
built. Silent early-return looks like a passing test in the runner
output; ctx.skip(reason) reports the test as skipped with a clear
explanation pointing at the missing build.

Addresses CodeRabbit review action A06i on PR #354.
wmadden added a commit that referenced this pull request Apr 20, 2026
Replace inline `...(this.meta.kind !== undefined ? { kind: ... } : {})`
spreads in PlannerProducedMongoMigration.renderTypeScript() with
`...ifDefined(key, value)` from @prisma-next/utils/defined per the
use-if-defined rule. Behaviour preserved.

Addresses CodeRabbit review action A06j on PR #354.
wmadden added a commit that referenced this pull request Apr 20, 2026
Extend the optional-metadata renderTypeScript test to assert that
`kind` and `labels` are actually embedded in the rendered source
(\`kind: "baseline"\`, \`labels: ["initial", "seed"]\`). Add a paired
case asserting that no `kind:` or `labels:` keys appear when the
metadata is omitted, documenting the absence contract too.

Addresses CodeRabbit review action A06k on PR #354.
wmadden added a commit that referenced this pull request Apr 20, 2026
Replace the `...(existing?.authorship ? { authorship } : {})` ternary in
`buildAttestedManifest` with `ifDefined("authorship", existing?.authorship)`
to match the codebase convention for conditionally-included optional
properties under exactOptionalPropertyTypes.

Addresses review comment:
#354 (comment)
wmadden added a commit that referenced this pull request Apr 20, 2026
The preamble in `mongo-emit.ts` was duplicating the cross-cutting story
already documented in `@prisma-next/cli/lib/migration-emit` (CLI dispatch,
who attests `migration.json`, the relationship to `Migration.run`, why both
flows produce byte-identical artifacts). Two copies of the same narrative
will drift.

Trim the preamble to mongo-specific responsibilities only (accepted
authoring shapes, dynamic-import + structured error propagation, dispatch
on default-export shape, MigrationPlan-shape validation for the factory
form, ops.json persistence) and link to the framework-side helper for the
cross-cutting story.

Addresses review comment:
#354 (comment)
@wmadden wmadden deleted the tml-2219-b-class-flow-scaffolding branch April 20, 2026 07:24
wmadden added a commit that referenced this pull request Apr 20, 2026
…ationPlanner directly

Drop the inline wrapper in `migrations.createPlanner` that bolted
`renderTypeScript()` onto plans and provided an out-of-band
`emptyMigration()`. Push both responsibilities into `PostgresMigrationPlanner`
itself so it implements `MigrationPlanner<"sql", "postgres">` directly:

  - `plan()` accepts the framework option shape (contract/schema as
    `unknown`, optional `fromHash`) and attaches the throwing
    `renderTypeScript()` stub to success plans, since Postgres is
    descriptor-flow and never authors plans back to TypeScript from a
    planner-produced plan.
  - `emptyMigration()` returns the descriptor-flow stub (a plan with no
    operations whose `renderTypeScript()` calls `renderDescriptorTypeScript`).
  - SQL-typed callers (notably this package`s unit tests) keep working
    because their stricter `SqlMigrationPlannerPlanOptions` payloads
    structurally satisfy the looser framework option shape, and the
    success plan still extends `SqlMigrationPlan<PostgresPlanTargetDetails>`.

`migrations.createPlanner` is now just `return createPostgresMigrationPlanner()`.

Addresses review comment:
#354 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants