Skip to content

Commit 7d22b3b

Browse files
feat(migration): close plan-only data safety with UAT-DATA gates for #686 (#753)
Add rename classification, connection-target blocking, rollback-failed verdicts, cleanup obligations, MCP migration_safety, and UAT-DATA-1..6 without contacting databases.
1 parent e605323 commit 7d22b3b

18 files changed

Lines changed: 739 additions & 52 deletions

File tree

‎docs/migration.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Migration safety
2+
3+
CodeDecay analyzes repo-local PostgreSQL migration SQL as a **plan-only** safety
4+
check. It does not connect to a database or apply migrations.
5+
6+
## What it can establish
7+
8+
- Operation classification (add/drop/rename/alter/index/backfill)
9+
- Rolling-deploy blockers for destructive or rename operations
10+
- NOT NULL without default/backfill as a static blocker
11+
- Five-state deployment matrix statuses
12+
- Connection-target classification (`localhost` vs production-looking hosts)
13+
- Cleanup obligations for disposable targets (plan recorded, not executed)
14+
- Verdicts: `plan-ready`, `plan-blocked`, `needs-execution-proof`, `not-fully-verified`
15+
16+
## What it cannot establish
17+
18+
- Existing-data compatibility
19+
- Lock duration / live rollback success
20+
- Mixed-version application behavior
21+
- A `fullyVerified: true` result (always false in this slice)
22+
23+
## CLI / MCP
24+
25+
```bash
26+
codedecay migration --file migration.sql --target-kind disposable-local --cleanup-plan "drop volume codedecay-mig"
27+
codedecay migration --file migration.sql --connection-host db.rds.amazonaws.com
28+
```
29+
30+
MCP tool: `migration_safety`.
31+
32+
Prisma schema-diff planning remains available through
33+
`createPrismaMigrationAdapterPlan` (read-only `prisma migrate diff`); applying
34+
migrations is still blocked by CodeDecay execution safety.

‎packages/cli/src/commands/migration.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,17 @@ export function runMigrationCommand(context: CliCommandContext, dependencies: Ru
1212
const options = parseMigrationArgs(context.args);
1313
const cwd = resolve(context.runtimeCwd, options.cwd ?? ".");
1414
const rootDir = dependencies.resolveRepoRoot(cwd, options);
15-
const report = analyzeMigrationSafety({ rootDir, files: options.files, rollbackFiles: options.rollbackFiles, targetKind: options.targetKind });
15+
const report = analyzeMigrationSafety({
16+
rootDir,
17+
files: options.files,
18+
rollbackFiles: options.rollbackFiles,
19+
targetKind: options.targetKind,
20+
connectionUrl: options.connectionUrl,
21+
connectionHost: options.connectionHost,
22+
databaseUrlEnv: options.databaseUrlEnv,
23+
cleanupPlan: options.cleanupPlan,
24+
rollbackFailed: options.rollbackFailed
25+
});
1626
const rendered = options.format === "json" ? `${JSON.stringify(report, null, 2)}\n` : renderMigrationSafetyMarkdown(report);
1727
dependencies.writeOutput({ cwd: rootDir, output: options.output, rendered, runtime: context.runtime });
1828
}

