gencow 0.1.182 → 0.1.185

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,12 +13,15 @@ import {
13
13
  normalizeDiagnosticLogLines,
14
14
  } from "./app-diagnostics.mjs";
15
15
  import { buildDeployClaimFromArchive } from "./deploy-package-claim.mjs";
16
+ import { formatDeployCorrelationId } from "./deploy-correlation-diagnostic.mjs";
16
17
  import { hasProjectDatabaseSchemaSource } from "./project-migration-manifest.mjs";
17
18
  import {
18
19
  extractActionableMigrationDiagnostic,
19
20
  isActionableMigrationDiagnostic,
21
+ normalizeRetiredMigrationGateDiagnostic,
20
22
  renderMigrationDiagnosticLines,
21
23
  } from "./migration-diagnostic.mjs";
24
+ import { emitMigrationAdvisories, emitMigrationResponsibilityNotice } from "./migration-advisory.mjs";
22
25
  import { resolveCreatedAppResponse } from "./app-create-response.mjs";
23
26
 
24
27
  export function resolveDevCloudAppName({
@@ -259,6 +262,7 @@ export function createDevCloudCommand(deps) {
259
262
  const form = new FormData();
260
263
  form.append("bundle", new Blob([bundle], { type: "application/gzip" }), "bundle.tar.gz");
261
264
 
265
+ if (schemaExists) emitMigrationResponsibilityNotice(deps.warnImpl);
262
266
  const res = await deps.platformFetchImpl(creds, `/platform/apps/${appName}/deploy`, {
263
267
  method: "POST",
264
268
  body: form,
@@ -276,7 +280,7 @@ export function createDevCloudCommand(deps) {
276
280
  deps.logImpl(`${deps.DIM}${ts2}${deps.RESET} ${deps.RED}[deploy]${deps.RESET} ✗ Deploy failed!`);
277
281
  const errMsg = data.error || "Unknown error";
278
282
  const migrationDiagnostic = isActionableMigrationDiagnostic(data?.diagnostic)
279
- ? extractActionableMigrationDiagnostic(data.diagnostic)
283
+ ? normalizeRetiredMigrationGateDiagnostic(extractActionableMigrationDiagnostic(data.diagnostic))
280
284
  : null;
281
285
  if (migrationDiagnostic) {
282
286
  for (const line of renderMigrationDiagnosticLines(migrationDiagnostic)) {
@@ -284,6 +288,8 @@ export function createDevCloudCommand(deps) {
284
288
  }
285
289
  } else {
286
290
  deps.logImpl(` ${deps.RED}${errMsg}${deps.RESET}`);
291
+ const correlationLine = formatDeployCorrelationId(data?.correlationId);
292
+ if (correlationLine) deps.logImpl(` ${correlationLine}`);
287
293
  }
288
294
 
289
295
  let diagnosticLines = normalizeDiagnosticLogLines(data.crashLogs);
@@ -315,6 +321,7 @@ export function createDevCloudCommand(deps) {
315
321
  return;
316
322
  }
317
323
 
324
+ emitMigrationAdvisories(data?.warnings, deps.warnImpl);
318
325
  const ts2 = new Date().toLocaleTimeString("en-US", { hour12: false });
319
326
  deps.logImpl(
320
327
  `${deps.DIM}${ts2}${deps.RESET} ${deps.GREEN}[deploy]${deps.RESET} ✔ Deploy complete ${deps.DIM}(${sizeKB}KB, ${elapsed}s)${deps.RESET}`,
@@ -395,10 +402,12 @@ export function createDevCloudCommand(deps) {
395
402
  }, delayMs);
396
403
  }
397
404
 
398
- const { readdirSync, watch } = await import("fs");
405
+ const fsModule = deps.readdirSyncImpl && deps.watchImpl ? null : await import("fs");
406
+ const readdirSyncImpl = deps.readdirSyncImpl ?? fsModule.readdirSync;
407
+ const watchImpl = deps.watchImpl ?? fsModule.watch;
399
408
  const watchedFiles = [];
400
409
  try {
401
- const files = readdirSync(absoluteFunctions).filter(
410
+ const files = readdirSyncImpl(absoluteFunctions).filter(
402
411
  (file) => file.endsWith(".ts") && !file.startsWith(".") && file !== "api.ts",
403
412
  );
404
413
  watchedFiles.push(...files);
@@ -428,7 +437,7 @@ ${deps.BOLD}${deps.CYAN}🚀 Gencow Cloud Dev${deps.RESET}
428
437
  let debounceTimer = null;
429
438
  let pendingFile = null;
430
439
  const GENERATED_FILES = new Set(["api.ts", "README.md"]);
431
- watch(absoluteFunctions, { recursive: true }, (_eventType, filename) => {
440
+ watchImpl(absoluteFunctions, { recursive: true }, (_eventType, filename) => {
432
441
  if (!filename || filename.includes("node_modules")) return;
433
442
  if (isServerCodegenWatchPath(filename, cwd, config, { resolvePathImpl: deps.resolvePathImpl })) return;
434
443
  const base = (filename.includes("/") ? filename.split("/").pop() : filename) ?? filename;
@@ -0,0 +1,41 @@
1
+ const ADVISORY_SUMMARIES = Object.freeze({
2
+ MIGRATION_POTENTIAL_DATA_LOSS: "The pending migration may remove tenant data.",
3
+ MIGRATION_LOCK_RISK: "The pending migration may take a blocking PostgreSQL lock.",
4
+ MIGRATION_COMPATIBILITY_RISK: "Coordinate this schema change with application rollout compatibility.",
5
+ MIGRATION_BACKUP_NOT_CREATED:
6
+ "Gencow could not create an automatic backup; verify your own recovery point.",
7
+ });
8
+
9
+ export function extractMigrationAdvisories(value) {
10
+ if (!Array.isArray(value)) return [];
11
+ const advisories = [];
12
+ const seen = new Set();
13
+ for (const item of value.slice(0, 16)) {
14
+ const code = typeof item?.code === "string" ? item.code : "";
15
+ if (!(code in ADVISORY_SUMMARIES) || seen.has(code)) continue;
16
+ seen.add(code);
17
+ advisories.push({
18
+ code,
19
+ severity: "warning",
20
+ responsibility: "user",
21
+ summary: ADVISORY_SUMMARIES[code],
22
+ });
23
+ }
24
+ return advisories;
25
+ }
26
+
27
+ export function emitMigrationAdvisories(value, warnImpl) {
28
+ const advisories = extractMigrationAdvisories(value);
29
+ for (const advisory of advisories) {
30
+ warnImpl(
31
+ `Migration warning [${advisory.code}]: ${advisory.summary} Review and proceed at your discretion.`,
32
+ );
33
+ }
34
+ return advisories;
35
+ }
36
+
37
+ export function emitMigrationResponsibilityNotice(warnImpl) {
38
+ warnImpl(
39
+ "Migration responsibility: review pending SQL and verify your own recovery point before execution; automatic backups are best-effort.",
40
+ );
41
+ }
@@ -8,6 +8,56 @@ export {
8
8
  isActionableMigrationDiagnostic,
9
9
  } from "./migration-diagnostic-contract.mjs";
10
10
 
11
+ const RETIRED_PLATFORM_GATES = new Set([
12
+ "MIGRATION_BUNDLE_GENERATOR_UNSUPPORTED",
13
+ "MIGRATION_SQL_DESTRUCTIVE",
14
+ "MIGRATION_SQL_UNSUPPORTED",
15
+ "PRE_DEPLOY_SAFETY_BACKUP_REQUIRED",
16
+ "MIGRATION_CHECK_STATE_UNAVAILABLE",
17
+ ]);
18
+
19
+ export function normalizeRetiredMigrationGateDiagnostic(diagnostic) {
20
+ if (
21
+ !diagnostic ||
22
+ !RETIRED_PLATFORM_GATES.has(diagnostic.code) ||
23
+ diagnostic.databaseState !== "UNCHANGED" ||
24
+ !["PREFLIGHT", "AUDIT", "REHEARSAL", "VERIFY"].includes(diagnostic.phase)
25
+ ) {
26
+ return diagnostic;
27
+ }
28
+ if (
29
+ diagnostic.failureClass === "RETRYABLE_INFRA" &&
30
+ diagnostic.retryPolicy === "AFTER_DELAY" &&
31
+ diagnostic.nextAction?.kind === "WAIT"
32
+ ) {
33
+ return diagnostic;
34
+ }
35
+ return {
36
+ diagnosticVersion: 1,
37
+ code: diagnostic.code,
38
+ failureClass: "RETRYABLE_INFRA",
39
+ phase: diagnostic.phase,
40
+ databaseState: "UNCHANGED",
41
+ safeToRetryNow: false,
42
+ retryPolicy: "AFTER_DELAY",
43
+ retryAfterSeconds: 15,
44
+ retryCondition: "Retry the same immutable bundle after the Blue-Green platform rollout converges.",
45
+ correlationId: diagnostic.correlationId,
46
+ reason: {
47
+ code: "retired_platform_gate",
48
+ summary: "An older platform slot applied a migration gate that is no longer enforced.",
49
+ detail:
50
+ "No tenant database mutation occurred. Keep the project unchanged while the current platform policy finishes rolling out.",
51
+ },
52
+ suggestedFixes: [],
53
+ nextAction: { kind: "WAIT" },
54
+ forbiddenActions: [],
55
+ redaction: { serverReturnedRawSql: false, localExcerpt: "NONE" },
56
+ ...(diagnostic.docsUrl ? { docsUrl: diagnostic.docsUrl } : {}),
57
+ ...(diagnostic.supportedCliRange ? { supportedCliRange: diagnostic.supportedCliRange } : {}),
58
+ };
59
+ }
60
+
11
61
  function skipSingleQuoted(sql, start) {
12
62
  let index = start + 1;
13
63
  while (index < sql.length) {
@@ -161,6 +211,7 @@ export function enrichMigrationDiagnostic(
161
211
  projectRoot,
162
212
  functionsDir,
163
213
  retryCommand,
214
+ inspectionCommand,
164
215
  readFileSyncImpl = readFileSync,
165
216
  realpathSyncImpl = realpathSync,
166
217
  },
@@ -171,9 +222,12 @@ export function enrichMigrationDiagnostic(
171
222
  diagnostic.safeToRetryNow === true &&
172
223
  diagnostic.retryPolicy !== "BLOCKED" &&
173
224
  (diagnostic.nextAction?.kind === "EDIT_AND_CHECK" || diagnostic.nextAction?.kind === "RETRY");
225
+ const canAttachInspectionCommand = inspectionCommand && diagnostic.nextAction?.kind === "INSPECT_STATE";
174
226
  const nextAction = canAttachRetryCommand
175
227
  ? { kind: diagnostic.nextAction.kind, command: retryCommand }
176
- : { kind: diagnostic.nextAction?.kind };
228
+ : canAttachInspectionCommand
229
+ ? { kind: "INSPECT_STATE", command: inspectionCommand }
230
+ : { kind: diagnostic.nextAction?.kind };
177
231
  const base = {
178
232
  ...diagnostic,
179
233
  ...(diagnostic.migration
@@ -230,9 +284,30 @@ export function enrichMigrationDiagnostic(
230
284
  }
231
285
  }
232
286
 
287
+ function migrationDiagnosticTitle(diagnostic) {
288
+ if (diagnostic.failureClass === "RECOVERY_REQUIRED" || diagnostic.databaseState === "RECOVERY_REQUIRED") {
289
+ return "Migration outcome verification required";
290
+ }
291
+ if (diagnostic.databaseState === "ROLLED_BACK") return "Migration rolled back";
292
+ if (diagnostic.failureClass === "USER_FIXABLE") return "Migration input needs attention";
293
+ if (diagnostic.code === "SCHEMA_PUSH_IN_PROGRESS") return "Migration waiting for an active operation";
294
+ if (diagnostic.nextAction?.kind === "INSPECT_STATE") return "Migration state inspection required";
295
+ if (diagnostic.failureClass === "RETRYABLE_INFRA") return "Migration check temporarily unavailable";
296
+ return "Migration action required";
297
+ }
298
+
299
+ function shouldRenderForbiddenActions(diagnostic) {
300
+ return (
301
+ diagnostic.failureClass === "RECOVERY_REQUIRED" ||
302
+ /(?:HASH_DRIFT|FILE_MISSING|HISTORY|JOURNAL|POLICY_VIOLATION|SECURITY_BOUNDARY)/u.test(
303
+ diagnostic.code ?? "",
304
+ )
305
+ );
306
+ }
307
+
233
308
  export function renderMigrationDiagnosticLines(diagnostic) {
234
309
  const lines = [
235
- "Migration blocked",
310
+ migrationDiagnosticTitle(diagnostic),
236
311
  "",
237
312
  `Code: ${diagnostic.code}`,
238
313
  `Responsibility: ${diagnostic.failureClass}`,
@@ -267,12 +342,13 @@ export function renderMigrationDiagnosticLines(diagnostic) {
267
342
  lines.push(` ${index + 1}. ${fix.title}`, ` ${fix.detail}`),
268
343
  );
269
344
  }
270
- if (diagnostic.forbiddenActions?.length > 0) {
345
+ if (shouldRenderForbiddenActions(diagnostic) && diagnostic.forbiddenActions?.length > 0) {
271
346
  lines.push("", "Do not:");
272
347
  diagnostic.forbiddenActions.forEach((action) => lines.push(` - ${action}`));
273
348
  }
274
349
  const nextText =
275
- diagnostic.nextAction?.kind === "VIEW_STATUS" && diagnostic.nextAction?.command
350
+ (diagnostic.nextAction?.kind === "VIEW_STATUS" || diagnostic.nextAction?.kind === "INSPECT_STATE") &&
351
+ diagnostic.nextAction?.command
276
352
  ? diagnostic.nextAction.command
277
353
  : diagnostic.safeToRetryNow && diagnostic.nextAction?.command
278
354
  ? diagnostic.nextAction.command
@@ -282,7 +358,7 @@ export function renderMigrationDiagnosticLines(diagnostic) {
282
358
  INSPECT_STATE:
283
359
  diagnostic.code === "MIGRATION_LOCAL_EXECUTION_FAILED"
284
360
  ? "Inspect the local Drizzle journal and database catalog before retrying"
285
- : "Run read-only gencow db:check for the same app before any new mutation request",
361
+ : "Run the platform's read-only migration-state inspection before any new mutation request",
286
362
  UPDATE_CLI: "Update the CLI to the exact supported range shown above",
287
363
  VIEW_STATUS: "View the exact operation receipt before any new mutation request",
288
364
  RETRY: "Regenerate a stable bundle and retry from a fresh plan",
@@ -3,6 +3,7 @@ import {
3
3
  extractMigrationSchemaOperationReceipt,
4
4
  extractPrimaryMigrationExecutionResult,
5
5
  } from "@gencow/migration-contract";
6
+ import { extractMigrationAdvisories } from "./migration-advisory.mjs";
6
7
 
7
8
  const RECEIPT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
8
9
 
@@ -53,7 +54,7 @@ export function extractMigrationPushSuccess(data, expected) {
53
54
  ) {
54
55
  return null;
55
56
  }
56
- return { migration, receipt };
57
+ return { migration, receipt, warnings: extractMigrationAdvisories(data?.warnings) };
57
58
  }
58
59
 
59
60
  export async function fetchMigrationOperationStatus({
@@ -0,0 +1,32 @@
1
+ export const PROJECT_CONFIG_CANDIDATES = Object.freeze(["gencow.config.js", "gencow.config.ts"]);
2
+
3
+ const reportedSelections = new Set();
4
+
5
+ export function selectProjectConfigFile(availableFiles) {
6
+ const available = new Set(availableFiles);
7
+ const detectedFiles = PROJECT_CONFIG_CANDIDATES.filter((file) => available.has(file));
8
+ return {
9
+ selectedFile: detectedFiles[0] ?? null,
10
+ ignoredFiles: detectedFiles.slice(1),
11
+ };
12
+ }
13
+
14
+ export function detectProjectConfigFile({ cwd, existsSyncImpl, resolvePathImpl }) {
15
+ return selectProjectConfigFile(
16
+ PROJECT_CONFIG_CANDIDATES.filter((file) => existsSyncImpl(resolvePathImpl(cwd, file))),
17
+ );
18
+ }
19
+
20
+ export function formatProjectConfigSelectionWarning(selection) {
21
+ if (!selection.selectedFile || selection.ignoredFiles.length === 0) return null;
22
+ return `[gencow] Multiple project config files found. Using \`${selection.selectedFile}\`; ignoring deprecated \`${selection.ignoredFiles.join("`, `")}\`.`;
23
+ }
24
+
25
+ export function warnProjectConfigSelectionOnce({ cwd, selection, warnImpl }) {
26
+ const warning = formatProjectConfigSelectionWarning(selection);
27
+ if (!warning) return;
28
+ const key = `${cwd}\0${selection.selectedFile}\0${selection.ignoredFiles.join("\0")}`;
29
+ if (reportedSelections.has(key)) return;
30
+ reportedSelections.add(key);
31
+ warnImpl(warning);
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.182",
3
+ "version": "0.1.185",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,10 +24,10 @@
24
24
  "drizzle-orm": "1.0.0-rc.4",
25
25
  "esbuild": "^0.27.7",
26
26
  "open": "^10.2.0",
27
- "tar": "^7.5.21",
27
+ "tar": "7.5.15",
28
28
  "ws": "^8.21.1",
29
29
  "zod": "^4.4.3",
30
- "@gencow/migration-contract": "0.1.2"
30
+ "@gencow/migration-contract": "0.1.8"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^25.9.5",
@@ -14611,9 +14611,6 @@ var gencowConfigSchema = rawConfigSchema.transform((v) => {
14611
14611
  const trimPath = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
14612
14612
  const rootDir = trimPath(v.rootDir);
14613
14613
  const functionsDir = trimPath(v.functionsDir);
14614
- if (rootDir && functionsDir) {
14615
- throw new Error("[gencow] Invalid gencow.config: set either `rootDir` or `functionsDir`, not both.");
14616
- }
14617
14614
  const resolvedRootDir = rootDir ?? functionsDir ?? GENCOW_CONFIG_DEFAULTS.rootDir;
14618
14615
  const envFile = trimPath(v.envFile) ?? `${resolvedRootDir.replace(/[\\/]+$/, "")}/.env`;
14619
14616
  const clientCodegenDir = trimPath(v.codegen?.clientOutDir) ?? trimPath(v.codegen?.outDir) ?? GENCOW_CONFIG_DEFAULTS.codegen.clientOutDir;