@prisma-next/cli 0.14.0-dev.49 → 0.14.0-dev.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/cli.mjs +13 -13
  2. package/dist/{client-CWTNqJdj.mjs → client-DL6KE_c0.mjs} +22 -37
  3. package/dist/client-DL6KE_c0.mjs.map +1 -0
  4. package/dist/{command-helpers-6cNJZ863.mjs → command-helpers-DMG0RsR1.mjs} +3 -3
  5. package/dist/command-helpers-DMG0RsR1.mjs.map +1 -0
  6. package/dist/commands/contract-emit.mjs +1 -1
  7. package/dist/commands/contract-infer.mjs +1 -1
  8. package/dist/commands/db-init.mjs +3 -3
  9. package/dist/commands/db-schema.mjs +3 -3
  10. package/dist/commands/db-sign.mjs +4 -4
  11. package/dist/commands/db-update.mjs +4 -4
  12. package/dist/commands/db-verify.mjs +1 -1
  13. package/dist/commands/migrate.d.mts +1 -1
  14. package/dist/commands/migrate.mjs +6 -6
  15. package/dist/commands/migrate.mjs.map +1 -1
  16. package/dist/commands/migration-check.d.mts +1 -1
  17. package/dist/commands/migration-check.mjs +1 -1
  18. package/dist/commands/migration-graph.d.mts +1 -1
  19. package/dist/commands/migration-graph.mjs +3 -3
  20. package/dist/commands/migration-list.d.mts +1 -1
  21. package/dist/commands/migration-list.mjs +1 -1
  22. package/dist/commands/migration-log.d.mts +1 -1
  23. package/dist/commands/migration-log.mjs +1 -1
  24. package/dist/commands/migration-new.mjs +3 -3
  25. package/dist/commands/migration-plan.d.mts.map +1 -1
  26. package/dist/commands/migration-plan.mjs +1 -1
  27. package/dist/commands/migration-show.mjs +3 -3
  28. package/dist/commands/migration-status.d.mts +1 -1
  29. package/dist/commands/migration-status.mjs +1 -1
  30. package/dist/commands/ref.d.mts +1 -1
  31. package/dist/commands/ref.mjs +2 -2
  32. package/dist/commands/telemetry/index.mjs +1 -1
  33. package/dist/{contract-at-errors-DdxWRjMc.mjs → contract-at-errors-bF4CGDl0.mjs} +2 -2
  34. package/dist/{contract-at-errors-DdxWRjMc.mjs.map → contract-at-errors-bF4CGDl0.mjs.map} +1 -1
  35. package/dist/{contract-emit-CMwCj7_m.mjs → contract-emit-BM7oxYIG.mjs} +3 -3
  36. package/dist/{contract-emit-CMwCj7_m.mjs.map → contract-emit-BM7oxYIG.mjs.map} +1 -1
  37. package/dist/{contract-emit-klH7zzaV.mjs → contract-emit-DnCG2JaQ.mjs} +3 -3
  38. package/dist/{contract-emit-klH7zzaV.mjs.map → contract-emit-DnCG2JaQ.mjs.map} +1 -1
  39. package/dist/{contract-infer-B__56PGb.mjs → contract-infer-Dq9XNQCx.mjs} +3 -3
  40. package/dist/{contract-infer-B__56PGb.mjs.map → contract-infer-Dq9XNQCx.mjs.map} +1 -1
  41. package/dist/{contract-space-aggregate-loader-Bu2fjSWV.mjs → contract-space-aggregate-loader-DAa8u6bi.mjs} +2 -2
  42. package/dist/{contract-space-aggregate-loader-Bu2fjSWV.mjs.map → contract-space-aggregate-loader-DAa8u6bi.mjs.map} +1 -1
  43. package/dist/{db-verify-DlqHvkkA.mjs → db-verify-BpfJvC8O.mjs} +14 -38
  44. package/dist/db-verify-BpfJvC8O.mjs.map +1 -0
  45. package/dist/exports/control-api.d.mts +1 -1
  46. package/dist/exports/control-api.mjs +2 -2
  47. package/dist/exports/index.mjs +2 -2
  48. package/dist/{format-C3XdaDuA.mjs → format-DrUSkdqR.mjs} +2 -2
  49. package/dist/{format-C3XdaDuA.mjs.map → format-DrUSkdqR.mjs.map} +1 -1
  50. package/dist/{framework-components-5RljWT7Q.mjs → framework-components-BdmVlRSS.mjs} +2 -2
  51. package/dist/{framework-components-5RljWT7Q.mjs.map → framework-components-BdmVlRSS.mjs.map} +1 -1
  52. package/dist/{init-BF3s9NcB.mjs → init-C0YyVoGj.mjs} +3 -3
  53. package/dist/{init-BF3s9NcB.mjs.map → init-C0YyVoGj.mjs.map} +1 -1
  54. package/dist/{inspect-live-schema-Dd1Ft0dw.mjs → inspect-live-schema-Bs_0M6z8.mjs} +3 -3
  55. package/dist/{inspect-live-schema-Dd1Ft0dw.mjs.map → inspect-live-schema-Bs_0M6z8.mjs.map} +1 -1
  56. package/dist/{migration-check-C9F4RopI.mjs → migration-check-DYxrNfK-.mjs} +4 -5
  57. package/dist/{migration-check-C9F4RopI.mjs.map → migration-check-DYxrNfK-.mjs.map} +1 -1
  58. package/dist/{migration-command-scaffold-DW2BpjZW.mjs → migration-command-scaffold-DB5DQLvF.mjs} +3 -3
  59. package/dist/{migration-command-scaffold-DW2BpjZW.mjs.map → migration-command-scaffold-DB5DQLvF.mjs.map} +1 -1
  60. package/dist/{migration-list-BOzQzJK4.mjs → migration-list-BX5w9HAn.mjs} +3 -3
  61. package/dist/{migration-list-BOzQzJK4.mjs.map → migration-list-BX5w9HAn.mjs.map} +1 -1
  62. package/dist/{migration-log-q50rqMiU.mjs → migration-log-CB1KzVOG.mjs} +3 -3
  63. package/dist/{migration-log-q50rqMiU.mjs.map → migration-log-CB1KzVOG.mjs.map} +1 -1
  64. package/dist/{migration-path-target-zK6RG6JY.mjs → migration-path-target-CEFRK2Q7.mjs} +2 -2
  65. package/dist/{migration-path-target-zK6RG6JY.mjs.map → migration-path-target-CEFRK2Q7.mjs.map} +1 -1
  66. package/dist/{migration-plan-BwJLPkwV.mjs → migration-plan-DMKl_HnV.mjs} +11 -10
  67. package/dist/migration-plan-DMKl_HnV.mjs.map +1 -0
  68. package/dist/{migration-status-RrGUf39y.mjs → migration-status-BeNAmEju.mjs} +5 -6
  69. package/dist/{migration-status-RrGUf39y.mjs.map → migration-status-BeNAmEju.mjs.map} +1 -1
  70. package/dist/{telemetry-BckOSUWG.mjs → telemetry-_19qtfXp.mjs} +2 -2
  71. package/dist/{telemetry-BckOSUWG.mjs.map → telemetry-_19qtfXp.mjs.map} +1 -1
  72. package/dist/{types-CKgJmH02.d.mts → types-DFoqNIcQ.d.mts} +2 -2
  73. package/dist/{types-CKgJmH02.d.mts.map → types-DFoqNIcQ.d.mts.map} +1 -1
  74. package/dist/{verify-DC14_nPu.mjs → verify-Djo-Jep_.mjs} +36 -161
  75. package/dist/verify-Djo-Jep_.mjs.map +1 -0
  76. package/package.json +21 -21
  77. package/src/commands/db-verify.ts +1 -1
  78. package/src/commands/migrate.ts +4 -4
  79. package/src/commands/migration-plan.ts +11 -0
  80. package/src/control-api/operations/db-run.ts +13 -12
  81. package/src/control-api/operations/db-verify.ts +16 -13
  82. package/src/control-api/operations/migrate.ts +8 -8
  83. package/src/control-api/operations/run-migration.ts +5 -4
  84. package/src/control-api/types.ts +1 -1
  85. package/src/utils/combine-verify-results.ts +8 -27
  86. package/src/utils/formatters/errors.ts +12 -4
  87. package/src/utils/formatters/verify.ts +50 -230
  88. package/dist/client-CWTNqJdj.mjs.map +0 -1
  89. package/dist/command-helpers-6cNJZ863.mjs.map +0 -1
  90. package/dist/db-verify-DlqHvkkA.mjs.map +0 -1
  91. package/dist/migration-plan-BwJLPkwV.mjs.map +0 -1
  92. package/dist/verify-DC14_nPu.mjs.map +0 -1