‎packages/cli/src/docs/command-docs/analysis.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,23 @@ export const ANALYSIS_COMMAND_DOCS: Record<string, CommandDoc> = {
1010
{ flag: "--file <path>", description: "Repo-local migration SQL file; repeat for multiple files" },
1111
{ flag: "--rollback-file <path>", description: "Repo-local rollback SQL file; repeat for multiple files" },
1212
{ flag: "--target-kind <kind>", description: "unspecified, disposable-local, remote-unapproved, or production-like" },
13+
{ flag: "--connection-url <url>", description: "Optional DB URL used only for host classification; secret values are redacted" },
14+
{ flag: "--connection-host <host>", description: "Optional DB host used for target classification" },
15+
{ flag: "--database-url-env <NAME>", description: "Env var name holding credentials; values are never read" },
16+
{ flag: "--cleanup-plan <text>", description: "Disposable database cleanup plan recorded in the report" },
17+
{ flag: "--rollback-failed", description: "Mark rollback as failed so the verdict stays not fully verified" },
1318
{ flag: "--cwd <path>", description: "Repository working directory (default: current directory)" },
1419
{ flag: "--format <format>", description: "json or markdown (default: markdown)" },
1520
{ flag: "--output <path>", description: "Write the plan to a file instead of stdout" }
1621
],
17-
examples: ["codedecay migration --file prisma/migrations/20260802_change/migration.sql --target-kind disposable-local", "codedecay migration --file migration.sql --target-kind production-like --format json"],
18-
notes: ["This command is plan-only: it reads no database secret, contacts no database, and applies no migration."]
22+
examples: [
23+
"codedecay migration --file prisma/migrations/20260802_change/migration.sql --target-kind disposable-local",
24+
"codedecay migration --file migration.sql --connection-host localhost --cleanup-plan \"drop docker volume codedecay-mig\" --format json"
25+
],
26+
notes: [
27+
"This command is plan-only: it reads no database secret, contacts no database, and applies no migration.",
28+
"See docs/migration.md for what plan-ready vs fully-verified means."
29+
]
1930
},
2031
runtime: {
2132
name: "runtime",

‎packages/cli/src/parsers/migration.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export function parseMigrationArgs(args: string[]): MigrationOptions {
88
const arg = args[index];
99
if (!arg) continue;
1010
if (arg === "--help" || arg === "-h") throw new HelpRequested();
11+
if (arg === "--rollback-failed") {
12+
options.rollbackFailed = true;
13+
continue;
14+
}
1115
const [flag, inline] = splitArg(arg);
1216
const value = () => inline ?? requireValue(args, index, flag);
1317
if (flag === "--file") options.files.push(value());
@@ -16,7 +20,14 @@ export function parseMigrationArgs(args: string[]): MigrationOptions {
1620
else if (flag === "--output") options.output = value();
1721
else if (flag === "--format") options.format = parseFormat(value());
1822
else if (flag === "--target-kind") options.targetKind = parseTarget(value());
19-
else { throwUnknownOption(arg, "migration"); continue; }
23+
else if (flag === "--connection-url") options.connectionUrl = value();
24+
else if (flag === "--connection-host") options.connectionHost = value();
25+
else if (flag === "--database-url-env") options.databaseUrlEnv = value();
26+
else if (flag === "--cleanup-plan") options.cleanupPlan = value();
27+
else {
28+
throwUnknownOption(arg, "migration");
29+
continue;
30+
}
2031
if (inline === undefined) index += 1;
2132
}
2233
return options;

‎packages/cli/src/types/migration.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,9 @@ export interface MigrationOptions {
88
targetKind: MigrationTargetKind;
99
format: ConfigFormat;
1010
output?: string | undefined;
11+
connectionUrl?: string | undefined;
12+
connectionHost?: string | undefined;
13+
databaseUrlEnv?: string | undefined;
14+
cleanupPlan?: string | undefined;
15+
rollbackFailed?: boolean | undefined;
1116
}

‎packages/knowledge/src/index.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,20 @@ export {
7070
export { ingestRuntimeEvidence, persistRuntimeEvidenceArtifact, RUNTIME_EVIDENCE_ARTIFACT_PATH } from "./runtime/ingest";
7171
export { analyzeMigrationSafety } from "./migration/analyze";
7272
export type { AnalyzeMigrationSafetyOptions } from "./migration/analyze";
73+
export { classifyMigrationConnectionTarget } from "./migration/target-safety";
7374
export { renderMigrationSafetyMarkdown } from "./migration/render";
7475
export { MIGRATION_EVIDENCE_SCHEMA_VERSION } from "./migration/types";
7576
export type {
77+
MigrationCleanupEvidence,
78+
MigrationConnectionTarget,
7679
MigrationMatrixState,
7780
MigrationOperationEvidence,
7881
MigrationOperationKind,
7982
MigrationRisk,
83+
MigrationRollbackStatus,
8084
MigrationSafetyReport,
81-
MigrationTargetKind
85+
MigrationTargetKind,
86+
MigrationVerdict
8287
} from "./migration/types";
8388
export type { IngestRuntimeEvidenceOptions } from "./runtime/ingest";
8489
export { renderRuntimeEvidenceMarkdown } from "./runtime/render";

0 commit comments

Comments
 (0)