@prisma-next/cli 0.14.0-dev.5 → 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 (112) hide show
  1. package/dist/cli.mjs +14 -14
  2. package/dist/cli.mjs.map +1 -1
  3. package/dist/{client-EnAjSmZg.mjs → client-DL6KE_c0.mjs} +88 -99
  4. package/dist/client-DL6KE_c0.mjs.map +1 -0
  5. package/dist/{command-helpers-6cNJZ863.mjs → command-helpers-DMG0RsR1.mjs} +3 -3
  6. package/dist/command-helpers-DMG0RsR1.mjs.map +1 -0
  7. package/dist/commands/contract-emit.mjs +1 -1
  8. package/dist/commands/contract-infer.mjs +1 -1
  9. package/dist/commands/db-init.mjs +3 -3
  10. package/dist/commands/db-schema.mjs +3 -3
  11. package/dist/commands/db-sign.mjs +4 -4
  12. package/dist/commands/db-update.mjs +4 -4
  13. package/dist/commands/db-verify.mjs +1 -1
  14. package/dist/commands/migrate.d.mts +1 -1
  15. package/dist/commands/migrate.d.mts.map +1 -1
  16. package/dist/commands/migrate.mjs +35 -35
  17. package/dist/commands/migrate.mjs.map +1 -1
  18. package/dist/commands/migration-check.d.mts +1 -1
  19. package/dist/commands/migration-check.mjs +1 -1
  20. package/dist/commands/migration-graph.d.mts +1 -1
  21. package/dist/commands/migration-graph.mjs +7 -7
  22. package/dist/commands/migration-graph.mjs.map +1 -1
  23. package/dist/commands/migration-list.d.mts +6 -6
  24. package/dist/commands/migration-list.d.mts.map +1 -1
  25. package/dist/commands/migration-list.mjs +1 -1
  26. package/dist/commands/migration-log.d.mts +1 -1
  27. package/dist/commands/migration-log.mjs +1 -1
  28. package/dist/commands/migration-new.mjs +3 -3
  29. package/dist/commands/migration-plan.d.mts.map +1 -1
  30. package/dist/commands/migration-plan.mjs +1 -1
  31. package/dist/commands/migration-show.mjs +3 -3
  32. package/dist/commands/migration-status.d.mts +1 -1
  33. package/dist/commands/migration-status.mjs +1 -1
  34. package/dist/commands/ref.d.mts +1 -1
  35. package/dist/commands/ref.mjs +2 -2
  36. package/dist/commands/telemetry/index.mjs +1 -1
  37. package/dist/{contract-at-errors-DdxWRjMc.mjs → contract-at-errors-bF4CGDl0.mjs} +2 -2
  38. package/dist/{contract-at-errors-DdxWRjMc.mjs.map → contract-at-errors-bF4CGDl0.mjs.map} +1 -1
  39. package/dist/{contract-emit-fz6pDR2n.mjs → contract-emit-BM7oxYIG.mjs} +7 -5
  40. package/dist/{contract-emit-fz6pDR2n.mjs.map → contract-emit-BM7oxYIG.mjs.map} +1 -1
  41. package/dist/{contract-emit-D4xDDFkO.mjs → contract-emit-DnCG2JaQ.mjs} +3 -3
  42. package/dist/{contract-emit-D4xDDFkO.mjs.map → contract-emit-DnCG2JaQ.mjs.map} +1 -1
  43. package/dist/{contract-infer-Ars4yjBv.mjs → contract-infer-Dq9XNQCx.mjs} +3 -3
  44. package/dist/{contract-infer-Ars4yjBv.mjs.map → contract-infer-Dq9XNQCx.mjs.map} +1 -1
  45. package/dist/{contract-space-aggregate-loader-Bu2fjSWV.mjs → contract-space-aggregate-loader-DAa8u6bi.mjs} +2 -2
  46. package/dist/{contract-space-aggregate-loader-Bu2fjSWV.mjs.map → contract-space-aggregate-loader-DAa8u6bi.mjs.map} +1 -1
  47. package/dist/{db-verify-CZxRV_er.mjs → db-verify-BpfJvC8O.mjs} +56 -60
  48. package/dist/db-verify-BpfJvC8O.mjs.map +1 -0
  49. package/dist/exports/control-api.d.mts +3 -3
  50. package/dist/exports/control-api.mjs +2 -2
  51. package/dist/exports/index.mjs +2 -2
  52. package/dist/extension-pack-inputs-1ySHqxKG.mjs.map +1 -1
  53. package/dist/{format-C3XdaDuA.mjs → format-DrUSkdqR.mjs} +2 -2
  54. package/dist/{format-C3XdaDuA.mjs.map → format-DrUSkdqR.mjs.map} +1 -1
  55. package/dist/{framework-components-5RljWT7Q.mjs → framework-components-BdmVlRSS.mjs} +2 -2
  56. package/dist/{framework-components-5RljWT7Q.mjs.map → framework-components-BdmVlRSS.mjs.map} +1 -1
  57. package/dist/{init-C79Yb97u.mjs → init-C0YyVoGj.mjs} +3 -3
  58. package/dist/{init-C79Yb97u.mjs.map → init-C0YyVoGj.mjs.map} +1 -1
  59. package/dist/{inspect-live-schema-DR8Eyt29.mjs → inspect-live-schema-Bs_0M6z8.mjs} +3 -3
  60. package/dist/{inspect-live-schema-DR8Eyt29.mjs.map → inspect-live-schema-Bs_0M6z8.mjs.map} +1 -1
  61. package/dist/{migration-check-CTcJfCC4.mjs → migration-check-DYxrNfK-.mjs} +9 -9
  62. package/dist/migration-check-DYxrNfK-.mjs.map +1 -0
  63. package/dist/{migration-command-scaffold-6OT_Q-3Y.mjs → migration-command-scaffold-DB5DQLvF.mjs} +3 -3
  64. package/dist/{migration-command-scaffold-6OT_Q-3Y.mjs.map → migration-command-scaffold-DB5DQLvF.mjs.map} +1 -1
  65. package/dist/{migration-list-DVHVmMrf.mjs → migration-list-BX5w9HAn.mjs} +14 -14
  66. package/dist/migration-list-BX5w9HAn.mjs.map +1 -0
  67. package/dist/{migration-log-CQpc9JoL.mjs → migration-log-CB1KzVOG.mjs} +3 -3
  68. package/dist/{migration-log-CQpc9JoL.mjs.map → migration-log-CB1KzVOG.mjs.map} +1 -1
  69. package/dist/{migration-path-target-zK6RG6JY.mjs → migration-path-target-CEFRK2Q7.mjs} +2 -2
  70. package/dist/{migration-path-target-zK6RG6JY.mjs.map → migration-path-target-CEFRK2Q7.mjs.map} +1 -1
  71. package/dist/{migration-plan-o2aqjnfd.mjs → migration-plan-DMKl_HnV.mjs} +29 -28
  72. package/dist/migration-plan-DMKl_HnV.mjs.map +1 -0
  73. package/dist/{migration-status-2aXGtYMW.mjs → migration-status-BeNAmEju.mjs} +12 -12
  74. package/dist/migration-status-BeNAmEju.mjs.map +1 -0
  75. package/dist/{telemetry-BckOSUWG.mjs → telemetry-_19qtfXp.mjs} +2 -2
  76. package/dist/{telemetry-BckOSUWG.mjs.map → telemetry-_19qtfXp.mjs.map} +1 -1
  77. package/dist/{types-qETCCNbe.d.mts → types-DFoqNIcQ.d.mts} +26 -20
  78. package/dist/types-DFoqNIcQ.d.mts.map +1 -0
  79. package/dist/{verify-DLqhL2LU.mjs → verify-Djo-Jep_.mjs} +57 -159
  80. package/dist/verify-Djo-Jep_.mjs.map +1 -0
  81. package/package.json +21 -21
  82. package/src/commands/db-verify.ts +31 -19
  83. package/src/commands/lsp.ts +3 -1
  84. package/src/commands/migrate.ts +46 -43
  85. package/src/commands/migration-check.ts +5 -5
  86. package/src/commands/migration-graph.ts +4 -4
  87. package/src/commands/migration-list.ts +12 -12
  88. package/src/commands/migration-plan.ts +14 -3
  89. package/src/commands/migration-status.ts +9 -9
  90. package/src/control-api/client.ts +5 -3
  91. package/src/control-api/operations/contract-emit.ts +7 -6
  92. package/src/control-api/operations/db-init.ts +2 -2
  93. package/src/control-api/operations/db-run.ts +18 -17
  94. package/src/control-api/operations/db-verify.ts +52 -44
  95. package/src/control-api/operations/migrate.ts +72 -72
  96. package/src/control-api/operations/run-migration.ts +6 -5
  97. package/src/control-api/types.ts +8 -8
  98. package/src/utils/combine-verify-results.ts +94 -0
  99. package/src/utils/extension-pack-inputs.ts +1 -1
  100. package/src/utils/formatters/errors.ts +12 -4
  101. package/src/utils/formatters/verify.ts +88 -227
  102. package/src/utils/plan-resolution.ts +18 -18
  103. package/dist/client-EnAjSmZg.mjs.map +0 -1
  104. package/dist/command-helpers-6cNJZ863.mjs.map +0 -1
  105. package/dist/db-verify-CZxRV_er.mjs.map +0 -1
  106. package/dist/migration-check-CTcJfCC4.mjs.map +0 -1
  107. package/dist/migration-list-DVHVmMrf.mjs.map +0 -1
  108. package/dist/migration-plan-o2aqjnfd.mjs.map +0 -1
  109. package/dist/migration-status-2aXGtYMW.mjs.map +0 -1
  110. package/dist/types-qETCCNbe.d.mts.map +0 -1
  111. package/dist/verify-DLqhL2LU.mjs.map +0 -1
  112. package/src/utils/combine-schema-results.ts +0 -84