@@ -45,10 +45,11 @@ export interface RunMigrationInputs<TFamilyId extends string, TTargetId extends
45
45
  readonly aggregate: ContractSpaceAggregate;
46
46
  /**
47
47
  * Per-space plans, keyed by `spaceId`. Produced by either the full
48
- * {@link planMigration} pipeline (`db init` / `db update` — synth
49
- * for the app, graph-walk for extensions) or by direct
50
- * {@link graphWalkStrategy} calls (`migrate` — graph-walk
51
- * for every contract space). Either way, the runner consumes the same shape.
48
+ * {@link planMigration} pipeline (`db init` / `db update` —
49
+ * `planFromDiff` for the app, `resolveRecordedPath` for extensions) or
50
+ * by direct {@link resolveRecordedPath} calls (`migrate` — resolves
51
+ * the recorded path for every contract space). Either way, the runner
52
+ * consumes the same shape.
52
53
  */
53
54
  readonly perSpacePlans: ReadonlyMap<string, PerSpacePlan>;
54
55
  /**
@@ -545,7 +545,7 @@ export type EmitResult = Result<EmitSuccess, EmitFailure>;
545
545
  *
546
546
  * The control-api operation is responsible for: loading the
547
547
  * contract-space aggregate, reading per-space marker rows from the
548
- * live database, plotting per-space paths via `graphWalkStrategy`
548
+ * live database, plotting per-space paths via `resolveRecordedPath`
549
549
  * (replay-only — no synth, no introspection), and dispatching
550
550
  * through the shared `runMigration` primitive. The CLI command
551
551
  * just resolves the descriptor surface (config, refs, contract
@@ -12,11 +12,11 @@ export interface CombinedVerifyResult {
12
12
 
13
13
  /**
14
14
  * Collapse the aggregate verifier's per-space contract-satisfaction results
15
- * into a single {@link VerifyDatabaseSchemaResult} for the existing CLI
16
- * display surface, and carry the deduplicated unclaimed-elements list
17
- * alongside it. Concatenates issues across spaces; sums counts; uses the app
18
- * space's result as the structural envelope (storage hash, target). Extras are
19
- * already stripped from each per-space result, so nothing here duplicates an
15
+ * into a single {@link VerifyDatabaseSchemaResult} for the CLI display
16
+ * surface, and carry the deduplicated unclaimed-elements list alongside it.
17
+ * Concatenates the issue lists across spaces; uses the app space's result as
18
+ * the structural envelope (storage hash, target). Extras are already
19
+ * stripped from each per-space result, so nothing here duplicates an
20
20
  * unclaimed element per space.
21
21
  *
22
22
  * **Unclaimed disposition.** In strict mode a non-empty `unclaimed` list fails
@@ -50,21 +50,14 @@ export function combineVerifyResults(
50
50
  let okAll = true;
51
51
  let firstFailure: VerifyDatabaseSchemaResult | undefined;
52
52
  let issues: VerifyDatabaseSchemaResult['schema']['issues'] = [];
53
- let schemaDiffIssues: VerifyDatabaseSchemaResult['schema']['schemaDiffIssues'] = [];
54
- const counts = { pass: 0, warn: 0, fail: 0, totalNodes: 0 };
55
- const childRoots: Array<VerifyDatabaseSchemaResult['schema']['root']> = [];
53
+ let warningIssues: VerifyDatabaseSchemaResult['schema']['issues'] = [];
56
54
  for (const [, result] of perSpace) {
57
55
  if (!result.ok) {
58
56
  okAll = false;
59
57
  if (firstFailure === undefined) firstFailure = result;
60
58
  }
61
59
  issues = [...issues, ...result.schema.issues];
62
- schemaDiffIssues = [...schemaDiffIssues, ...result.schema.schemaDiffIssues];
63
- counts.pass += result.schema.counts.pass;
64
- counts.warn += result.schema.counts.warn;
65
- counts.fail += result.schema.counts.fail;
66
- counts.totalNodes += result.schema.counts.totalNodes;
67
- childRoots.push(result.schema.root);
60
+ warningIssues = [...warningIssues, ...(result.schema.warnings?.issues ?? [])];
68
61
  }
69
62
 
70
63
  const unclaimedFails = strict && unclaimed.length > 0;
@@ -91,19 +84,7 @@ export function combineVerifyResults(
91
84
  target: appResult.target,
92
85
  schema: {
93
86
  issues,
94
- schemaDiffIssues,
95
- root: {
96
- status: ok ? 'pass' : 'fail',
97
- kind: 'aggregate',
98
- name: 'aggregate',
99
- contractPath: '',
100
- code: 'AGGREGATE',
101
- message: ok ? 'Aggregate schema matches' : 'Aggregate schema mismatch',
102
- expected: undefined,
103
- actual: undefined,
104
- children: childRoots,
105
- },
106
- counts,
87
+ warnings: { issues: warningIssues },
107
88
  },
108
89
  meta: { strict },
109
90
  timings: { total: 0 },
@@ -48,9 +48,17 @@ export function formatErrorOutput(error: CliErrorEnvelope, flags: GlobalFlags):
48
48
  }
49
49
  }
50
50
  }
51
- // Show issues list if present (always show a short list; show full list when verbose)
51
+ // Show issues list if present (always show a short list; show full list when verbose).
52
+ // `issues` is a shared error-envelope field: PSL interpretation diagnostics stamp
53
+ // `kind` with their diagnostic code (e.g. `PSL_ORPHANED_BACKRELATION_LIST`); schema-diff
54
+ // issues carry no `kind` and stamp `reason` instead (e.g. `not-found`). Prefer whichever
55
+ // the producer supplied.
52
56
  if (error.meta?.['issues']) {
53
- const issues = error.meta['issues'] as readonly { kind?: string; message?: string }[];
57
+ const issues = error.meta['issues'] as readonly {
58
+ kind?: string;
59
+ reason?: string;
60
+ message?: string;
61
+ }[];
54
62
  if (issues.length > 0) {
55
63
  const maxToShow = isVerbose(flags, 1) ? issues.length : Math.min(3, issues.length);
56
64
  const header = isVerbose(flags, 1)
@@ -58,9 +66,9 @@ export function formatErrorOutput(error: CliErrorEnvelope, flags: GlobalFlags):
58
66
  : ` Issues (showing ${maxToShow} of ${issues.length}):`;
59
67
  lines.push(`${formatDimText(header)}`);
60
68
  for (const issue of issues.slice(0, maxToShow)) {
61
- const kind = issue.kind ?? 'issue';
69
+ const label = issue.kind ?? issue.reason ?? 'issue';
62
70
  const message = issue.message ?? '';
63
- lines.push(`${formatDimText(` - [${kind}] ${message}`)}`);
71
+ lines.push(`${formatDimText(` - [${label}] ${message}`)}`);
64
72
  }
65
73
  if (!isVerbose(flags, 1) && issues.length > maxToShow) {
66
74
  lines.push(`${formatDimText(' Re-run with -v/--verbose to see all issues')}`);
@@ -1,8 +1,9 @@
1
1
  import type {
2
2
  CoreSchemaView,
3
+ ExpectationFailureReason,
3
4
  IntrospectSchemaResult,
5
+ SchemaDiffIssue,
4
6
  SchemaTreeNode,
5
- SchemaVerificationNode,
6
7
  SignDatabaseResult,
7
8
  VerifyDatabaseResult,
8
9
  VerifyDatabaseSchemaResult,
@@ -12,6 +13,23 @@ import { bold, cyan, dim, green, magenta, red, yellow } from 'colorette';
12
13
  import type { GlobalFlags } from '../global-flags';
13
14
  import { createColorFormatter, formatDim, isVerbose } from './helpers';
14
15
 
16
+ /** Human-readable label for each failure reason, prefixed onto an issue's message for display. */
17
+ const REASON_LABEL: Record<ExpectationFailureReason, string> = {
18
+ 'not-found': 'missing',
19
+ 'not-expected': 'extra',
20
+ 'not-equal': 'mismatch',
21
+ };
22
+
23
+ /**
24
+ * The issue's display text: its own message (the differ's `path`), prefixed
25
+ * with a human label for why it's flagged. Turning `reason` into a label is
26
+ * this formatter's job, not the differ's — the differ's issue is data
27
+ * (`path` + `reason` + nodes), not prose.
28
+ */
29
+ function formatIssueMessage(issue: SchemaDiffIssue): string {
30
+ return `${REASON_LABEL[issue.reason]}: ${issue.message}`;
31
+ }
32
+
15
33
  // ============================================================================
