@prisma-next/cli 0.14.0-dev.5 → 0.14.0-dev.51
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.
- package/dist/cli.mjs +14 -14
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-EnAjSmZg.mjs → client-DL6KE_c0.mjs} +88 -99
- package/dist/client-DL6KE_c0.mjs.map +1 -0
- package/dist/{command-helpers-6cNJZ863.mjs → command-helpers-DMG0RsR1.mjs} +3 -3
- package/dist/command-helpers-DMG0RsR1.mjs.map +1 -0
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +3 -3
- package/dist/commands/db-schema.mjs +3 -3
- package/dist/commands/db-sign.mjs +4 -4
- package/dist/commands/db-update.mjs +4 -4
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +1 -1
- package/dist/commands/migrate.d.mts.map +1 -1
- package/dist/commands/migrate.mjs +35 -35
- package/dist/commands/migrate.mjs.map +1 -1
- package/dist/commands/migration-check.d.mts +1 -1
- package/dist/commands/migration-check.mjs +1 -1
- package/dist/commands/migration-graph.d.mts +1 -1
- package/dist/commands/migration-graph.mjs +7 -7
- package/dist/commands/migration-graph.mjs.map +1 -1
- package/dist/commands/migration-list.d.mts +6 -6
- package/dist/commands/migration-list.d.mts.map +1 -1
- package/dist/commands/migration-list.mjs +1 -1
- package/dist/commands/migration-log.d.mts +1 -1
- package/dist/commands/migration-log.mjs +1 -1
- package/dist/commands/migration-new.mjs +3 -3
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.mjs +3 -3
- package/dist/commands/migration-status.d.mts +1 -1
- package/dist/commands/migration-status.mjs +1 -1
- package/dist/commands/ref.d.mts +1 -1
- package/dist/commands/ref.mjs +2 -2
- package/dist/commands/telemetry/index.mjs +1 -1
- package/dist/{contract-at-errors-DdxWRjMc.mjs → contract-at-errors-bF4CGDl0.mjs} +2 -2
- package/dist/{contract-at-errors-DdxWRjMc.mjs.map → contract-at-errors-bF4CGDl0.mjs.map} +1 -1
- package/dist/{contract-emit-fz6pDR2n.mjs → contract-emit-BM7oxYIG.mjs} +7 -5
- package/dist/{contract-emit-fz6pDR2n.mjs.map → contract-emit-BM7oxYIG.mjs.map} +1 -1
- package/dist/{contract-emit-D4xDDFkO.mjs → contract-emit-DnCG2JaQ.mjs} +3 -3
- package/dist/{contract-emit-D4xDDFkO.mjs.map → contract-emit-DnCG2JaQ.mjs.map} +1 -1
- package/dist/{contract-infer-Ars4yjBv.mjs → contract-infer-Dq9XNQCx.mjs} +3 -3
- package/dist/{contract-infer-Ars4yjBv.mjs.map → contract-infer-Dq9XNQCx.mjs.map} +1 -1
- package/dist/{contract-space-aggregate-loader-Bu2fjSWV.mjs → contract-space-aggregate-loader-DAa8u6bi.mjs} +2 -2
- package/dist/{contract-space-aggregate-loader-Bu2fjSWV.mjs.map → contract-space-aggregate-loader-DAa8u6bi.mjs.map} +1 -1
- package/dist/{db-verify-CZxRV_er.mjs → db-verify-BpfJvC8O.mjs} +56 -60
- package/dist/db-verify-BpfJvC8O.mjs.map +1 -0
- package/dist/exports/control-api.d.mts +3 -3
- package/dist/exports/control-api.mjs +2 -2
- package/dist/exports/index.mjs +2 -2
- package/dist/extension-pack-inputs-1ySHqxKG.mjs.map +1 -1
- package/dist/{format-C3XdaDuA.mjs → format-DrUSkdqR.mjs} +2 -2
- package/dist/{format-C3XdaDuA.mjs.map → format-DrUSkdqR.mjs.map} +1 -1
- package/dist/{framework-components-5RljWT7Q.mjs → framework-components-BdmVlRSS.mjs} +2 -2
- package/dist/{framework-components-5RljWT7Q.mjs.map → framework-components-BdmVlRSS.mjs.map} +1 -1
- package/dist/{init-C79Yb97u.mjs → init-C0YyVoGj.mjs} +3 -3
- package/dist/{init-C79Yb97u.mjs.map → init-C0YyVoGj.mjs.map} +1 -1
- package/dist/{inspect-live-schema-DR8Eyt29.mjs → inspect-live-schema-Bs_0M6z8.mjs} +3 -3
- package/dist/{inspect-live-schema-DR8Eyt29.mjs.map → inspect-live-schema-Bs_0M6z8.mjs.map} +1 -1
- package/dist/{migration-check-CTcJfCC4.mjs → migration-check-DYxrNfK-.mjs} +9 -9
- package/dist/migration-check-DYxrNfK-.mjs.map +1 -0
- package/dist/{migration-command-scaffold-6OT_Q-3Y.mjs → migration-command-scaffold-DB5DQLvF.mjs} +3 -3
- package/dist/{migration-command-scaffold-6OT_Q-3Y.mjs.map → migration-command-scaffold-DB5DQLvF.mjs.map} +1 -1
- package/dist/{migration-list-DVHVmMrf.mjs → migration-list-BX5w9HAn.mjs} +14 -14
- package/dist/migration-list-BX5w9HAn.mjs.map +1 -0
- package/dist/{migration-log-CQpc9JoL.mjs → migration-log-CB1KzVOG.mjs} +3 -3
- package/dist/{migration-log-CQpc9JoL.mjs.map → migration-log-CB1KzVOG.mjs.map} +1 -1
- package/dist/{migration-path-target-zK6RG6JY.mjs → migration-path-target-CEFRK2Q7.mjs} +2 -2
- package/dist/{migration-path-target-zK6RG6JY.mjs.map → migration-path-target-CEFRK2Q7.mjs.map} +1 -1
- package/dist/{migration-plan-o2aqjnfd.mjs → migration-plan-DMKl_HnV.mjs} +29 -28
- package/dist/migration-plan-DMKl_HnV.mjs.map +1 -0
- package/dist/{migration-status-2aXGtYMW.mjs → migration-status-BeNAmEju.mjs} +12 -12
- package/dist/migration-status-BeNAmEju.mjs.map +1 -0
- package/dist/{telemetry-BckOSUWG.mjs → telemetry-_19qtfXp.mjs} +2 -2
- package/dist/{telemetry-BckOSUWG.mjs.map → telemetry-_19qtfXp.mjs.map} +1 -1
- package/dist/{types-qETCCNbe.d.mts → types-DFoqNIcQ.d.mts} +26 -20
- package/dist/types-DFoqNIcQ.d.mts.map +1 -0
- package/dist/{verify-DLqhL2LU.mjs → verify-Djo-Jep_.mjs} +57 -159
- package/dist/verify-Djo-Jep_.mjs.map +1 -0
- package/package.json +21 -21
- package/src/commands/db-verify.ts +31 -19
- package/src/commands/lsp.ts +3 -1
- package/src/commands/migrate.ts +46 -43
- package/src/commands/migration-check.ts +5 -5
- package/src/commands/migration-graph.ts +4 -4
- package/src/commands/migration-list.ts +12 -12
- package/src/commands/migration-plan.ts +14 -3
- package/src/commands/migration-status.ts +9 -9
- package/src/control-api/client.ts +5 -3
- package/src/control-api/operations/contract-emit.ts +7 -6
- package/src/control-api/operations/db-init.ts +2 -2
- package/src/control-api/operations/db-run.ts +18 -17
- package/src/control-api/operations/db-verify.ts +52 -44
- package/src/control-api/operations/migrate.ts +72 -72
- package/src/control-api/operations/run-migration.ts +6 -5
- package/src/control-api/types.ts +8 -8
- package/src/utils/combine-verify-results.ts +94 -0
- package/src/utils/extension-pack-inputs.ts +1 -1
- package/src/utils/formatters/errors.ts +12 -4
- package/src/utils/formatters/verify.ts +88 -227
- package/src/utils/plan-resolution.ts +18 -18
- package/dist/client-EnAjSmZg.mjs.map +0 -1
- package/dist/command-helpers-6cNJZ863.mjs.map +0 -1
- package/dist/db-verify-CZxRV_er.mjs.map +0 -1
- package/dist/migration-check-CTcJfCC4.mjs.map +0 -1
- package/dist/migration-list-DVHVmMrf.mjs.map +0 -1
- package/dist/migration-plan-o2aqjnfd.mjs.map +0 -1
- package/dist/migration-status-2aXGtYMW.mjs.map +0 -1
- package/dist/types-qETCCNbe.d.mts.map +0 -1
- package/dist/verify-DLqhL2LU.mjs.map +0 -1
- 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` —
|
|
49
|
-
* for the app,
|
|
50
|
-
* {@link
|
|
51
|
-
* for every
|
|
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
|
|
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
|
package/src/control-api/types.ts
CHANGED
|
@@ -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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
69
|
+
const label = issue.kind ?? issue.reason ?? 'issue';
|
|
62
70
|
const message = issue.message ?? '';
|
|
63
|
-
lines.push(`${formatDimText(` - [${
|
|
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.
|
|
75
|
-
lines.push(
|
|
76
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
568
|
-
|
|
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
|
// ============================================================================
|