@@ -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 member). 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
  /**
@@ -229,7 +230,7 @@ export function buildPerSpaceBreakdown(
229
230
 
230
231
  /**
231
232
  * Materialise the `applyOrder` ordering into resolved per-space
232
- * entries. Throws if the planner output is missing a member listed
233
+ * entries. Throws if the planner output is missing a contract space listed
233
234
  * in `applyOrder` — a wiring bug that should never reach runtime.
234
235
  *
235
236
  * Exported so callers building their own success envelopes after a
@@ -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
@@ -557,9 +557,9 @@ export interface MigrateOptions {
557
557
  /** Migrations root directory (`migrations/` under the project). */
558
558
  readonly migrationsDir: string;
559
559
  /**
560
- * Optional app-space ref override. When provided, the app member's
560
+ * Optional app-space ref override. When provided, the app space's
561
561
  * graph-walk targets this hash instead of `contract.storage.storageHash`.
562
- * Extension members always walk to their own `headRef.hash`.
562
+ * Extension spaces always walk to their own `headRef.hash`.
563
563
  */
564
564
  readonly refHash?: string;
565
565
  /**
@@ -631,17 +631,17 @@ export interface MigrateRanEntry {
631
631
 
632
632
  /**
633
633
  * Successful migrate result. Carries both the top-level fields
634
- * (`markerHash` is the **app member's** post-migrate marker) and the
634
+ * (`markerHash` is the **app space's** post-migrate marker) and the
635
635
  * per-space breakdown (`perSpace` — markers / operations in canonical
636
636
  * schedule order).
637
637
  */
