@prisma-next/cli 0.14.0 → 0.15.0-dev.10

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 (157) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.mjs +33 -14
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-CJzuo5wX.mjs → client-DwOyZz0a.mjs} +141 -123
  5. package/dist/client-DwOyZz0a.mjs.map +1 -0
  6. package/dist/{command-helpers-DGMvGBeX.mjs → command-helpers-CNwURRf9.mjs} +30 -11
  7. package/dist/command-helpers-CNwURRf9.mjs.map +1 -0
  8. package/dist/commands/contract-emit.mjs +1 -1
  9. package/dist/commands/contract-infer.mjs +1 -1
  10. package/dist/commands/db-init.mjs +3 -3
  11. package/dist/commands/db-schema.mjs +3 -3
  12. package/dist/commands/db-sign.mjs +5 -5
  13. package/dist/commands/db-sign.mjs.map +1 -1
  14. package/dist/commands/db-update.mjs +4 -4
  15. package/dist/commands/db-verify.d.mts.map +1 -1
  16. package/dist/commands/db-verify.mjs +1 -1
  17. package/dist/commands/migrate.d.mts +1 -1
  18. package/dist/commands/migrate.d.mts.map +1 -1
  19. package/dist/commands/migrate.mjs +37 -38
  20. package/dist/commands/migrate.mjs.map +1 -1
  21. package/dist/commands/migration-check.d.mts +1 -1
  22. package/dist/commands/migration-check.mjs +1 -1
  23. package/dist/commands/migration-graph.d.mts +1 -1
  24. package/dist/commands/migration-graph.mjs +9 -9
  25. package/dist/commands/migration-graph.mjs.map +1 -1
  26. package/dist/commands/migration-list.d.mts +6 -6
  27. package/dist/commands/migration-list.d.mts.map +1 -1
  28. package/dist/commands/migration-list.mjs +1 -1
  29. package/dist/commands/migration-log.d.mts +1 -1
  30. package/dist/commands/migration-log.mjs +1 -1
  31. package/dist/commands/migration-new.mjs +4 -4
  32. package/dist/commands/migration-new.mjs.map +1 -1
  33. package/dist/commands/migration-plan.d.mts.map +1 -1
  34. package/dist/commands/migration-plan.mjs +1 -1
  35. package/dist/commands/migration-show.mjs +4 -4
  36. package/dist/commands/migration-show.mjs.map +1 -1
  37. package/dist/commands/migration-status.d.mts +1 -1
  38. package/dist/commands/migration-status.mjs +1 -1
  39. package/dist/commands/ref.d.mts +1 -1
  40. package/dist/commands/ref.mjs +3 -3
  41. package/dist/commands/ref.mjs.map +1 -1
  42. package/dist/commands/telemetry/index.mjs +1 -1
  43. package/dist/{contract-at-errors-CFXsstzm.mjs → contract-at-errors-BHbY04pl.mjs} +2 -2
  44. package/dist/{contract-at-errors-CFXsstzm.mjs.map → contract-at-errors-BHbY04pl.mjs.map} +1 -1
  45. package/dist/{contract-emit-C8HmtboH.mjs → contract-emit-BeidkyUC.mjs} +9 -11
  46. package/dist/contract-emit-BeidkyUC.mjs.map +1 -0
  47. package/dist/{contract-emit-B_qriF8B.mjs → contract-emit-IQBYv351.mjs} +6 -6
  48. package/dist/contract-emit-IQBYv351.mjs.map +1 -0
  49. package/dist/{contract-infer-BYT_ra_U.mjs → contract-infer-B3Wq2cu1.mjs} +3 -3
  50. package/dist/{contract-infer-BYT_ra_U.mjs.map → contract-infer-B3Wq2cu1.mjs.map} +1 -1
  51. package/dist/{contract-space-aggregate-loader-ClI1KN6d.mjs → contract-space-aggregate-loader-BirzMmW1.mjs} +63 -5
  52. package/dist/contract-space-aggregate-loader-BirzMmW1.mjs.map +1 -0
  53. package/dist/{db-verify-C24FKhb7.mjs → db-verify-DpRrecs7.mjs} +57 -61
  54. package/dist/db-verify-DpRrecs7.mjs.map +1 -0
  55. package/dist/exports/control-api.d.mts +3 -3
  56. package/dist/exports/control-api.d.mts.map +1 -1
  57. package/dist/exports/control-api.mjs +2 -2
  58. package/dist/exports/index.d.mts +5 -1
  59. package/dist/exports/index.d.mts.map +1 -1
  60. package/dist/exports/index.mjs +4 -3
  61. package/dist/exports/index.mjs.map +1 -1
  62. package/dist/format-CaCF63ld.mjs +102 -0
  63. package/dist/format-CaCF63ld.mjs.map +1 -0
  64. package/dist/{framework-components-YVQHhPH7.mjs → framework-components-CnBcJDmF.mjs} +2 -2
  65. package/dist/{framework-components-YVQHhPH7.mjs.map → framework-components-CnBcJDmF.mjs.map} +1 -1
  66. package/dist/{init-0HwB-Vh8.mjs → init-Boa0LzeJ.mjs} +27 -19
  67. package/dist/init-Boa0LzeJ.mjs.map +1 -0
  68. package/dist/{inspect-live-schema-DF6IwcDl.mjs → inspect-live-schema-CldSQjVI.mjs} +4 -4
  69. package/dist/inspect-live-schema-CldSQjVI.mjs.map +1 -0
  70. package/dist/{migration-check-VwM8xCZV.mjs → migration-check-CGtYnR9k.mjs} +10 -11
  71. package/dist/migration-check-CGtYnR9k.mjs.map +1 -0
  72. package/dist/migration-cli.mjs +1 -1
  73. package/dist/migration-cli.mjs.map +1 -1
  74. package/dist/{migration-command-scaffold-DA-Lhx6o.mjs → migration-command-scaffold-B3m-MMAx.mjs} +4 -4
  75. package/dist/migration-command-scaffold-B3m-MMAx.mjs.map +1 -0
  76. package/dist/{migration-graph-command-render-CEez7YUK.mjs → migration-graph-command-render-B83xrnNy.mjs} +24 -24
  77. package/dist/{migration-graph-command-render-CEez7YUK.mjs.map → migration-graph-command-render-B83xrnNy.mjs.map} +1 -1
  78. package/dist/{migration-list-DlJJ_38Z.mjs → migration-list-DBU6Pneg.mjs} +16 -16
  79. package/dist/migration-list-DBU6Pneg.mjs.map +1 -0
  80. package/dist/{migration-log-CG0qQAFm.mjs → migration-log-BYngyvXa.mjs} +5 -5
  81. package/dist/migration-log-BYngyvXa.mjs.map +1 -0
  82. package/dist/{migration-path-target-Ce6OZImp.mjs → migration-path-target-DQa4Caoc.mjs} +2 -2
  83. package/dist/{migration-path-target-Ce6OZImp.mjs.map → migration-path-target-DQa4Caoc.mjs.map} +1 -1
  84. package/dist/{migration-plan-W_E8FQOk.mjs → migration-plan-D6bHpCzE.mjs} +30 -30
  85. package/dist/migration-plan-D6bHpCzE.mjs.map +1 -0
  86. package/dist/{migration-status-CD-LC2Ip.mjs → migration-status-CpQ5UhcU.mjs} +14 -14
  87. package/dist/migration-status-CpQ5UhcU.mjs.map +1 -0
  88. package/dist/{telemetry-BIM4beEO.mjs → telemetry-BQJ_XHa9.mjs} +2 -2
  89. package/dist/{telemetry-BIM4beEO.mjs.map → telemetry-BQJ_XHa9.mjs.map} +1 -1
  90. package/dist/{types-C_tYiJYx.d.mts → types-DFoqNIcQ.d.mts} +27 -21
  91. package/dist/types-DFoqNIcQ.d.mts.map +1 -0
  92. package/dist/{verify-DcOYZ1tH.mjs → verify-CInGyt4U.mjs} +58 -159
  93. package/dist/verify-CInGyt4U.mjs.map +1 -0
  94. package/package.json +28 -30
  95. package/src/cli.ts +7 -0
  96. package/src/commands/contract-emit.ts +1 -1
  97. package/src/commands/db-sign.ts +1 -1
  98. package/src/commands/db-verify.ts +34 -20
  99. package/src/commands/format.ts +75 -0
  100. package/src/commands/init/detect-package-manager.ts +8 -4
  101. package/src/commands/init/hygiene-package-scripts.ts +16 -3
  102. package/src/commands/inspect-live-schema.ts +1 -1
  103. package/src/commands/lsp.ts +32 -0
  104. package/src/commands/migrate.ts +47 -44
  105. package/src/commands/migration-check.ts +6 -6
  106. package/src/commands/migration-graph.ts +5 -5
  107. package/src/commands/migration-list.ts +13 -13
  108. package/src/commands/migration-log.ts +1 -1
  109. package/src/commands/migration-new.ts +1 -1
  110. package/src/commands/migration-plan.ts +15 -4
  111. package/src/commands/migration-show.ts +1 -1
  112. package/src/commands/migration-status.ts +10 -10
  113. package/src/commands/ref.ts +1 -1
  114. package/src/control-api/client.ts +8 -23
  115. package/src/control-api/operations/contract-emit.ts +9 -27
  116. package/src/control-api/operations/db-init.ts +2 -2
  117. package/src/control-api/operations/db-run.ts +18 -17
  118. package/src/control-api/operations/db-verify.ts +52 -44
  119. package/src/control-api/operations/format.ts +98 -0
  120. package/src/control-api/operations/migrate.ts +139 -95
  121. package/src/control-api/operations/run-migration.ts +6 -5
  122. package/src/control-api/types.ts +8 -8
  123. package/src/exports/index.ts +1 -0
  124. package/src/migration-cli.ts +1 -1
  125. package/src/utils/cli-errors.ts +14 -1
  126. package/src/utils/combine-verify-results.ts +94 -0
  127. package/src/utils/extension-pack-inputs.ts +1 -1
  128. package/src/utils/formatters/errors.ts +34 -5
  129. package/src/utils/formatters/verify.ts +89 -227
  130. package/src/utils/migration-command-scaffold.ts +1 -1
  131. package/src/utils/plan-resolution.ts +18 -18
  132. package/dist/client-CJzuo5wX.mjs.map +0 -1
  133. package/dist/command-helpers-DGMvGBeX.mjs.map +0 -1
  134. package/dist/config-loader-p9JMrekQ.mjs +0 -88
  135. package/dist/config-loader-p9JMrekQ.mjs.map +0 -1
  136. package/dist/config-loader.d.mts +0 -16
  137. package/dist/config-loader.d.mts.map +0 -1
  138. package/dist/config-loader.mjs +0 -2
  139. package/dist/contract-emit-B_qriF8B.mjs.map +0 -1
  140. package/dist/contract-emit-C8HmtboH.mjs.map +0 -1
  141. package/dist/contract-space-aggregate-loader-ClI1KN6d.mjs.map +0 -1
  142. package/dist/db-verify-C24FKhb7.mjs.map +0 -1
  143. package/dist/extension-pack-inputs-1ySHqxKG.mjs +0 -62
  144. package/dist/extension-pack-inputs-1ySHqxKG.mjs.map +0 -1
  145. package/dist/init-0HwB-Vh8.mjs.map +0 -1
  146. package/dist/inspect-live-schema-DF6IwcDl.mjs.map +0 -1
  147. package/dist/migration-check-VwM8xCZV.mjs.map +0 -1
  148. package/dist/migration-command-scaffold-DA-Lhx6o.mjs.map +0 -1
  149. package/dist/migration-list-DlJJ_38Z.mjs.map +0 -1
  150. package/dist/migration-log-CG0qQAFm.mjs.map +0 -1
  151. package/dist/migration-plan-W_E8FQOk.mjs.map +0 -1
  152. package/dist/migration-status-CD-LC2Ip.mjs.map +0 -1
  153. package/dist/types-C_tYiJYx.d.mts.map +0 -1
  154. package/dist/verify-DcOYZ1tH.mjs.map +0 -1
  155. package/src/config-loader.ts +0 -92
  156. package/src/config-path-validation.ts +0 -74
  157. package/src/utils/combine-schema-results.ts +0 -84