16
34
  // Verify Output Formatters
17
35
  // ============================================================================
@@ -27,8 +45,13 @@ export interface DbVerifyCommandSuccessResult {
27
45
  readonly codecCoverageSkipped?: VerifyDatabaseResult['codecCoverageSkipped'];
28
46
  readonly schema?: {
29
47
  readonly summary: string;
30
- readonly counts: VerifyDatabaseSchemaResult['schema']['counts'];
31
48
  readonly strict: boolean;
49
+ /**
50
+ * Warn-graded finding messages (observed-policy drift). Informational —
51
+ * present on a passing verify; the full-mode result summarizes them as a
52
+ * flat message list.
53
+ */
54
+ readonly warnings?: readonly string[];
32
55
  };
33
56
  /**
34
57
  * Live element names no contract space declares. In full success this is
@@ -77,11 +100,14 @@ export function formatVerifyOutput(
77
100
  if (result.contract.profileHash) {
78
101
  lines.push(`${formatDimText(` profileHash: ${result.contract.profileHash}`)}`);
79
102
  }
80
- if (result.mode === 'full' && result.schema && isVerbose(flags, 1)) {
81
- lines.push(
82
- `${formatDimText(` schema: pass=${result.schema.counts.pass} warn=${result.schema.counts.warn} fail=${result.schema.counts.fail}`)}`,
83
- );
103
+ if (result.schema?.warnings && result.schema.warnings.length > 0) {
104
+ lines.push('');
105
+ lines.push(formatYellow('Schema warnings:'));
106
+ for (const message of result.schema.warnings) {
107
+ lines.push(` ${formatYellow('⚠')} ${message}`);
108
+ }
84
109
  }
110
+
85
111
  if (result.unclaimed && result.unclaimed.length > 0) {
86
112
  lines.push('');
87
113
  lines.push(formatYellow('Unclaimed elements (declared by no contract):'));
@@ -139,7 +165,7 @@ export function formatIntrospectJson(result: IntrospectSchemaResult<unknown>): s
139
165
 
140
166
  /**
141
167
  * Renders a schema tree structure from CoreSchemaView.
142
- * Matches the style of renderSchemaVerificationTree for consistency.
168
+ * Status-glyph tree styling shared with the retired verification-tree renderer.
143
169
  */
144
170
  function renderSchemaTree(
145
171
  node: SchemaTreeNode,
@@ -325,207 +351,6 @@ export function formatIntrospectOutput(
325
351
  return lines.join('\n');
326
352
  }
327
353
 
328
- /**
329
- * Renders a schema verification tree structure from SchemaVerificationNode.
330
- * Similar to renderSchemaTree but for verification nodes with status-based colors and glyphs.
331
- */
332
- function renderSchemaVerificationTree(
333
- node: SchemaVerificationNode,
334
- flags: GlobalFlags,
335
- options: {
336
- readonly isLast: boolean;
337
- readonly prefix: string;
338
- readonly useColor: boolean;
339
- readonly formatDimText: (text: string) => string;
340
- readonly isRoot?: boolean;
341
- },
342
- ): string[] {
343
- const { isLast, prefix, useColor, formatDimText, isRoot = false } = options;
344
- const lines: string[] = [];
345
-
346
- // Format status glyph and color based on status
347
- let statusGlyph = '';
348
- let statusColor: (text: string) => string = (text) => text;
349
- if (useColor) {
350
- switch (node.status) {
351
- case 'pass':
352
- statusGlyph = '✔';
353
- statusColor = green;
354
- break;
355
- case 'warn':
356
- statusGlyph = '⚠';
357
- statusColor = (text) => (useColor ? yellow(text) : text);
358
- break;
359
- case 'fail':
360
- statusGlyph = '✖';
361
- statusColor = red;
362
- break;
363
- }
364
- } else {
365
- switch (node.status) {
366
- case 'pass':
367
- statusGlyph = '✔';
368
- break;
369
- case 'warn':
370
- statusGlyph = '⚠';
371
- break;
372
- case 'fail':
373
- statusGlyph = '✖';
374
- break;
375
- }
376
- }
377
-
378
- // Format node label with color based on kind
379
- // For column nodes, we need to parse the name to color code different parts
380
- let labelColor: (text: string) => string = (text) => text;
381
- let formattedLabel: string = node.name;
382
-
383
- if (useColor) {
384
- switch (node.kind) {
385
- case 'contract':
386
- case 'schema':
387
- labelColor = bold;
388
- formattedLabel = labelColor(node.name);
389
- break;
390
- case 'table': {
391
- // Parse "table tableName" format - color "table" dim, tableName cyan
392
- const tableMatch = node.name.match(/^table\s+(.+)$/);
393
- if (tableMatch?.[1]) {
394
- const tableName = tableMatch[1];
395
- formattedLabel = `${dim('table')} ${cyan(tableName)}`;
396
- } else {
397
- formattedLabel = dim(node.name);
398
- }
399
- break;
400
- }
401
- case 'columns':
402
- labelColor = dim;
403
- formattedLabel = labelColor(node.name);
404
- break;
405
- case 'column': {
406
- // Parse column name format: "columnName: contractType -> nativeType (nullability)"
407
- // Color code: column name (cyan), contract type (default), native type (dim), nullability (dim)
408
- const columnMatch = node.name.match(/^([^:]+):\s*(.+)$/);
409
- if (columnMatch?.[1] && columnMatch[2]) {
410
- const columnName = columnMatch[1];
411
- const rest = columnMatch[2];
412
- // Parse rest: "contractType -> nativeType (nullability)"
413
- // Match contract type (can contain /, @, etc.), arrow, native type, then nullability in parentheses
414
- const typeMatch = rest.match(/^([^\s→]+)\s*→\s*([^\s(]+)\s*(\([^)]+\))$/);
415
- if (typeMatch?.[1] && typeMatch[2] && typeMatch[3]) {
416
- const contractType = typeMatch[1];
417
- const nativeType = typeMatch[2];
418
- const nullability = typeMatch[3];
419
- formattedLabel = `${cyan(columnName)}: ${contractType} → ${dim(nativeType)} ${dim(nullability)}`;
420
- } else {
421
- // Fallback if format doesn't match (e.g., no native type or no nullability)
422
- formattedLabel = `${cyan(columnName)}: ${rest}`;
423
- }
424
- } else {
425
- formattedLabel = node.name;
426
- }
427
- break;
428
- }
429
- case 'type':
430
- case 'nullability':
431
- labelColor = (text) => text; // Default color
432
- formattedLabel = labelColor(node.name);
433
- break;
434
- case 'primaryKey': {
435
- // Parse "primary key: columnName" format - color "primary key" dim, columnName cyan
436
- const pkMatch = node.name.match(/^primary key:\s*(.+)$/);
437
- if (pkMatch?.[1]) {
438
- const columnNames = pkMatch[1];
439
- formattedLabel = `${dim('primary key')}: ${cyan(columnNames)}`;
440
- } else {
441
- formattedLabel = dim(node.name);
442
- }
443
- break;
444
- }
445
- case 'foreignKey':
446
- case 'unique':
447
- case 'index':
448
- labelColor = dim;
449
- formattedLabel = labelColor(node.name);
450
- break;
451
- case 'dependency': {
452
- // Parse specific extension message formats
453
- // "database is postgres" -> dim "database is", cyan "postgres"
454
- const dbMatch = node.name.match(/^database is\s+(.+)$/);
455
- if (dbMatch?.[1]) {
456
- const dbName = dbMatch[1];
457
- formattedLabel = `${dim('database is')} ${cyan(dbName)}`;
458
- } else {
459
- // "vector extension is enabled" -> dim everything except extension name
460
- // Match pattern: "extensionName extension is enabled"
461
- const extMatch = node.name.match(/^([^\s]+)\s+(extension is enabled)$/);
462
- if (extMatch?.[1] && extMatch[2]) {
463
- const extName = extMatch[1];
464
- const rest = extMatch[2];
465
- formattedLabel = `${cyan(extName)} ${dim(rest)}`;
466
- } else {
467
- // Fallback: color entire name with magenta
468
- labelColor = magenta;
469
- formattedLabel = labelColor(node.name);
470
- }
471
- }
472
- break;
473
- }
474
- default:
475
- formattedLabel = node.name;
476
- break;
477
- }
478
- } else {
479
- formattedLabel = node.name;
480
- }
481
-
482
- const statusGlyphColored = statusColor(statusGlyph);
483
-
484
- // Build the label with optional message for failure/warn nodes
485
- let nodeLabel = formattedLabel;
486
- if (
487
- (node.status === 'fail' || node.status === 'warn') &&
488
- node.message &&
489
- node.message.length > 0
490
- ) {
491
- // Always show message for failure/warn nodes - it provides crucial context
492
- // For parent nodes, the message summarizes child failures
493
- // For leaf nodes, the message explains the specific issue
494
- const messageText = formatDimText(`(${node.message})`);
495
- nodeLabel = `${formattedLabel} ${messageText}`;
496
- }
497
-
498
- // Root node renders without tree characters or | prefix
499
- // Root node renders without tree characters or prefix
500
- if (isRoot) {
501
- lines.push(`${statusGlyphColored} ${nodeLabel}`);
502
- } else {
503
- const treeChar = isLast ? '└' : '├';
504
- const treePrefix = `${formatDimText(treeChar)}─ `;
505
- lines.push(`${prefix}${treePrefix}${statusGlyphColored} ${nodeLabel}`);
506
- }
507
-
508
- // Render children if present
509
- if (node.children && node.children.length > 0) {
510
- const childPrefix = isRoot ? '' : `${prefix}${isLast ? ' ' : `${formatDimText('│')} `}`;
511
- for (let i = 0; i < node.children.length; i++) {
512
- const child = node.children[i];
513
- if (!child) continue;
514
- const isLastChild = i === node.children.length - 1;
515
- const childLines = renderSchemaVerificationTree(child, flags, {
516
- isLast: isLastChild,
517
- prefix: childPrefix,
518
- useColor,
519
- formatDimText,
520
- isRoot: false,
521
- });
522
- lines.push(...childLines);
523
- }
524
- }
525
-
526
- return lines;
527
- }
528
-
529
354
  /**
530
355
  * Formats human-readable output for database schema verification.
531
356
  */
@@ -546,27 +371,26 @@ export function formatSchemaVerifyOutput(
546
371
  const formatYellow = createColorFormatter(useColor, yellow);
547
372
  const formatDimText = (text: string) => formatDim(useColor, text);
548
373
 
549
- // Render verification tree first
550
- const treeLines = renderSchemaVerificationTree(result.schema.root, flags, {
551
- isLast: true,
552
- prefix: '',
553
- useColor,
554
- formatDimText,
555
- isRoot: true,
556
- });
557
- lines.push(...treeLines);
558
-
559
- if (result.schema.schemaDiffIssues.length > 0) {
560
- lines.push('');
561
- lines.push(formatRed('Schema drift:'));
562
- for (const issue of result.schema.schemaDiffIssues) {
563
- lines.push(` ${formatRed('✖')} ${issue.message}`);
374
+ const issueMessages = result.schema.issues.map(formatIssueMessage);
375
+ if (issueMessages.length > 0) {
376
+ lines.push(formatRed('Schema issues:'));
377
+ for (const message of issueMessages) {
378
+ lines.push(` ${formatRed('✖')} ${message}`);
379
+ }
380
+ }
381
+
382
+ const warningMessages = (result.schema.warnings?.issues ?? []).map(formatIssueMessage);
383
+ if (warningMessages.length > 0) {
384
+ if (lines.length > 0) lines.push('');
385
+ lines.push(formatYellow('Schema warnings:'));
386
+ for (const message of warningMessages) {
387
+ lines.push(` ${formatYellow('⚠')} ${message}`);
564
388
  }
565
389
  }
566
390
 
567
391
  if (unclaimed.length > 0) {
568
392
  const strict = result.meta?.strict ?? false;
569
- lines.push('');
393
+ if (lines.length > 0) lines.push('');
570
394
  lines.push(
571
395
  (strict ? formatRed : formatYellow)('Unclaimed elements (declared by no contract):'),
572
396
  );
@@ -575,18 +399,14 @@ export function formatSchemaVerifyOutput(
575
399
  }
576
400
  }
577
401
 
578
- // Add counts and timings in verbose mode
579
402
  if (isVerbose(flags, 1)) {
580
403
  lines.push(`${formatDimText(` Total time: ${result.timings.total}ms`)}`);
581
- lines.push(
582
- `${formatDimText(` pass=${result.schema.counts.pass} warn=${result.schema.counts.warn} fail=${result.schema.counts.fail}`)}`,
583
- );
584
404
  }
585
405
 
586
406
  // Blank line before summary
587
- lines.push('');
407
+ if (lines.length > 0) lines.push('');
588
408
 
589
- // Summary line at the end: summary with status glyph
409
+ // Summary line at the end: verdict with status glyph
590
410
  if (result.ok) {
591
411
  lines.push(`${formatGreen('✔')} ${result.summary}`);
592
412
  } else {