638
638
  /**
639
- * Path-decision summary for the **app member** post-migrate. Surfaced
639
+ * Path-decision summary for the **app space** post-migrate. Surfaced
640
640
  * at the top level (and consumed by the cli-journeys suite, which
641
641
  * inspects `requiredInvariants`/`satisfiedInvariants`/
642
642
  * `selectedPath` to validate invariant routing).
643
643
  *
644
- * Per-space path decisions for extension members are not surfaced —
644
+ * Per-space path decisions for extension spaces are not surfaced —
645
645
  * extensions own their own ref/invariant control.
646
646
  */
647
647
  export interface MigratePathDecision {
@@ -673,7 +673,7 @@ export interface MigrateSuccess {
673
673
  */
674
674
  readonly perSpace: ReadonlyArray<PerSpaceExecutionEntry>;
675
675
  /**
676
- * Path-decision data for the app member. Present whenever the
676
+ * Path-decision data for the app space. Present whenever the
677
677
  * graph-walk strategy ran for the app (i.e. always for the
678
678
  * aggregate-walking migrate path). Absent only for the no-op
679
679
  * "Already up to date" early return when the app has no plan.
@@ -864,7 +864,7 @@ export interface ControlClient {
864
864
  * markers, and (unless `skipSchema` is true) per-space schema
865
865
  * verification with pre-projection (closes F23).
866
866
  *
867
- * @returns Result pattern: per-space schema results on success;
867
+ * @returns Result pattern: per-space verify results on success;
868
868
  * structured CLI error on marker / loader failure.
869
869
  * @throws If not connected or infrastructure failure
870
870
  */
@@ -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.
@@ -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,9 +45,20 @@ 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
  };
56
+ /**
57
+ * Live element names no contract space declares. In full success this is
58
+ * only ever non-empty in lenient mode — strict mode fails on it — and is
59
+ * rendered informationally.
60
+ */
61
+ readonly unclaimed?: readonly string[];
33
62
  readonly warning?: string;
34
63
  readonly meta?:
35
64
  | (NonNullable<VerifyDatabaseResult['meta']> & {
@@ -71,11 +100,22 @@ export function formatVerifyOutput(
71
100
  if (result.contract.profileHash) {
72
101
  lines.push(`${formatDimText(` profileHash: ${result.contract.profileHash}`)}`);
73
102
  }
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
- );
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
+ }
78
109
  }
110
+
111
+ if (result.unclaimed && result.unclaimed.length > 0) {
112
+ lines.push('');
113
+ lines.push(formatYellow('Unclaimed elements (declared by no contract):'));
114
+ for (const name of result.unclaimed) {
115
+ lines.push(` ${formatYellow('⚠')} ${name}`);
116
+ }
117
+ }
118
+
79
119
  if (result.warning) {
80
120
  lines.push('');
81
121
  lines.push(`${formatYellow('⚠')} ${result.warning}`);
@@ -107,6 +147,7 @@ export function formatVerifyJson(result: DbVerifyCommandSuccessResult): string {
107
147
  ...ifDefined('missingCodecs', result.missingCodecs),
108
148
  ...ifDefined('codecCoverageSkipped', result.codecCoverageSkipped),
109
149
  ...ifDefined('schema', result.schema),
150
+ unclaimed: result.unclaimed ?? [],
110
151
  ...ifDefined('warning', result.warning),
111
152
  ...ifDefined('meta', result.meta),
112
153
  timings: result.timings,
@@ -124,7 +165,7 @@ export function formatIntrospectJson(result: IntrospectSchemaResult<unknown>): s
124
165
 
125
166
  /**
126
167
  * Renders a schema tree structure from CoreSchemaView.
127
- * Matches the style of renderSchemaVerificationTree for consistency.
168
+ * Status-glyph tree styling shared with the retired verification-tree renderer.
128
169
  */
129
170
  function renderSchemaTree(
130
171
  node: SchemaTreeNode,
@@ -310,213 +351,13 @@ export function formatIntrospectOutput(
310
351
  return lines.join('\n');
311
352
  }
312
353
 
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
354
  /**
515
355
  * Formats human-readable output for database schema verification.
516
356
  */
517
357
  export function formatSchemaVerifyOutput(
518
358
  result: VerifyDatabaseSchemaResult,
519
359
  flags: GlobalFlags,
360
+ unclaimed: readonly string[] = [],
520
361
  ): string {
521
362
  if (flags.quiet) {
522
363
  return '';
@@ -527,30 +368,45 @@ export function formatSchemaVerifyOutput(
527
368
  const useColor = flags.color !== false;
528
369
  const formatGreen = createColorFormatter(useColor, green);
529
370
  const formatRed = createColorFormatter(useColor, red);
371
+ const formatYellow = createColorFormatter(useColor, yellow);
530
372
  const formatDimText = (text: string) => formatDim(useColor, text);
531
373
 
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`)}`);
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}`);
388
+ }
389
+ }
390
+
391
+ if (unclaimed.length > 0) {
392
+ const strict = result.meta?.strict ?? false;
393
+ if (lines.length > 0) lines.push('');
545
394
  lines.push(
546
- `${formatDimText(` pass=${result.schema.counts.pass} warn=${result.schema.counts.warn} fail=${result.schema.counts.fail}`)}`,
395
+ (strict ? formatRed : formatYellow)('Unclaimed elements (declared by no contract):'),
547
396
  );
397
+ for (const name of unclaimed) {
398
+ lines.push(` ${(strict ? formatRed : formatYellow)(strict ? '✖' : '⚠')} ${name}`);
399
+ }
400
+ }
401
+
402
+ if (isVerbose(flags, 1)) {
403
+ lines.push(`${formatDimText(` Total time: ${result.timings.total}ms`)}`);
548
404
  }
549
405
 
550
406
  // Blank line before summary
551
- lines.push('');
407
+ if (lines.length > 0) lines.push('');
552
408
 
553
- // Summary line at the end: summary with status glyph
409
+ // Summary line at the end: verdict with status glyph
554
410
  if (result.ok) {
555
411
  lines.push(`${formatGreen('✔')} ${result.summary}`);
556
412
  } else {
@@ -562,10 +418,15 @@ export function formatSchemaVerifyOutput(
562
418
  }
563
419
 
564
420
  /**
565
- * Formats JSON output for database schema verification.
421
+ * Formats JSON output for database schema verification. The unclaimed-elements
422
+ * list is a top-level field alongside the combined result, reported once for
423
+ * the whole database.
566
424
  */
567
- export function formatSchemaVerifyJson(result: VerifyDatabaseSchemaResult): string {
568
- return JSON.stringify(result, null, 2);
425
+ export function formatSchemaVerifyJson(
426
+ result: VerifyDatabaseSchemaResult,
427
+ unclaimed: readonly string[] = [],
428
+ ): string {
429
+ return JSON.stringify({ ...result, unclaimed }, null, 2);
569
430
  }
570
431
 
571
432
  // ============================================================================