@@ -0,0 +1,94 @@
1
+ import type { VerifyDatabaseSchemaResult } from '@prisma-next/framework-components/control';
2
+
3
+ /**
4
+ * The combined per-space contract-satisfaction result plus the standalone
5
+ * unclaimed-elements list, reported once. The CLI renders
6
+ * both; `unclaimed` is never folded into the combined tree or its issues.
7
+ */
8
+ export interface CombinedVerifyResult {
9
+ readonly result: VerifyDatabaseSchemaResult;
10
+ readonly unclaimed: readonly string[];
11
+ }
12
+
13
+ /**
14
+ * Collapse the aggregate verifier's per-space contract-satisfaction results
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
+ * unclaimed element per space.
21
+ *
22
+ * **Unclaimed disposition.** In strict mode a non-empty `unclaimed` list fails
23
+ * the combined verdict (`ok: false`); in lenient mode it is carried for
24
+ * informational rendering only. The list itself is returned unchanged for the
25
+ * renderer.
26
+ *
27
+ * **Summary policy.** Preserve the per-family phrasing whenever the
28
+ * combined `ok` flag agrees with the app space's `ok` flag — this is
29
+ * the common case (single-family deployments, single-app deployments)
30
+ * and the family's "satisfies / does not satisfy contract" phrasing
31
+ * stays user-visible. When the app passes but an extension fails (or
32
+ * vice versa) the app's summary contradicts the envelope, so fall back
33
+ * to the first failing space's summary. This keeps family phrasing
34
+ * intact and the envelope internally consistent (`ok: false` ↔ failure
35
+ * summary).
36
+ */
37
+ export function combineVerifyResults(
38
+ perSpace: ReadonlyMap<string, VerifyDatabaseSchemaResult>,
39
+ appSpaceId: string,
40
+ strict: boolean,
41
+ unclaimed: readonly string[],
42
+ ): CombinedVerifyResult {
43
+ const appResult = perSpace.get(appSpaceId) ?? perSpace.values().next().value;
44
+ if (appResult === undefined) {
45
+ throw new Error(
46
+ 'Aggregate verifier returned no per-space verify results — this is a wiring bug.',
47
+ );
48
+ }
49
+
50
+ let okAll = true;
51
+ let firstFailure: VerifyDatabaseSchemaResult | undefined;
52
+ let issues: VerifyDatabaseSchemaResult['schema']['issues'] = [];
53
+ let warningIssues: VerifyDatabaseSchemaResult['schema']['issues'] = [];
54
+ for (const [, result] of perSpace) {
55
+ if (!result.ok) {
56
+ okAll = false;
57
+ if (firstFailure === undefined) firstFailure = result;
58
+ }
59
+ issues = [...issues, ...result.schema.issues];
60
+ warningIssues = [...warningIssues, ...(result.schema.warnings?.issues ?? [])];
61
+ }
62
+
63
+ const unclaimedFails = strict && unclaimed.length > 0;
64
+ const ok = okAll && !unclaimedFails;
65
+
66
+ // Prefer a failing space's family phrasing; else, when only the unclaimed list
67
+ // fails the verdict, say so; else keep the app space's phrasing. When `okAll`
68
+ // is false the loop assigned `firstFailure`, so the `?? appResult.summary`
69
+ // fallback is unreachable — it exists only to keep the read cast-free.
70
+ const summary = okAll
71
+ ? unclaimedFails
72
+ ? `Database schema has ${unclaimed.length} unclaimed element${unclaimed.length === 1 ? '' : 's'} (not in any contract)`
73
+ : appResult.summary
74
+ : appResult.ok
75
+ ? (firstFailure?.summary ?? appResult.summary)
76
+ : appResult.summary;
77
+
78
+ return {
79
+ result: {
80
+ ok,
81
+ ...(ok ? {} : { code: appResult.code ?? 'PN-RUN-3010' }),
82
+ summary,
83
+ contract: appResult.contract,
84
+ target: appResult.target,
85
+ schema: {
86
+ issues,
87
+ warnings: { issues: warningIssues },
88
+ },
89
+ meta: { strict },
90
+ timings: { total: 0 },
91
+ },
92
+ unclaimed,
93
+ };
94
+ }
@@ -111,7 +111,7 @@ export function toExtensionInputs(
111
111
  * {@link import('./contract-space-aggregate-loader').buildContractSpaceAggregate}.
112
112
  *
113
113
  * Codec-only extensions (no `contractSpace` declaration) are filtered
114
- * out: they are not contract-space members, so the aggregate loader
114
+ * out: they contribute no contract space, so the aggregate loader
115
115
  * has nothing to do with them. Filtering happens at this descriptor-
116
116
  * import boundary so the loader stays oblivious to that distinction —
117
117
  * every entry it sees expects an on-disk `migrations/<id>/` directory.
@@ -7,6 +7,25 @@ import type { GlobalFlags } from '../global-flags';
7
7
  import { createColorFormatter, formatDim, isVerbose } from './helpers';
8
8
  import { formatPlannerWarningsBlock } from './migrations';
9
9
 
10
+ /**
11
+ * The display label for a schema-diff issue in the shared error envelope,
12
+ * derived from which sides are present: expected-only is a missing object,
13
+ * actual-only an extra one, both a mismatch. `undefined` when the entry is not
14
+ * a schema-diff issue (it carries neither side) so the caller can fall through
15
+ * to its generic label.
16
+ */
17
+ function schemaDiffIssueLabel(issue: {
18
+ readonly expected?: unknown;
19
+ readonly actual?: unknown;
20
+ }): 'missing' | 'extra' | 'mismatch' | undefined {
21
+ const hasExpected = issue.expected !== undefined;
22
+ const hasActual = issue.actual !== undefined;
23
+ if (hasExpected && hasActual) return 'mismatch';
24
+ if (hasExpected) return 'missing';
25
+ if (hasActual) return 'extra';
26
+ return undefined;
27
+ }
28
+
10
29
  /**
11
30
  * Formats error output for human-readable display.
12
31
  */
@@ -48,9 +67,19 @@ export function formatErrorOutput(error: CliErrorEnvelope, flags: GlobalFlags):
48
67
  }
49
68
  }
50
69
  }
51
- // Show issues list if present (always show a short list; show full list when verbose)
70
+ // Show issues list if present (always show a short list; show full list when verbose).
71
+ // `issues` is a shared error-envelope field: PSL interpretation diagnostics stamp
72
+ // `kind` and `message` (their diagnostic code and prose); schema-diff issues
73
+ // (`SchemaDiffIssue`) carry no `message` and stamp `path` plus the
74
+ // `expected`/`actual` presence a label is derived from.
52
75
  if (error.meta?.['issues']) {
53
- const issues = error.meta['issues'] as readonly { kind?: string; message?: string }[];
76
+ const issues = error.meta['issues'] as readonly {
77
+ kind?: string;
78
+ message?: string;
79
+ path?: readonly string[];
80
+ expected?: unknown;
81
+ actual?: unknown;
82
+ }[];
54
83
  if (issues.length > 0) {
55
84
  const maxToShow = isVerbose(flags, 1) ? issues.length : Math.min(3, issues.length);
56
85
  const header = isVerbose(flags, 1)
@@ -58,9 +87,9 @@ export function formatErrorOutput(error: CliErrorEnvelope, flags: GlobalFlags):
58
87
  : ` Issues (showing ${maxToShow} of ${issues.length}):`;
59
88
  lines.push(`${formatDimText(header)}`);
60
89
  for (const issue of issues.slice(0, maxToShow)) {
61
- const kind = issue.kind ?? 'issue';
62
- const message = issue.message ?? '';
63
- lines.push(`${formatDimText(` - [${kind}] ${message}`)}`);
90
+ const label = issue.kind ?? schemaDiffIssueLabel(issue) ?? 'issue';
91
+ const message = issue.message ?? issue.path?.join('/') ?? '';
92
+ lines.push(`${formatDimText(` - [${label}] ${message}`)}`);
64
93
  }
65
94
  if (!isVerbose(flags, 1) && issues.length > maxToShow) {
66
95
  lines.push(`${formatDimText(' Re-run with -v/--verbose to see all issues')}`);
@@ -1,17 +1,36 @@
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,
9
10
  } from '@prisma-next/framework-components/control';
11
+ import { issueOutcome } from '@prisma-next/framework-components/control';
10
12
  import { ifDefined } from '@prisma-next/utils/defined';
11
13
  import { bold, cyan, dim, green, magenta, red, yellow } from 'colorette';
12
14
  import type { GlobalFlags } from '../global-flags';
13
15
  import { createColorFormatter, formatDim, isVerbose } from './helpers';
14
16
 
17
+ /** Human-readable label for each outcome, prefixed onto an issue's message for display. */
18
+ const OUTCOME_LABEL: Record<ExpectationFailureReason, string> = {
19
+ 'not-found': 'missing',
20
+ 'not-expected': 'extra',
21
+ 'not-equal': 'mismatch',
22
+ };
23
+
24
+ /**
25
+ * The issue's display text: its own path, prefixed with a human label for
26
+ * why it's flagged. Turning the presence-derived outcome (and the path) into
27
+ * prose is this formatter's job, not the differ's — the differ's issue is
28
+ * data (`path` + nodes), not prose.
29
+ */
30
+ function formatIssueMessage(issue: SchemaDiffIssue): string {
31
+ return `${OUTCOME_LABEL[issueOutcome(issue)]}: ${issue.path.join('/')}`;
32
+ }
33
+
15
34
  // ============================================================================
16
35
  // Verify Output Formatters
17
36
  // ============================================================================
@@ -27,9 +46,20 @@ export interface DbVerifyCommandSuccessResult {
27
46
  readonly codecCoverageSkipped?: VerifyDatabaseResult['codecCoverageSkipped'];
28
47
  readonly schema?: {
29
48
  readonly summary: string;
30
- readonly counts: VerifyDatabaseSchemaResult['schema']['counts'];
31
49
  readonly strict: boolean;
50
+ /**
51
+ * Warn-graded finding messages (observed-policy drift). Informational —
52
+ * present on a passing verify; the full-mode result summarizes them as a
53
+ * flat message list.
54
+ */
55
+ readonly warnings?: readonly string[];
32
56
  };
57
+ /**
58
+ * Live element names no contract space declares. In full success this is
59
+ * only ever non-empty in lenient mode — strict mode fails on it — and is
60
+ * rendered informationally.
61
+ */
62
+ readonly unclaimed?: readonly string[];
33
63
  readonly warning?: string;
34
64
  readonly meta?:
35
65
  | (NonNullable<VerifyDatabaseResult['meta']> & {
@@ -71,11 +101,22 @@ export function formatVerifyOutput(
71
101
  if (result.contract.profileHash) {
72
102
  lines.push(`${formatDimText(` profileHash: ${result.contract.profileHash}`)}`);
73
103
  }
74
- if (result.mode === 'full' && result.schema && isVerbose(flags, 1)) {
75
- lines.push(
76
- `${formatDimText(` schema: pass=${result.schema.counts.pass} warn=${result.schema.counts.warn} fail=${result.schema.counts.fail}`)}`,
77
- );
104
+ if (result.schema?.warnings && result.schema.warnings.length > 0) {
105
+ lines.push('');
106
+ lines.push(formatYellow('Schema warnings:'));
107
+ for (const message of result.schema.warnings) {
108
+ lines.push(` ${formatYellow('⚠')} ${message}`);
109
+ }
78
110
  }
111
+
112
+ if (result.unclaimed && result.unclaimed.length > 0) {
113
+ lines.push('');
114
+ lines.push(formatYellow('Unclaimed elements (declared by no contract):'));
115
+ for (const name of result.unclaimed) {
116
+ lines.push(` ${formatYellow('⚠')} ${name}`);
117
+ }
118
+ }
119
+
79
120
  if (result.warning) {
80
121
  lines.push('');
81
122
  lines.push(`${formatYellow('⚠')} ${result.warning}`);
@@ -107,6 +148,7 @@ export function formatVerifyJson(result: DbVerifyCommandSuccessResult): string {
107
148
  ...ifDefined('missingCodecs', result.missingCodecs),
108
149
  ...ifDefined('codecCoverageSkipped', result.codecCoverageSkipped),
109
150
  ...ifDefined('schema', result.schema),
151
+ unclaimed: result.unclaimed ?? [],
110
152
  ...ifDefined('warning', result.warning),
111
153
  ...ifDefined('meta', result.meta),
112
154
  timings: result.timings,
@@ -124,7 +166,7 @@ export function formatIntrospectJson(result: IntrospectSchemaResult<unknown>): s
124
166
 
125
167
  /**
126
168
  * Renders a schema tree structure from CoreSchemaView.
127
- * Matches the style of renderSchemaVerificationTree for consistency.
169
+ * Status-glyph tree styling shared with the retired verification-tree renderer.
128
170
  */
129
171
  function renderSchemaTree(
130
172
  node: SchemaTreeNode,
@@ -310,213 +352,13 @@ export function formatIntrospectOutput(
310
352
  return lines.join('\n');
311
353
  }
312
354
 
313
- /**
314
- * Renders a schema verification tree structure from SchemaVerificationNode.
315
- * Similar to renderSchemaTree but for verification nodes with status-based colors and glyphs.
316
- */
317
- function renderSchemaVerificationTree(
318
- node: SchemaVerificationNode,
319
- flags: GlobalFlags,
320
- options: {
321
- readonly isLast: boolean;
322
- readonly prefix: string;
323
- readonly useColor: boolean;
324
- readonly formatDimText: (text: string) => string;
325
- readonly isRoot?: boolean;
326
- },
327
- ): string[] {
328
- const { isLast, prefix, useColor, formatDimText, isRoot = false } = options;
329
- const lines: string[] = [];
330
-
331
- // Format status glyph and color based on status
332
- let statusGlyph = '';
333
- let statusColor: (text: string) => string = (text) => text;
334
- if (useColor) {
335
- switch (node.status) {
336
- case 'pass':
337
- statusGlyph = '✔';
338
- statusColor = green;
339
- break;
340
- case 'warn':
341
- statusGlyph = '⚠';
342
- statusColor = (text) => (useColor ? yellow(text) : text);
343
- break;
344
- case 'fail':
345
- statusGlyph = '✖';
346
- statusColor = red;
347
- break;
348
- }
349
- } else {
350
- switch (node.status) {
351
- case 'pass':
352
- statusGlyph = '✔';
353
- break;
354
- case 'warn':
355
- statusGlyph = '⚠';
356
- break;
357
- case 'fail':
358
- statusGlyph = '✖';
359
- break;
360
- }
361
- }
362
-
363
- // Format node label with color based on kind
364
- // For column nodes, we need to parse the name to color code different parts
365
- let labelColor: (text: string) => string = (text) => text;
366
- let formattedLabel: string = node.name;
367
-
368
- if (useColor) {
369
- switch (node.kind) {
370
- case 'contract':
371
- case 'schema':
372
- labelColor = bold;
373
- formattedLabel = labelColor(node.name);
374
- break;
375
- case 'table': {
376
- // Parse "table tableName" format - color "table" dim, tableName cyan
377
- const tableMatch = node.name.match(/^table\s+(.+)$/);
378
- if (tableMatch?.[1]) {
379
- const tableName = tableMatch[1];
380
- formattedLabel = `${dim('table')} ${cyan(tableName)}`;
381
- } else {
382
- formattedLabel = dim(node.name);
383
- }
384
- break;
385
- }
386
- case 'columns':
387
- labelColor = dim;
388
- formattedLabel = labelColor(node.name);
389
- break;
390
- case 'column': {
391
- // Parse column name format: "columnName: contractType -> nativeType (nullability)"
392
- // Color code: column name (cyan), contract type (default), native type (dim), nullability (dim)
393
- const columnMatch = node.name.match(/^([^:]+):\s*(.+)$/);
394
- if (columnMatch?.[1] && columnMatch[2]) {
395
- const columnName = columnMatch[1];
396
- const rest = columnMatch[2];
397
- // Parse rest: "contractType -> nativeType (nullability)"
398
- // Match contract type (can contain /, @, etc.), arrow, native type, then nullability in parentheses
399
- const typeMatch = rest.match(/^([^\s→]+)\s*→\s*([^\s(]+)\s*(\([^)]+\))$/);
400
- if (typeMatch?.[1] && typeMatch[2] && typeMatch[3]) {
401
- const contractType = typeMatch[1];
402
- const nativeType = typeMatch[2];
403
- const nullability = typeMatch[3];
404
- formattedLabel = `${cyan(columnName)}: ${contractType} → ${dim(nativeType)} ${dim(nullability)}`;
405
- } else {
406
- // Fallback if format doesn't match (e.g., no native type or no nullability)
407
- formattedLabel = `${cyan(columnName)}: ${rest}`;
408
- }
409
- } else {
410
- formattedLabel = node.name;
411
- }
412
- break;
413
- }
414
- case 'type':
415
- case 'nullability':
416
- labelColor = (text) => text; // Default color
417
- formattedLabel = labelColor(node.name);
418
- break;
419
- case 'primaryKey': {
420
- // Parse "primary key: columnName" format - color "primary key" dim, columnName cyan
421
- const pkMatch = node.name.match(/^primary key:\s*(.+)$/);
422
- if (pkMatch?.[1]) {
423
- const columnNames = pkMatch[1];
424
- formattedLabel = `${dim('primary key')}: ${cyan(columnNames)}`;
425
- } else {
426
- formattedLabel = dim(node.name);
427
- }
428
- break;
429
- }
430
- case 'foreignKey':
431
- case 'unique':
432
- case 'index':
433
- labelColor = dim;
434
- formattedLabel = labelColor(node.name);
435
- break;
436
- case 'dependency': {
437
- // Parse specific extension message formats
438
- // "database is postgres" -> dim "database is", cyan "postgres"
439
- const dbMatch = node.name.match(/^database is\s+(.+)$/);
440
- if (dbMatch?.[1]) {
441
- const dbName = dbMatch[1];
442
- formattedLabel = `${dim('database is')} ${cyan(dbName)}`;
443
- } else {
444
- // "vector extension is enabled" -> dim everything except extension name
445
- // Match pattern: "extensionName extension is enabled"
446
- const extMatch = node.name.match(/^([^\s]+)\s+(extension is enabled)$/);
447
- if (extMatch?.[1] && extMatch[2]) {
448
- const extName = extMatch[1];
449
- const rest = extMatch[2];
450
- formattedLabel = `${cyan(extName)} ${dim(rest)}`;
451
- } else {
452
- // Fallback: color entire name with magenta
453
- labelColor = magenta;
454
- formattedLabel = labelColor(node.name);
455
- }
456
- }
457
- break;
458
- }
459
- default:
460
- formattedLabel = node.name;
461
- break;
462
- }
463
- } else {
464
- formattedLabel = node.name;
465
- }
466
-
467
- const statusGlyphColored = statusColor(statusGlyph);
468
-
469
- // Build the label with optional message for failure/warn nodes
470
- let nodeLabel = formattedLabel;
471
- if (
472
- (node.status === 'fail' || node.status === 'warn') &&
473
- node.message &&
474
- node.message.length > 0
475
- ) {
476
- // Always show message for failure/warn nodes - it provides crucial context
477
- // For parent nodes, the message summarizes child failures
478
- // For leaf nodes, the message explains the specific issue
479
- const messageText = formatDimText(`(${node.message})`);
480
- nodeLabel = `${formattedLabel} ${messageText}`;
481
- }
482
-
483
- // Root node renders without tree characters or | prefix
484
- // Root node renders without tree characters or prefix
485
- if (isRoot) {
486
- lines.push(`${statusGlyphColored} ${nodeLabel}`);
487
- } else {
488
- const treeChar = isLast ? '└' : '├';
489
- const treePrefix = `${formatDimText(treeChar)}─ `;
490
- lines.push(`${prefix}${treePrefix}${statusGlyphColored} ${nodeLabel}`);
491
- }
492
-
493
- // Render children if present
494
- if (node.children && node.children.length > 0) {
495
- const childPrefix = isRoot ? '' : `${prefix}${isLast ? ' ' : `${formatDimText('│')} `}`;
496
- for (let i = 0; i < node.children.length; i++) {
497
- const child = node.children[i];
498
- if (!child) continue;
499
- const isLastChild = i === node.children.length - 1;
500
- const childLines = renderSchemaVerificationTree(child, flags, {
501
- isLast: isLastChild,
502
- prefix: childPrefix,
503
- useColor,
504
- formatDimText,
505
- isRoot: false,
506
- });
507
- lines.push(...childLines);
508
- }
509
- }
510
-
511
- return lines;
512
- }
513
-
514
355
  /**
515
356
  * Formats human-readable output for database schema verification.
516
357
  */
517
358
  export function formatSchemaVerifyOutput(
518
359
  result: VerifyDatabaseSchemaResult,
519
360
  flags: GlobalFlags,
361
+ unclaimed: readonly string[] = [],
520
362
  ): string {
521
363
  if (flags.quiet) {
522
364
  return '';
@@ -527,30 +369,45 @@ export function formatSchemaVerifyOutput(
527
369
  const useColor = flags.color !== false;
528
370
  const formatGreen = createColorFormatter(useColor, green);
529
371
  const formatRed = createColorFormatter(useColor, red);
372
+ const formatYellow = createColorFormatter(useColor, yellow);
530
373
  const formatDimText = (text: string) => formatDim(useColor, text);
531
374
 
532
- // Render verification tree first
533
- const treeLines = renderSchemaVerificationTree(result.schema.root, flags, {
534
- isLast: true,
535
- prefix: '',
536
- useColor,
537
- formatDimText,
538
- isRoot: true,
539
- });
540
- lines.push(...treeLines);
541
-
542
- // Add counts and timings in verbose mode
543
- if (isVerbose(flags, 1)) {
544
- lines.push(`${formatDimText(` Total time: ${result.timings.total}ms`)}`);
375
+ const issueMessages = result.schema.issues.map(formatIssueMessage);
376
+ if (issueMessages.length > 0) {
377
+ lines.push(formatRed('Schema issues:'));
378
+ for (const message of issueMessages) {
379
+ lines.push(` ${formatRed('✖')} ${message}`);
380
+ }
381
+ }
382
+
383
+ const warningMessages = (result.schema.warnings?.issues ?? []).map(formatIssueMessage);
384
+ if (warningMessages.length > 0) {
385
+ if (lines.length > 0) lines.push('');
386
+ lines.push(formatYellow('Schema warnings:'));
387
+ for (const message of warningMessages) {
388
+ lines.push(` ${formatYellow('⚠')} ${message}`);
389
+ }
390
+ }
391
+
392
+ if (unclaimed.length > 0) {
393
+ const strict = result.meta?.strict ?? false;
394
+ if (lines.length > 0) lines.push('');
545
395
  lines.push(
546
- `${formatDimText(` pass=${result.schema.counts.pass} warn=${result.schema.counts.warn} fail=${result.schema.counts.fail}`)}`,
396
+ (strict ? formatRed : formatYellow)('Unclaimed elements (declared by no contract):'),
547
397
  );
398
+ for (const name of unclaimed) {
399
+ lines.push(` ${(strict ? formatRed : formatYellow)(strict ? '✖' : '⚠')} ${name}`);
400
+ }
401
+ }
402
+
403
+ if (isVerbose(flags, 1)) {
404
+ lines.push(`${formatDimText(` Total time: ${result.timings.total}ms`)}`);
548
405
  }
549
406
 
550
407
  // Blank line before summary
551
- lines.push('');
408
+ if (lines.length > 0) lines.push('');
552
409
 
553
- // Summary line at the end: summary with status glyph
410
+ // Summary line at the end: verdict with status glyph
554
411
  if (result.ok) {
555
412
  lines.push(`${formatGreen('✔')} ${result.summary}`);
556
413
  } else {
@@ -562,10 +419,15 @@ export function formatSchemaVerifyOutput(
562
419
  }
563
420
 
564
421
  /**
565
- * Formats JSON output for database schema verification.
422
+ * Formats JSON output for database schema verification. The unclaimed-elements
423
+ * list is a top-level field alongside the combined result, reported once for
424
+ * the whole database.
566
425
  */
567
- export function formatSchemaVerifyJson(result: VerifyDatabaseSchemaResult): string {
568
- return JSON.stringify(result, null, 2);
426
+ export function formatSchemaVerifyJson(
427
+ result: VerifyDatabaseSchemaResult,
428
+ unclaimed: readonly string[] = [],
429
+ ): string {
430
+ return JSON.stringify({ ...result, unclaimed }, null, 2);
569
431
  }
570
432
 
571
433
  // ============================================================================
@@ -1,9 +1,9 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { relative, resolve } from 'node:path';
3
+ import { loadConfig } from '@prisma-next/config-loader';
3
4
  import { hasMigrations } from '@prisma-next/framework-components/control';
4
5
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
5
6
  import type { Command } from 'commander';
6
- import { loadConfig } from '../config-loader';
7
7
  import { createControlClient } from '../control-api/client';
8
8
  import type { ControlClient } from '../control-api/types';
9
9
  import {