@ddt-tools/cli 0.2.0 → 0.2.4

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 (203) hide show
  1. package/dist/advise-tests-YNMKVJCD.js +87 -0
  2. package/dist/advise-tests-YNMKVJCD.js.map +1 -0
  3. package/dist/ai-NTNPYEKZ.js +86 -0
  4. package/dist/ai-NTNPYEKZ.js.map +1 -0
  5. package/dist/anonymize-LERTWUQO.js +139 -0
  6. package/dist/anonymize-LERTWUQO.js.map +1 -0
  7. package/dist/approval-GGZGKIU4.js +73 -0
  8. package/dist/approval-GGZGKIU4.js.map +1 -0
  9. package/dist/approval-chain-GWJKZHVU.js +118 -0
  10. package/dist/approval-chain-GWJKZHVU.js.map +1 -0
  11. package/dist/audit-log-2PH55BU4.js +159 -0
  12. package/dist/audit-log-2PH55BU4.js.map +1 -0
  13. package/dist/backlog-QNXGOUF4.js +76 -0
  14. package/dist/backlog-QNXGOUF4.js.map +1 -0
  15. package/dist/bisect-W3XKKRWG.js +111 -0
  16. package/dist/bisect-W3XKKRWG.js.map +1 -0
  17. package/dist/bookmarks-XVOGXGMC.js +107 -0
  18. package/dist/bookmarks-XVOGXGMC.js.map +1 -0
  19. package/dist/branch-S3I2IJGQ.js +103 -0
  20. package/dist/branch-S3I2IJGQ.js.map +1 -0
  21. package/dist/build-MP3JQEFO.js +20 -0
  22. package/dist/build-MP3JQEFO.js.map +1 -0
  23. package/dist/catalog-3J3NFNXP.js +137 -0
  24. package/dist/catalog-3J3NFNXP.js.map +1 -0
  25. package/dist/changelog-ZQAH3ULB.js +216 -0
  26. package/dist/changelog-ZQAH3ULB.js.map +1 -0
  27. package/dist/chunk-2FT6HXKS.js +55 -0
  28. package/dist/chunk-2FT6HXKS.js.map +1 -0
  29. package/dist/chunk-DGUM43GV.js +11 -0
  30. package/dist/chunk-DGUM43GV.js.map +1 -0
  31. package/dist/chunk-DL3V7UJ2.js +25 -0
  32. package/dist/chunk-DL3V7UJ2.js.map +1 -0
  33. package/dist/chunk-VM2H4LAO.js +15 -0
  34. package/dist/chunk-VM2H4LAO.js.map +1 -0
  35. package/dist/chunk-XFXG347C.js +40 -0
  36. package/dist/chunk-XFXG347C.js.map +1 -0
  37. package/dist/cli.js +499 -19402
  38. package/dist/cli.js.map +1 -1
  39. package/dist/compare-P7JOV76O.js +379 -0
  40. package/dist/compare-P7JOV76O.js.map +1 -0
  41. package/dist/compare-profiles-H33CXZPD.js +219 -0
  42. package/dist/compare-profiles-H33CXZPD.js.map +1 -0
  43. package/dist/completion-ZSNCQKJ2.js +89 -0
  44. package/dist/completion-ZSNCQKJ2.js.map +1 -0
  45. package/dist/connection-CDGVEFUC.js +148 -0
  46. package/dist/connection-CDGVEFUC.js.map +1 -0
  47. package/dist/cost-estimate-S2MKHT2H.js +321 -0
  48. package/dist/cost-estimate-S2MKHT2H.js.map +1 -0
  49. package/dist/data-compare-46ZI7KHL.js +128 -0
  50. package/dist/data-compare-46ZI7KHL.js.map +1 -0
  51. package/dist/data-fit-WGEPLD5S.js +127 -0
  52. package/dist/data-fit-WGEPLD5S.js.map +1 -0
  53. package/dist/deploy-status-4H5KJFRC.js +58 -0
  54. package/dist/deploy-status-4H5KJFRC.js.map +1 -0
  55. package/dist/design-ILX3ZSWW.js +135 -0
  56. package/dist/design-ILX3ZSWW.js.map +1 -0
  57. package/dist/diagnose-WPUL67E4.js +150 -0
  58. package/dist/diagnose-WPUL67E4.js.map +1 -0
  59. package/dist/discover-DEO2R5T6.js +78 -0
  60. package/dist/discover-DEO2R5T6.js.map +1 -0
  61. package/dist/docs-QNY3MUVO.js +183 -0
  62. package/dist/docs-QNY3MUVO.js.map +1 -0
  63. package/dist/drift-FDRNPWQA.js +233 -0
  64. package/dist/drift-FDRNPWQA.js.map +1 -0
  65. package/dist/drift-gate-6BWWWMHW.js +103 -0
  66. package/dist/drift-gate-6BWWWMHW.js.map +1 -0
  67. package/dist/error-lookup-4R3Y4RBC.js +56 -0
  68. package/dist/error-lookup-4R3Y4RBC.js.map +1 -0
  69. package/dist/errorReporting-3LPE2IJY.js +109 -0
  70. package/dist/errorReporting-3LPE2IJY.js.map +1 -0
  71. package/dist/exec-JOLH5LPT.js +122 -0
  72. package/dist/exec-JOLH5LPT.js.map +1 -0
  73. package/dist/explain-NS26WE2Y.js +189 -0
  74. package/dist/explain-NS26WE2Y.js.map +1 -0
  75. package/dist/explorer-GSYYYOAL.js +58 -0
  76. package/dist/explorer-GSYYYOAL.js.map +1 -0
  77. package/dist/extract-4LWEZG4O.js +152 -0
  78. package/dist/extract-4LWEZG4O.js.map +1 -0
  79. package/dist/features-KQV4OFIZ.js +54 -0
  80. package/dist/features-KQV4OFIZ.js.map +1 -0
  81. package/dist/feedback-CBLGXUEG.js +158 -0
  82. package/dist/feedback-CBLGXUEG.js.map +1 -0
  83. package/dist/find-SMXRCZ76.js +176 -0
  84. package/dist/find-SMXRCZ76.js.map +1 -0
  85. package/dist/format-HMGG6MY3.js +277 -0
  86. package/dist/format-HMGG6MY3.js.map +1 -0
  87. package/dist/generate-W7VLBDLI.js +160 -0
  88. package/dist/generate-W7VLBDLI.js.map +1 -0
  89. package/dist/graph-YYL5UYCJ.js +168 -0
  90. package/dist/graph-YYL5UYCJ.js.map +1 -0
  91. package/dist/history-GDRFP4PG.js +184 -0
  92. package/dist/history-GDRFP4PG.js.map +1 -0
  93. package/dist/hosts-DRFZTMIJ.js +45 -0
  94. package/dist/hosts-DRFZTMIJ.js.map +1 -0
  95. package/dist/impact-A4NU6CB2.js +63 -0
  96. package/dist/impact-A4NU6CB2.js.map +1 -0
  97. package/dist/import-2RNYDL4E.js +79 -0
  98. package/dist/import-2RNYDL4E.js.map +1 -0
  99. package/dist/index.cjs +11 -5
  100. package/dist/index.cjs.map +1 -1
  101. package/dist/index.js +8 -2
  102. package/dist/index.js.map +1 -1
  103. package/dist/init-EAOGNGXI.js +54 -0
  104. package/dist/init-EAOGNGXI.js.map +1 -0
  105. package/dist/install-hooks-G3Y5LVXK.js +109 -0
  106. package/dist/install-hooks-G3Y5LVXK.js.map +1 -0
  107. package/dist/license-Z5YSC7XQ.js +43 -0
  108. package/dist/license-Z5YSC7XQ.js.map +1 -0
  109. package/dist/lineage-C5CGVP36.js +555 -0
  110. package/dist/lineage-C5CGVP36.js.map +1 -0
  111. package/dist/lint-AQFPZ3WG.js +144 -0
  112. package/dist/lint-AQFPZ3WG.js.map +1 -0
  113. package/dist/mcp-F7FND5X7.js +343 -0
  114. package/dist/mcp-F7FND5X7.js.map +1 -0
  115. package/dist/migrate-from-dbt-K4ELOWUD.js +156 -0
  116. package/dist/migrate-from-dbt-K4ELOWUD.js.map +1 -0
  117. package/dist/migrate-platform-E7VZFPO5.js +91 -0
  118. package/dist/migrate-platform-E7VZFPO5.js.map +1 -0
  119. package/dist/optimize-WUJ5ZN5Y.js +109 -0
  120. package/dist/optimize-WUJ5ZN5Y.js.map +1 -0
  121. package/dist/perf-UULZSREY.js +200 -0
  122. package/dist/perf-UULZSREY.js.map +1 -0
  123. package/dist/pii-QHU32VML.js +146 -0
  124. package/dist/pii-QHU32VML.js.map +1 -0
  125. package/dist/pilot-BR6GVK32.js +29 -0
  126. package/dist/pilot-BR6GVK32.js.map +1 -0
  127. package/dist/pr-comment-2FOA3EXG.js +81 -0
  128. package/dist/pr-comment-2FOA3EXG.js.map +1 -0
  129. package/dist/preview-XNY422OU.js +46 -0
  130. package/dist/preview-XNY422OU.js.map +1 -0
  131. package/dist/profile-SQTBNKYS.js +98 -0
  132. package/dist/profile-SQTBNKYS.js.map +1 -0
  133. package/dist/promote-FSGUPIPD.js +417 -0
  134. package/dist/promote-FSGUPIPD.js.map +1 -0
  135. package/dist/publish-AYCRMCE2.js +739 -0
  136. package/dist/publish-AYCRMCE2.js.map +1 -0
  137. package/dist/purge-Y5IOTXKA.js +56 -0
  138. package/dist/purge-Y5IOTXKA.js.map +1 -0
  139. package/dist/query-log-SDDGMJLJ.js +112 -0
  140. package/dist/query-log-SDDGMJLJ.js.map +1 -0
  141. package/dist/refactor-TC7S43F2.js +5809 -0
  142. package/dist/refactor-TC7S43F2.js.map +1 -0
  143. package/dist/refresh-MDJYOYV5.js +39 -0
  144. package/dist/refresh-MDJYOYV5.js.map +1 -0
  145. package/dist/replay-E4664A5K.js +118 -0
  146. package/dist/replay-E4664A5K.js.map +1 -0
  147. package/dist/revert-QWQWCJJB.js +111 -0
  148. package/dist/revert-QWQWCJJB.js.map +1 -0
  149. package/dist/review-7CAVLD67.js +164 -0
  150. package/dist/review-7CAVLD67.js.map +1 -0
  151. package/dist/rollback-suggest-C6D5YFCA.js +79 -0
  152. package/dist/rollback-suggest-C6D5YFCA.js.map +1 -0
  153. package/dist/safer-alternative-QR4QEFUV.js +84 -0
  154. package/dist/safer-alternative-QR4QEFUV.js.map +1 -0
  155. package/dist/safety-OFWUFLK4.js +165 -0
  156. package/dist/safety-OFWUFLK4.js.map +1 -0
  157. package/dist/savings-MEBE4TXI.js +95 -0
  158. package/dist/savings-MEBE4TXI.js.map +1 -0
  159. package/dist/scan-secrets-XCUBMLHL.js +54 -0
  160. package/dist/scan-secrets-XCUBMLHL.js.map +1 -0
  161. package/dist/schema-7JZIG6QR.js +447 -0
  162. package/dist/schema-7JZIG6QR.js.map +1 -0
  163. package/dist/script-BMYVBHFR.js +167 -0
  164. package/dist/script-BMYVBHFR.js.map +1 -0
  165. package/dist/search-TA3C3AZT.js +151 -0
  166. package/dist/search-TA3C3AZT.js.map +1 -0
  167. package/dist/seed-W4Q3L2IU.js +101 -0
  168. package/dist/seed-W4Q3L2IU.js.map +1 -0
  169. package/dist/sketch-6B2V6FJV.js +83 -0
  170. package/dist/sketch-6B2V6FJV.js.map +1 -0
  171. package/dist/snapshot-YMVS322L.js +171 -0
  172. package/dist/snapshot-YMVS322L.js.map +1 -0
  173. package/dist/snippets-EVTN63OU.js +74 -0
  174. package/dist/snippets-EVTN63OU.js.map +1 -0
  175. package/dist/standards-FGJW3CQL.js +238 -0
  176. package/dist/standards-FGJW3CQL.js.map +1 -0
  177. package/dist/suggest-V3LVIFZ5.js +44 -0
  178. package/dist/suggest-V3LVIFZ5.js.map +1 -0
  179. package/dist/suggest-constraints-EX2FCWOQ.js +154 -0
  180. package/dist/suggest-constraints-EX2FCWOQ.js.map +1 -0
  181. package/dist/suite-YTQ3CNX5.js +85 -0
  182. package/dist/suite-YTQ3CNX5.js.map +1 -0
  183. package/dist/telemetry-KOIY3NEQ.js +90 -0
  184. package/dist/telemetry-KOIY3NEQ.js.map +1 -0
  185. package/dist/template-MUJ6X6LN.js +396 -0
  186. package/dist/template-MUJ6X6LN.js.map +1 -0
  187. package/dist/test-XFSQHR2S.js +169 -0
  188. package/dist/test-XFSQHR2S.js.map +1 -0
  189. package/dist/trial-GFTGYCR3.js +31 -0
  190. package/dist/trial-GFTGYCR3.js.map +1 -0
  191. package/dist/validate-LFDEZFFH.js +107 -0
  192. package/dist/validate-LFDEZFFH.js.map +1 -0
  193. package/dist/verify-KRDYOJCR.js +76 -0
  194. package/dist/verify-KRDYOJCR.js.map +1 -0
  195. package/dist/watch-FSG23RR3.js +80 -0
  196. package/dist/watch-FSG23RR3.js.map +1 -0
  197. package/dist/xcompare-U4TXTTIR.js +87 -0
  198. package/dist/xcompare-U4TXTTIR.js.map +1 -0
  199. package/package.json +2 -2
  200. package/dist/cli.cjs +0 -19298
  201. package/dist/cli.cjs.map +0 -1
  202. package/dist/cli.d.cts +0 -1
  203. package/dist/cli.d.ts +0 -1
@@ -0,0 +1,379 @@
1
+ import {
2
+ attachRelatedOptions
3
+ } from "./chunk-DL3V7UJ2.js";
4
+ import {
5
+ addMappingFlags,
6
+ buildMappingFromOptions,
7
+ describeMapping
8
+ } from "./chunk-2FT6HXKS.js";
9
+ import "./chunk-DGUM43GV.js";
10
+
11
+ // src/commands/compare.ts
12
+ import { promises as fs } from "fs";
13
+ import path from "path";
14
+ import { Command } from "commander";
15
+ import {
16
+ CompareEngine,
17
+ PacSource,
18
+ ai,
19
+ compileSlice,
20
+ pac as pacNs,
21
+ renderHtmlReport,
22
+ safety,
23
+ typecheck,
24
+ writeCompareHistory
25
+ } from "@ddt-tools/core";
26
+
27
+ // src/util/color.ts
28
+ var ANSI = {
29
+ reset: "\x1B[0m",
30
+ bold: "\x1B[1m",
31
+ red: "\x1B[31m",
32
+ yellow: "\x1B[33m",
33
+ cyan: "\x1B[36m",
34
+ green: "\x1B[32m",
35
+ gray: "\x1B[90m"
36
+ };
37
+ function resolveColorMode(flag) {
38
+ const mode = (flag ?? "auto").toLowerCase();
39
+ if (mode === "always") return true;
40
+ if (mode === "never") return false;
41
+ if (process.env.NO_COLOR) return false;
42
+ return Boolean(process.stdout.isTTY);
43
+ }
44
+ function makeColorizer(enabled) {
45
+ if (!enabled) {
46
+ const identity = (s) => s;
47
+ return {
48
+ unrecoverable: identity,
49
+ destructive: identity,
50
+ expensive: identity,
51
+ warning: identity,
52
+ safe: identity,
53
+ dim: identity,
54
+ applyToBlock: identity
55
+ };
56
+ }
57
+ const wrap = (codes) => (s) => `${codes}${s}${ANSI.reset}`;
58
+ return {
59
+ unrecoverable: wrap(ANSI.bold + ANSI.red),
60
+ destructive: wrap(ANSI.red),
61
+ expensive: wrap(ANSI.yellow),
62
+ warning: wrap(ANSI.cyan),
63
+ safe: wrap(ANSI.green),
64
+ dim: wrap(ANSI.gray),
65
+ applyToBlock(block) {
66
+ return block.split("\n").map((line) => {
67
+ if (/UNRECOVERABLE/.test(line) || line.includes("\u{1F6D1}"))
68
+ return ANSI.bold + ANSI.red + line + ANSI.reset;
69
+ if (/DESTRUCTIVE/.test(line)) return ANSI.red + line + ANSI.reset;
70
+ if (/EXPENSIVE/.test(line)) return ANSI.yellow + line + ANSI.reset;
71
+ if (/WARNING/.test(line) || line.includes("\u26A0")) return ANSI.cyan + line + ANSI.reset;
72
+ if (/\bSAFE\b|\bOK\b/.test(line) || line.includes("\u2713"))
73
+ return ANSI.green + line + ANSI.reset;
74
+ return line;
75
+ }).join("\n");
76
+ }
77
+ };
78
+ }
79
+
80
+ // src/commands/compare.ts
81
+ function compareCommand() {
82
+ const cmd = new Command("compare");
83
+ cmd.description("Compare two .ddtpac files (pac\u2194pac); project / live compare pending v0.3 polish.").requiredOption("--source <path>", "Source .ddtpac (the desired state).").requiredOption("--target <path>", "Target .ddtpac (the current state).").option("--ignore-case", "Compare object FQNs case-insensitively.", false).option(
84
+ "--json",
85
+ "Emit a JSON CompareResult instead of human-readable output. (Alias for --format json.)",
86
+ false
87
+ ).option(
88
+ "--format <fmt>",
89
+ "Output format: summary | json | markdown. Default summary.",
90
+ "summary"
91
+ ).option("--report-html <path>", "Also write a self-contained HTML compare-report to <path>.").option(
92
+ "--no-slice",
93
+ "Disable the source pac's Project Slice (if it has one). Default: a sliced pac is partitioned automatically."
94
+ ).option(
95
+ "--explain",
96
+ "After the diff, call the configured AI provider to narrate each change in plain English with reasoning. Requires `ddt ai` to be configured.",
97
+ false
98
+ ).option(
99
+ "--color <mode>",
100
+ "Colorize severity output: always | never | auto. Default auto (color on TTY).",
101
+ "auto"
102
+ ).option(
103
+ "--type-safe",
104
+ "After the compare, run the TYPECHECK.1 impact analyzer. Exits with code 2 if the configured --break-on threshold is reached (default `error`). CI-friendly gate \u2014 pair with `--format json` for machine-readable output.",
105
+ false
106
+ ).option(
107
+ "--break-on <severity>",
108
+ "TYPECHECK.2 threshold for --type-safe: `error` (exit 2 on any error ripple, default) | `warning` (exit 2 on any error OR warning ripple, strict CI mode).",
109
+ "error"
110
+ ).option(
111
+ "--write-impact [path]",
112
+ "Write the TYPECHECK.1 impact analysis to a JSON file the VS Code extension can surface as squiggle-underlines. Default path: `.ddt/impact.json` resolved relative to CWD. Composes with `--type-safe` \u2014 the file is written before the gate decides exit code."
113
+ ).option(
114
+ "--no-history",
115
+ "Skip writing the compare-history audit record (AUDITCMP.1). Default: every compare run writes a record to `.ddt/history/compare/`, exportable via `ddt audit-log emit`."
116
+ );
117
+ addMappingFlags(cmd);
118
+ cmd.action(async (opts) => {
119
+ const nameMapping = await buildMappingFromOptions(opts);
120
+ const engine = new CompareEngine();
121
+ const source = new PacSource(String(opts.source), "source");
122
+ const target = new PacSource(String(opts.target), "target");
123
+ let slice;
124
+ if (opts.slice !== false) {
125
+ const srcPac = await pacNs.readPac(String(opts.source));
126
+ if (srcPac.manifest.slice) slice = compileSlice(srcPac.manifest.slice);
127
+ }
128
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
129
+ const result = await engine.compare(source, target, {
130
+ ignoreCase: !!opts.ignoreCase,
131
+ ...nameMapping ? { nameMapping } : {},
132
+ ...slice ? { sliceFilter: slice } : {}
133
+ });
134
+ if (opts.history !== false) {
135
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
136
+ try {
137
+ const histAssessment = safety.assess(result);
138
+ const changed = result.summary.added + result.summary.removed + result.summary.modified;
139
+ await writeCompareHistory(path.dirname(path.resolve(String(opts.source))), {
140
+ startedAt,
141
+ finishedAt,
142
+ durationMs: Date.parse(finishedAt) - Date.parse(startedAt),
143
+ outcome: histAssessment.blocked ? "BLOCKED" : changed === 0 ? "NO_CHANGES" : "CHANGES_FOUND",
144
+ source: { kind: result.source.kind, label: result.source.label },
145
+ target: { kind: result.target.kind, label: result.target.label },
146
+ summary: result.summary,
147
+ sliceApplied: Boolean(slice),
148
+ ...histAssessment.blocked && histAssessment.blockReason ? { blockReason: histAssessment.blockReason } : {}
149
+ });
150
+ } catch {
151
+ }
152
+ }
153
+ if (opts.reportHtml) {
154
+ const html = renderHtmlReport(result, {
155
+ title: `Compare ${result.source.label} \u2192 ${result.target.label}`,
156
+ safety: safety.assess(result)
157
+ });
158
+ const htmlPath = path.resolve(String(opts.reportHtml));
159
+ await fs.mkdir(path.dirname(htmlPath), { recursive: true });
160
+ await fs.writeFile(htmlPath, html, "utf8");
161
+ console.error(`Wrote ${htmlPath} (${html.length} bytes).`);
162
+ }
163
+ const fmt = opts.json ? "json" : String(opts.format ?? "summary").toLowerCase();
164
+ if (fmt === "json") {
165
+ console.log(JSON.stringify(result, null, 2));
166
+ return;
167
+ }
168
+ if (fmt === "markdown") {
169
+ const assessment2 = safety.assess(result);
170
+ console.log(renderCompareMarkdown(result, assessment2));
171
+ return;
172
+ }
173
+ const s = result.summary;
174
+ console.log(`Source: ${result.source.kind}:${result.source.label}`);
175
+ console.log(`Target: ${result.target.kind}:${result.target.label}`);
176
+ const mappingSummary = describeMapping(nameMapping);
177
+ if (mappingSummary) console.log(`Mapping: ${mappingSummary}`);
178
+ if (slice) {
179
+ const outside = result.outsideScope?.length ?? 0;
180
+ const refs = result.referenced?.length ?? 0;
181
+ console.log(
182
+ `Slice active: ${result.objects.length} owned \xB7 ${outside} outside scope (untouched) \xB7 ${refs} referenced`
183
+ );
184
+ }
185
+ console.log(`Summary: +${s.added} -${s.removed} ~${s.modified} =${s.unchanged}`);
186
+ for (const o of result.objects) {
187
+ if (o.kind === "unchanged") continue;
188
+ const glyph = o.kind === "added" ? "+" : o.kind === "removed" ? "-" : "~";
189
+ console.log(` ${glyph} ${o.identity.objectType} ${o.identity.fqn}`);
190
+ if (o.kind === "modified" && o.changes) {
191
+ for (const c of o.changes) {
192
+ console.log(
193
+ ` \u2022 ${c.path}: ${JSON.stringify(c.target)} \u2192 ${JSON.stringify(c.source)}`
194
+ );
195
+ }
196
+ }
197
+ }
198
+ const assessment = safety.assess(result);
199
+ const buckets = safety.groupByReversibility(assessment);
200
+ const total = buckets.unrecoverable.length + buckets.dataImpacting.length + buckets.reversible.length;
201
+ if (total > 0) {
202
+ const colorize = makeColorizer(resolveColorMode(opts.color));
203
+ console.log("");
204
+ console.log("Safety findings (grouped by reversibility):");
205
+ const block = safety.formatReversibilityBuckets(buckets);
206
+ const colored = colorize.applyToBlock(block);
207
+ for (const line of colored.split("\n")) console.log(" " + line);
208
+ if (assessment.blocked && assessment.blockReason) {
209
+ console.error(" " + colorize.unrecoverable("BLOCKED: " + assessment.blockReason));
210
+ }
211
+ }
212
+ if (opts.explain) {
213
+ console.log("");
214
+ console.log("AI explanation:");
215
+ try {
216
+ const userPrompt = buildExplainPrompt(result, assessment);
217
+ const reply = await ai.complete(
218
+ [
219
+ { role: "system", content: SYSTEM_PROMPT },
220
+ { role: "user", content: userPrompt }
221
+ ],
222
+ { feature: "compare.explain" }
223
+ );
224
+ for (const line of reply.text.split("\n")) console.log(" " + line);
225
+ } catch (err) {
226
+ console.error(" --explain failed: " + (err instanceof Error ? err.message : String(err)));
227
+ console.error(
228
+ " Run `ddt ai status` to verify your AI provider is configured (`ddt ai test` to send a probe)."
229
+ );
230
+ }
231
+ }
232
+ if (opts.writeImpact !== void 0) {
233
+ await writeImpactJson(
234
+ result,
235
+ opts.writeImpact === true ? void 0 : String(opts.writeImpact)
236
+ );
237
+ }
238
+ if (opts.typeSafe) {
239
+ const breakOnRaw = String(opts.breakOn ?? "error").toLowerCase();
240
+ if (breakOnRaw !== "error" && breakOnRaw !== "warning") {
241
+ throw new Error(
242
+ `--break-on must be 'error' or 'warning' (got '${opts.breakOn}'). 'error' fails on any error-severity ripple (default); 'warning' fails on any warning or error ripple (strict CI).`
243
+ );
244
+ }
245
+ const breakOn = breakOnRaw;
246
+ const impactFindings = typecheck.analyzeImpact(result);
247
+ const impactSummary = typecheck.summarizeImpact(impactFindings);
248
+ if (impactSummary.findingsCount > 0) {
249
+ console.log("");
250
+ console.log(
251
+ `Type-safety gate (--break-on ${breakOn}) \u2014 ${impactSummary.findingsCount} breaking change(s); ${impactSummary.errors} error ripple(s), ${impactSummary.warnings} warning ripple(s); ${impactSummary.affectedObjects.length} dependent object(s).`
252
+ );
253
+ for (const f of impactFindings) {
254
+ for (const r of f.ripples) {
255
+ const tag = r.severity === "error" ? "ERROR" : "WARN ";
256
+ console.log(
257
+ ` ${tag} ${r.fqn} \u2190 ${f.breakingChangeOn.fqn} (${f.breakingChangeOn.kind})`
258
+ );
259
+ }
260
+ }
261
+ const failOnError = impactSummary.errors > 0;
262
+ const failOnWarning = breakOn === "warning" && impactSummary.warnings > 0;
263
+ if (failOnError) {
264
+ console.error(
265
+ `Type-safety gate FAILED \u2014 ${impactSummary.errors} error ripple(s). Fix the dependents (drop / update / re-bind) before re-running compare, OR rerun with the change scoped out of the diff.`
266
+ );
267
+ process.exitCode = 2;
268
+ } else if (failOnWarning) {
269
+ console.error(
270
+ `Type-safety gate FAILED (strict --break-on warning) \u2014 ${impactSummary.warnings} warning ripple(s). Tighten the dependent SQL to accept the new type, or drop --break-on warning to allow.`
271
+ );
272
+ process.exitCode = 2;
273
+ }
274
+ } else {
275
+ console.log("Type-safety gate PASSED \u2014 no breaking-change ripples detected.");
276
+ }
277
+ }
278
+ });
279
+ attachRelatedOptions(cmd, [
280
+ "compare.ignoreCase",
281
+ "compare.ignoreComments",
282
+ "compare.ignoreFormattingDifferences",
283
+ "compare.ignoreClusterBy",
284
+ "compare.ignorePartitioning",
285
+ "compare.ignoreStorageLocation"
286
+ ]);
287
+ return cmd;
288
+ }
289
+ async function writeImpactJson(result, explicitPath) {
290
+ const findings = typecheck.analyzeImpact(result);
291
+ const summary = typecheck.summarizeImpact(findings);
292
+ const file = {
293
+ version: typecheck.IMPACT_FILE_VERSION,
294
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
295
+ source: `${result.source.kind}:${result.source.label}`,
296
+ target: `${result.target.kind}:${result.target.label}`,
297
+ findings,
298
+ summary
299
+ };
300
+ const outPath = explicitPath ? path.resolve(explicitPath) : path.resolve(".ddt", "impact.json");
301
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
302
+ await fs.writeFile(outPath, typecheck.serializeImpactFile(file));
303
+ console.log(
304
+ `Wrote ${outPath} \u2014 ${summary.findingsCount} finding(s), ${summary.errors} error / ${summary.warnings} warning ripple(s).`
305
+ );
306
+ }
307
+ var SYSTEM_PROMPT = `You are a senior Databricks Unity Catalog DBA reviewing a schema diff. Your job is to narrate the changes in plain English to a teammate who hasn't seen the underlying SQL. For each change, briefly say:
308
+ - what it is (e.g. "a new fact table is added")
309
+ - why a reasonable engineer might do this (the intent)
310
+ - what to watch out for (the risk, if any)
311
+
312
+ Be concrete; use the FQNs you're given. Keep it to 3-6 sentences per object. Don't repeat the raw diff back \u2014 explain it.`;
313
+ function buildExplainPrompt(result, assessment) {
314
+ const lines = [];
315
+ lines.push(`Source: ${result.source.label}`);
316
+ lines.push(`Target: ${result.target.label}`);
317
+ lines.push(
318
+ `Summary: +${result.summary.added} added, -${result.summary.removed} removed, ~${result.summary.modified} modified.`
319
+ );
320
+ lines.push(
321
+ `Safety: ${assessment.unrecoverable.length} unrecoverable, ${assessment.destructive.length} destructive, ${assessment.expensive.length} expensive, ${assessment.warnings.length} warnings.`
322
+ );
323
+ lines.push("");
324
+ lines.push("Changes (up to 40):");
325
+ const changed = result.objects.filter((o) => o.kind !== "unchanged").slice(0, 40);
326
+ for (const o of changed) {
327
+ lines.push(`- ${o.kind} ${o.identity.objectType} ${o.identity.fqn}`);
328
+ }
329
+ if (result.objects.filter((o) => o.kind !== "unchanged").length > 40) {
330
+ lines.push(
331
+ ` (\u2026 ${result.objects.filter((o) => o.kind !== "unchanged").length - 40} more truncated)`
332
+ );
333
+ }
334
+ lines.push("");
335
+ lines.push(
336
+ "Please narrate this diff in plain English. Group related changes together when it helps."
337
+ );
338
+ return lines.join("\n");
339
+ }
340
+ function renderCompareMarkdown(result, assessment) {
341
+ const lines = [];
342
+ lines.push(
343
+ `# Compare: ${result.source.kind}:${result.source.label} \u2192 ${result.target.kind}:${result.target.label}`
344
+ );
345
+ lines.push("");
346
+ lines.push(
347
+ `**Summary**: +${result.summary.added} added \xB7 -${result.summary.removed} removed \xB7 ~${result.summary.modified} modified \xB7 =${result.summary.unchanged} unchanged.`
348
+ );
349
+ lines.push("");
350
+ lines.push("**Safety**:");
351
+ lines.push(`- \u{1F6D1} Unrecoverable: ${assessment.unrecoverable.length}`);
352
+ lines.push(`- \u{1F525} Destructive: ${assessment.destructive.length}`);
353
+ lines.push(`- \u23F1 Expensive: ${assessment.expensive.length}`);
354
+ lines.push(`- \u26A0 Warnings: ${assessment.warnings.length}`);
355
+ if (assessment.blocked) {
356
+ lines.push("");
357
+ lines.push(
358
+ `> **BLOCKED**: ${assessment.blockReason ?? "safety classifier refuses to proceed"}`
359
+ );
360
+ }
361
+ lines.push("");
362
+ const changed = result.objects.filter((o) => o.kind !== "unchanged");
363
+ if (changed.length === 0) {
364
+ lines.push("_No object-level changes._");
365
+ return lines.join("\n");
366
+ }
367
+ lines.push("| Kind | Type | FQN |");
368
+ lines.push("| --- | --- | --- |");
369
+ for (const o of changed) {
370
+ lines.push(`| ${o.kind} | \`${o.identity.objectType}\` | \`${o.identity.fqn}\` |`);
371
+ }
372
+ return lines.join("\n");
373
+ }
374
+ export {
375
+ SYSTEM_PROMPT,
376
+ buildExplainPrompt,
377
+ compareCommand
378
+ };
379
+ //# sourceMappingURL=compare-P7JOV76O.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/compare.ts","../src/util/color.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport {\n CompareEngine,\n PacSource,\n ai,\n compileSlice,\n pac as pacNs,\n renderHtmlReport,\n safety,\n typecheck,\n writeCompareHistory,\n} from '@ddt-tools/core';\nimport { addMappingFlags, buildMappingFromOptions, describeMapping } from '../util/mapping.js';\nimport { attachRelatedOptions } from '../util/help-catalog.js';\nimport { makeColorizer, resolveColorMode } from '../util/color.js';\n\n/**\n * `ddt compare` — pac↔pac compare today. Project↔pac and project↔live require\n * the SQL parser pass (v0.3 polish) + a live driver session.\n *\n * Supports logical-name mapping (`--map`, `--map-file`) so two pacs whose\n * database / schema names differ can compare semantically. See\n * `docs/LOGICAL_NAME_MAPPING.md`.\n */\nexport function compareCommand(): Command {\n const cmd = new Command('compare');\n cmd\n .description('Compare two .ddtpac files (pac↔pac); project / live compare pending v0.3 polish.')\n .requiredOption('--source <path>', 'Source .ddtpac (the desired state).')\n .requiredOption('--target <path>', 'Target .ddtpac (the current state).')\n .option('--ignore-case', 'Compare object FQNs case-insensitively.', false)\n .option(\n '--json',\n 'Emit a JSON CompareResult instead of human-readable output. (Alias for --format json.)',\n false,\n )\n .option(\n '--format <fmt>',\n 'Output format: summary | json | markdown. Default summary.',\n 'summary',\n )\n .option('--report-html <path>', 'Also write a self-contained HTML compare-report to <path>.')\n .option(\n '--no-slice',\n \"Disable the source pac's Project Slice (if it has one). Default: a sliced pac is partitioned automatically.\",\n )\n .option(\n '--explain',\n 'After the diff, call the configured AI provider to narrate each change in plain English with reasoning. Requires `ddt ai` to be configured.',\n false,\n )\n .option(\n '--color <mode>',\n 'Colorize severity output: always | never | auto. Default auto (color on TTY).',\n 'auto',\n )\n .option(\n '--type-safe',\n 'After the compare, run the TYPECHECK.1 impact analyzer. Exits with code 2 if the configured --break-on threshold is reached (default `error`). CI-friendly gate — pair with `--format json` for machine-readable output.',\n false,\n )\n .option(\n '--break-on <severity>',\n 'TYPECHECK.2 threshold for --type-safe: `error` (exit 2 on any error ripple, default) | `warning` (exit 2 on any error OR warning ripple, strict CI mode).',\n 'error',\n )\n .option(\n '--write-impact [path]',\n 'Write the TYPECHECK.1 impact analysis to a JSON file the VS Code extension can surface as squiggle-underlines. Default path: `.ddt/impact.json` resolved relative to CWD. Composes with `--type-safe` — the file is written before the gate decides exit code.',\n )\n .option(\n '--no-history',\n 'Skip writing the compare-history audit record (AUDITCMP.1). Default: every compare run writes a record to `.ddt/history/compare/`, exportable via `ddt audit-log emit`.',\n );\n addMappingFlags(cmd);\n cmd.action(async (opts) => {\n const nameMapping = await buildMappingFromOptions(opts);\n const engine = new CompareEngine();\n const source = new PacSource(String(opts.source), 'source');\n const target = new PacSource(String(opts.target), 'target');\n\n // Auto-pull slice from the source pac's manifest unless --no-slice was set.\n let slice: ReturnType<typeof compileSlice> | undefined;\n if (opts.slice !== false) {\n const srcPac = await pacNs.readPac(String(opts.source));\n if (srcPac.manifest.slice) slice = compileSlice(srcPac.manifest.slice);\n }\n\n const startedAt = new Date().toISOString();\n const result = await engine.compare(source, target, {\n ignoreCase: !!opts.ignoreCase,\n ...(nameMapping ? { nameMapping } : {}),\n ...(slice ? { sliceFilter: slice } : {}),\n });\n\n // AUDITCMP.1 — compare operations leave the same audit trail deploys\n // do. Written before the format-specific early returns so every\n // output mode is covered. Best-effort: a failed write never breaks\n // the compare itself.\n if (opts.history !== false) {\n const finishedAt = new Date().toISOString();\n try {\n const histAssessment = safety.assess(result);\n const changed = result.summary.added + result.summary.removed + result.summary.modified;\n await writeCompareHistory(path.dirname(path.resolve(String(opts.source))), {\n startedAt,\n finishedAt,\n durationMs: Date.parse(finishedAt) - Date.parse(startedAt),\n outcome: histAssessment.blocked\n ? 'BLOCKED'\n : changed === 0\n ? 'NO_CHANGES'\n : 'CHANGES_FOUND',\n source: { kind: result.source.kind, label: result.source.label },\n target: { kind: result.target.kind, label: result.target.label },\n summary: result.summary,\n sliceApplied: Boolean(slice),\n ...(histAssessment.blocked && histAssessment.blockReason\n ? { blockReason: histAssessment.blockReason }\n : {}),\n });\n } catch {\n /* audit trail is best-effort; never fail the compare over it */\n }\n }\n\n if (opts.reportHtml) {\n const html = renderHtmlReport(result, {\n title: `Compare ${result.source.label} → ${result.target.label}`,\n safety: safety.assess(result),\n });\n const htmlPath = path.resolve(String(opts.reportHtml));\n await fs.mkdir(path.dirname(htmlPath), { recursive: true });\n await fs.writeFile(htmlPath, html, 'utf8');\n console.error(`Wrote ${htmlPath} (${html.length} bytes).`);\n }\n\n const fmt = opts.json ? 'json' : String(opts.format ?? 'summary').toLowerCase();\n if (fmt === 'json') {\n console.log(JSON.stringify(result, null, 2));\n return;\n }\n if (fmt === 'markdown') {\n const assessment = safety.assess(result);\n console.log(renderCompareMarkdown(result, assessment));\n return;\n }\n const s = result.summary;\n console.log(`Source: ${result.source.kind}:${result.source.label}`);\n console.log(`Target: ${result.target.kind}:${result.target.label}`);\n const mappingSummary = describeMapping(nameMapping);\n if (mappingSummary) console.log(`Mapping: ${mappingSummary}`);\n if (slice) {\n const outside = result.outsideScope?.length ?? 0;\n const refs = result.referenced?.length ?? 0;\n console.log(\n `Slice active: ${result.objects.length} owned · ${outside} outside scope (untouched) · ${refs} referenced`,\n );\n }\n console.log(`Summary: +${s.added} -${s.removed} ~${s.modified} =${s.unchanged}`);\n for (const o of result.objects) {\n if (o.kind === 'unchanged') continue;\n const glyph = o.kind === 'added' ? '+' : o.kind === 'removed' ? '-' : '~';\n console.log(` ${glyph} ${o.identity.objectType} ${o.identity.fqn}`);\n if (o.kind === 'modified' && o.changes) {\n for (const c of o.changes) {\n console.log(\n ` • ${c.path}: ${JSON.stringify(c.target)} → ${JSON.stringify(c.source)}`,\n );\n }\n }\n }\n\n // Reversibility-grouped safety findings — Unrecoverable first (highest\n // urgency), then Data-impacting, then Reversible. Empty buckets are\n // skipped, and an empty assessment is silent.\n const assessment = safety.assess(result);\n const buckets = safety.groupByReversibility(assessment);\n const total =\n buckets.unrecoverable.length + buckets.dataImpacting.length + buckets.reversible.length;\n if (total > 0) {\n const colorize = makeColorizer(resolveColorMode(opts.color));\n console.log('');\n console.log('Safety findings (grouped by reversibility):');\n const block = safety.formatReversibilityBuckets(buckets);\n const colored = colorize.applyToBlock(block);\n for (const line of colored.split('\\n')) console.log(' ' + line);\n if (assessment.blocked && assessment.blockReason) {\n console.error(' ' + colorize.unrecoverable('BLOCKED: ' + assessment.blockReason));\n }\n }\n\n // AI-narrated diff (--explain). Calls the configured provider with a\n // structured prompt containing the diff summary + per-object changes\n // + safety findings.\n if (opts.explain) {\n console.log('');\n console.log('AI explanation:');\n try {\n const userPrompt = buildExplainPrompt(result, assessment);\n const reply = await ai.complete(\n [\n { role: 'system', content: SYSTEM_PROMPT },\n { role: 'user', content: userPrompt },\n ],\n { feature: 'compare.explain' },\n );\n for (const line of reply.text.split('\\n')) console.log(' ' + line);\n } catch (err) {\n console.error(' --explain failed: ' + (err instanceof Error ? err.message : String(err)));\n console.error(\n ' Run `ddt ai status` to verify your AI provider is configured (`ddt ai test` to send a probe).',\n );\n }\n }\n\n // --write-impact: TYPECHECK.4 — write the impact analysis to a\n // JSON file the VS Code provider reads. Always runs the analyzer\n // (even when --type-safe is off) so the editor surface is\n // independent of the CI-gate flag. Composes with --type-safe.\n if (opts.writeImpact !== undefined) {\n await writeImpactJson(\n result,\n opts.writeImpact === true ? undefined : String(opts.writeImpact),\n );\n }\n\n // --type-safe: TYPECHECK.2 CI gate. Runs the TYPECHECK.1 impact\n // analyzer and exits with code 2 if any error-severity ripple\n // (column-drop or table-drop reaching a dependent object) is\n // detected. Stays silent on a clean diff so this composes well\n // with --format json (which already returned above).\n if (opts.typeSafe) {\n const breakOnRaw = String(opts.breakOn ?? 'error').toLowerCase();\n if (breakOnRaw !== 'error' && breakOnRaw !== 'warning') {\n throw new Error(\n `--break-on must be 'error' or 'warning' (got '${opts.breakOn}'). 'error' fails on any error-severity ripple (default); 'warning' fails on any warning or error ripple (strict CI).`,\n );\n }\n const breakOn = breakOnRaw as 'error' | 'warning';\n const impactFindings = typecheck.analyzeImpact(result);\n const impactSummary = typecheck.summarizeImpact(impactFindings);\n if (impactSummary.findingsCount > 0) {\n console.log('');\n console.log(\n `Type-safety gate (--break-on ${breakOn}) — ${impactSummary.findingsCount} breaking change(s); ${impactSummary.errors} error ripple(s), ${impactSummary.warnings} warning ripple(s); ${impactSummary.affectedObjects.length} dependent object(s).`,\n );\n for (const f of impactFindings) {\n for (const r of f.ripples) {\n const tag = r.severity === 'error' ? 'ERROR' : 'WARN ';\n console.log(\n ` ${tag} ${r.fqn} ← ${f.breakingChangeOn.fqn} (${f.breakingChangeOn.kind})`,\n );\n }\n }\n const failOnError = impactSummary.errors > 0;\n const failOnWarning = breakOn === 'warning' && impactSummary.warnings > 0;\n if (failOnError) {\n console.error(\n `Type-safety gate FAILED — ${impactSummary.errors} error ripple(s). Fix the dependents (drop / update / re-bind) before re-running compare, OR rerun with the change scoped out of the diff.`,\n );\n process.exitCode = 2;\n } else if (failOnWarning) {\n console.error(\n `Type-safety gate FAILED (strict --break-on warning) — ${impactSummary.warnings} warning ripple(s). Tighten the dependent SQL to accept the new type, or drop --break-on warning to allow.`,\n );\n process.exitCode = 2;\n }\n } else {\n console.log('Type-safety gate PASSED — no breaking-change ripples detected.');\n }\n }\n });\n attachRelatedOptions(cmd, [\n 'compare.ignoreCase',\n 'compare.ignoreComments',\n 'compare.ignoreFormattingDifferences',\n 'compare.ignoreClusterBy',\n 'compare.ignorePartitioning',\n 'compare.ignoreStorageLocation',\n ]);\n return cmd;\n}\n\n/**\n * TYPECHECK.4 — write the impact analysis JSON to disk so the VS Code\n * provider can surface it as squiggle-underlines. Defaults to\n * `<cwd>/.ddt/impact.json` (DDT compare today is pac↔pac so we don't\n * resolve a project root from the source arg — that wires in when the\n * project-source variant of compare ships).\n */\nasync function writeImpactJson(\n result: Parameters<typeof typecheck.analyzeImpact>[0],\n explicitPath: string | undefined,\n): Promise<void> {\n const findings = typecheck.analyzeImpact(result);\n const summary = typecheck.summarizeImpact(findings);\n const file: typecheck.ImpactFile = {\n version: typecheck.IMPACT_FILE_VERSION,\n generatedAt: new Date().toISOString(),\n source: `${result.source.kind}:${result.source.label}`,\n target: `${result.target.kind}:${result.target.label}`,\n findings,\n summary,\n };\n const outPath = explicitPath ? path.resolve(explicitPath) : path.resolve('.ddt', 'impact.json');\n await fs.mkdir(path.dirname(outPath), { recursive: true });\n await fs.writeFile(outPath, typecheck.serializeImpactFile(file));\n console.log(\n `Wrote ${outPath} — ${summary.findingsCount} finding(s), ${summary.errors} error / ${summary.warnings} warning ripple(s).`,\n );\n}\n\nexport const SYSTEM_PROMPT = `You are a senior Databricks Unity Catalog DBA reviewing a schema diff. Your job is to narrate the changes in plain English to a teammate who hasn't seen the underlying SQL. For each change, briefly say:\n - what it is (e.g. \"a new fact table is added\")\n - why a reasonable engineer might do this (the intent)\n - what to watch out for (the risk, if any)\n\nBe concrete; use the FQNs you're given. Keep it to 3-6 sentences per object. Don't repeat the raw diff back — explain it.`;\n\nexport function buildExplainPrompt(\n result: {\n source: { label: string };\n target: { label: string };\n objects: Array<{\n kind: string;\n identity: { fqn: string; objectType: string };\n changes?: unknown[];\n }>;\n summary: { added: number; removed: number; modified: number; unchanged: number };\n },\n assessment: {\n unrecoverable: unknown[];\n destructive: unknown[];\n expensive: unknown[];\n warnings: unknown[];\n },\n): string {\n const lines: string[] = [];\n lines.push(`Source: ${result.source.label}`);\n lines.push(`Target: ${result.target.label}`);\n lines.push(\n `Summary: +${result.summary.added} added, -${result.summary.removed} removed, ~${result.summary.modified} modified.`,\n );\n lines.push(\n `Safety: ${assessment.unrecoverable.length} unrecoverable, ${assessment.destructive.length} destructive, ${assessment.expensive.length} expensive, ${assessment.warnings.length} warnings.`,\n );\n lines.push('');\n lines.push('Changes (up to 40):');\n const changed = result.objects.filter((o) => o.kind !== 'unchanged').slice(0, 40);\n for (const o of changed) {\n lines.push(`- ${o.kind} ${o.identity.objectType} ${o.identity.fqn}`);\n }\n if (result.objects.filter((o) => o.kind !== 'unchanged').length > 40) {\n lines.push(\n ` (… ${result.objects.filter((o) => o.kind !== 'unchanged').length - 40} more truncated)`,\n );\n }\n lines.push('');\n lines.push(\n 'Please narrate this diff in plain English. Group related changes together when it helps.',\n );\n return lines.join('\\n');\n}\n\nfunction renderCompareMarkdown(\n result: {\n source: { kind: string; label: string };\n target: { kind: string; label: string };\n objects: Array<{ kind: string; identity: { fqn: string; objectType: string } }>;\n summary: { added: number; removed: number; modified: number; unchanged: number };\n },\n assessment: {\n unrecoverable: unknown[];\n destructive: unknown[];\n expensive: unknown[];\n warnings: unknown[];\n blocked?: boolean;\n blockReason?: string;\n },\n): string {\n const lines: string[] = [];\n lines.push(\n `# Compare: ${result.source.kind}:${result.source.label} → ${result.target.kind}:${result.target.label}`,\n );\n lines.push('');\n lines.push(\n `**Summary**: +${result.summary.added} added · -${result.summary.removed} removed · ~${result.summary.modified} modified · =${result.summary.unchanged} unchanged.`,\n );\n lines.push('');\n lines.push('**Safety**:');\n lines.push(`- 🛑 Unrecoverable: ${assessment.unrecoverable.length}`);\n lines.push(`- 🔥 Destructive: ${assessment.destructive.length}`);\n lines.push(`- ⏱ Expensive: ${assessment.expensive.length}`);\n lines.push(`- ⚠ Warnings: ${assessment.warnings.length}`);\n if (assessment.blocked) {\n lines.push('');\n lines.push(\n `> **BLOCKED**: ${assessment.blockReason ?? 'safety classifier refuses to proceed'}`,\n );\n }\n lines.push('');\n const changed = result.objects.filter((o) => o.kind !== 'unchanged');\n if (changed.length === 0) {\n lines.push('_No object-level changes._');\n return lines.join('\\n');\n }\n lines.push('| Kind | Type | FQN |');\n lines.push('| --- | --- | --- |');\n for (const o of changed) {\n lines.push(`| ${o.kind} | \\`${o.identity.objectType}\\` | \\`${o.identity.fqn}\\` |`);\n }\n return lines.join('\\n');\n}\n","/**\n * ANSI severity coloring for CLI output, DDT side. Paired with\n * `Snowflake/packages/cli/src/util/color.ts` byte-for-byte at the\n * API level.\n */\nexport type ColorMode = 'always' | 'never' | 'auto';\n\nconst ANSI = {\n reset: '\\x1b[0m',\n bold: '\\x1b[1m',\n red: '\\x1b[31m',\n yellow: '\\x1b[33m',\n cyan: '\\x1b[36m',\n green: '\\x1b[32m',\n gray: '\\x1b[90m',\n};\n\nexport function resolveColorMode(flag: string | undefined): boolean {\n const mode = (flag ?? 'auto').toLowerCase() as ColorMode;\n if (mode === 'always') return true;\n if (mode === 'never') return false;\n if (process.env.NO_COLOR) return false;\n return Boolean(process.stdout.isTTY);\n}\n\nexport interface Colorizer {\n unrecoverable(s: string): string;\n destructive(s: string): string;\n expensive(s: string): string;\n warning(s: string): string;\n safe(s: string): string;\n dim(s: string): string;\n applyToBlock(block: string): string;\n}\n\nexport function makeColorizer(enabled: boolean): Colorizer {\n if (!enabled) {\n const identity = (s: string): string => s;\n return {\n unrecoverable: identity,\n destructive: identity,\n expensive: identity,\n warning: identity,\n safe: identity,\n dim: identity,\n applyToBlock: identity,\n };\n }\n const wrap =\n (codes: string) =>\n (s: string): string =>\n `${codes}${s}${ANSI.reset}`;\n return {\n unrecoverable: wrap(ANSI.bold + ANSI.red),\n destructive: wrap(ANSI.red),\n expensive: wrap(ANSI.yellow),\n warning: wrap(ANSI.cyan),\n safe: wrap(ANSI.green),\n dim: wrap(ANSI.gray),\n applyToBlock(block: string): string {\n return block\n .split('\\n')\n .map((line) => {\n if (/UNRECOVERABLE/.test(line) || line.includes('🛑'))\n return ANSI.bold + ANSI.red + line + ANSI.reset;\n if (/DESTRUCTIVE/.test(line)) return ANSI.red + line + ANSI.reset;\n if (/EXPENSIVE/.test(line)) return ANSI.yellow + line + ANSI.reset;\n if (/WARNING/.test(line) || line.includes('⚠')) return ANSI.cyan + line + ANSI.reset;\n if (/\\bSAFE\\b|\\bOK\\b/.test(line) || line.includes('✓'))\n return ANSI.green + line + ANSI.reset;\n return line;\n })\n .join('\\n');\n },\n };\n}\n"],"mappings":"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","names":["assessment"]}
@@ -0,0 +1,219 @@
1
+ import "./chunk-DGUM43GV.js";
2
+
3
+ // src/commands/compare-profiles.ts
4
+ import { promises as fs } from "fs";
5
+ import { Command } from "commander";
6
+ import { catalog, compareProfiles, loadProject, pac, parseProjectModel } from "@ddt-tools/core";
7
+ function compareProfilesCommand() {
8
+ const cmd = new Command("compare-profiles");
9
+ cmd.description("Manage saved compare profiles (.ddt/compare-profiles.json).");
10
+ cmd.command("list").description("List every saved profile.").option("--root <path>", "Project root. Default cwd.", process.cwd()).option("--json", "Emit JSON instead of a human table.").action(async (opts) => {
11
+ const store = new compareProfiles.CompareProfilesStore({ root: String(opts.root) });
12
+ const all = await store.list();
13
+ if (opts.json) {
14
+ console.log(JSON.stringify(all, null, 2));
15
+ return;
16
+ }
17
+ if (all.length === 0) {
18
+ console.log("(no compare profiles saved yet)");
19
+ console.log(` store: ${store.path}`);
20
+ return;
21
+ }
22
+ console.log(`${all.length} compare profile(s):`);
23
+ for (const p of all) {
24
+ console.log(` ${p.id.padEnd(24)} ${p.name}`);
25
+ console.log(` source: ${p.source.kind}=${p.source.reference}`);
26
+ console.log(` target: ${p.target.kind}=${p.target.reference}`);
27
+ console.log(` mappings: ${p.mappings.length} updated: ${p.updatedAt}`);
28
+ }
29
+ });
30
+ cmd.command("show").description("Show one profile by id.").argument("<id>", "Profile id (from `compare-profiles list`).").option("--root <path>", "Project root. Default cwd.", process.cwd()).option("--json", "Emit JSON instead of a human table.").action(async (id, opts) => {
31
+ const store = new compareProfiles.CompareProfilesStore({ root: String(opts.root) });
32
+ const p = await store.get(String(id));
33
+ if (!p) {
34
+ console.error(
35
+ `No profile with id "${id}". Run \`ddt compare-profiles list\` to enumerate.`
36
+ );
37
+ process.exitCode = 1;
38
+ return;
39
+ }
40
+ if (opts.json) {
41
+ console.log(JSON.stringify(p, null, 2));
42
+ return;
43
+ }
44
+ console.log(`Profile: ${p.name} (${p.id})`);
45
+ if (p.description) console.log(` ${p.description}`);
46
+ console.log(
47
+ ` source: ${p.source.kind}=${p.source.reference}${p.source.catalog ? ` cat=${p.source.catalog}` : ""}${p.source.schema ? ` schema=${p.source.schema}` : ""}`
48
+ );
49
+ console.log(
50
+ ` target: ${p.target.kind}=${p.target.reference}${p.target.catalog ? ` cat=${p.target.catalog}` : ""}${p.target.schema ? ` schema=${p.target.schema}` : ""}`
51
+ );
52
+ console.log(` case-sensitive: ${p.caseSensitive ? "yes" : "no"}`);
53
+ console.log(` rewrite-inside-strings: ${p.rewriteInsideStrings ? "yes" : "no"}`);
54
+ console.log(` updatedAt: ${p.updatedAt}`);
55
+ if (p.mappings.length === 0) {
56
+ console.log(" mappings: (none \u2014 identity)");
57
+ } else {
58
+ console.log(` mappings (${p.mappings.length}):`);
59
+ for (const m of p.mappings) console.log(` ${m.source} => ${m.target}`);
60
+ }
61
+ });
62
+ cmd.command("save").description("Upsert a profile from a JSON file or stdin.").option("--root <path>", "Project root. Default cwd.", process.cwd()).option("--json-file <path>", 'Path to a JSON file containing one profile. Use "-" for stdin.').action(async (opts) => {
63
+ if (!opts.jsonFile) {
64
+ throw new Error('--json-file is required (use a path or "-" for stdin).');
65
+ }
66
+ const raw = String(opts.jsonFile) === "-" ? await readStdin() : await fs.readFile(String(opts.jsonFile), "utf8");
67
+ const parsed = JSON.parse(raw);
68
+ if (!parsed?.id || !parsed?.name || !parsed?.source || !parsed?.target) {
69
+ throw new Error(
70
+ "Profile JSON must contain at minimum: id, name, source, target. mappings defaults to []."
71
+ );
72
+ }
73
+ const store = new compareProfiles.CompareProfilesStore({ root: String(opts.root) });
74
+ const stamped = await store.upsert({ ...parsed, mappings: parsed.mappings ?? [] });
75
+ console.log(`Saved profile "${stamped.name}" (${stamped.id})`);
76
+ console.log(` store: ${store.path}`);
77
+ });
78
+ cmd.command("remove").description("Delete one profile by id.").argument("<id>", "Profile id (from `compare-profiles list`).").option("--root <path>", "Project root. Default cwd.", process.cwd()).action(async (id, opts) => {
79
+ const store = new compareProfiles.CompareProfilesStore({ root: String(opts.root) });
80
+ const removed = await store.remove(String(id));
81
+ if (removed) {
82
+ console.log(`Removed profile "${id}".`);
83
+ } else {
84
+ console.warn(`No profile with id "${id}" \u2014 nothing to remove.`);
85
+ }
86
+ });
87
+ cmd.command("preview").description(
88
+ "Preview which FQNs match between the profile's source and target. Local-only \u2014 no workspace round-trip."
89
+ ).argument("<id>", "Profile id (from `compare-profiles list`).").option("--root <path>", "Project root. Default cwd.", process.cwd()).option(
90
+ "--examples <n>",
91
+ "Max example FQNs to show per bucket in human output. Default 5.",
92
+ (v) => parseInt(v, 10),
93
+ 5
94
+ ).option("--json", "Emit the full PreviewSummary as JSON.").action(async (id, opts) => {
95
+ const store = new compareProfiles.CompareProfilesStore({ root: String(opts.root) });
96
+ const profile = await store.get(String(id));
97
+ if (!profile) {
98
+ console.error(
99
+ `No profile with id "${id}". Run \`ddt compare-profiles list\` to enumerate.`
100
+ );
101
+ process.exitCode = 1;
102
+ return;
103
+ }
104
+ const root = String(opts.root);
105
+ const source = await resolveEndpointFqns(profile.source, root);
106
+ const target = await resolveEndpointFqns(profile.target, root);
107
+ const summary = compareProfiles.previewMatch({
108
+ source: source.fqns,
109
+ target: target.fqns,
110
+ mappings: profile.mappings,
111
+ ...profile.caseSensitive ? { caseSensitive: true } : {}
112
+ });
113
+ if (opts.json) {
114
+ console.log(
115
+ JSON.stringify(
116
+ {
117
+ profile: { id: profile.id, name: profile.name },
118
+ source: {
119
+ kind: profile.source.kind,
120
+ reference: profile.source.reference,
121
+ count: source.fqns.length
122
+ },
123
+ target: {
124
+ kind: profile.target.kind,
125
+ reference: profile.target.reference,
126
+ count: target.fqns.length
127
+ },
128
+ summary
129
+ },
130
+ null,
131
+ 2
132
+ )
133
+ );
134
+ return;
135
+ }
136
+ console.log(`Preview: ${profile.name} (${profile.id})`);
137
+ console.log(
138
+ ` source (${profile.source.kind}=${profile.source.reference}): ${source.fqns.length} object(s)${source.note ? ` \u2014 ${source.note}` : ""}`
139
+ );
140
+ console.log(
141
+ ` target (${profile.target.kind}=${profile.target.reference}): ${target.fqns.length} object(s)${target.note ? ` \u2014 ${target.note}` : ""}`
142
+ );
143
+ console.log("");
144
+ console.log(` matched: ${summary.matchedCount}`);
145
+ console.log(` source-only: ${summary.sourceOnlyCount}`);
146
+ console.log(` target-only: ${summary.targetOnlyCount}`);
147
+ const exN = Math.max(0, Number(opts.examples));
148
+ printBucket("matched", summary.matched, exN);
149
+ printBucket("source-only", summary.sourceOnly, exN);
150
+ printBucket("target-only", summary.targetOnly, exN);
151
+ if (summary.matchedCount === 0 && (summary.sourceOnlyCount > 0 || summary.targetOnlyCount > 0)) {
152
+ console.warn("No FQNs matched \u2014 check the profile's scope and mapping rules.");
153
+ }
154
+ });
155
+ return cmd;
156
+ }
157
+ async function resolveEndpointFqns(endpoint, root) {
158
+ if (endpoint.kind === "connection") {
159
+ const cache = new catalog.CatalogCache({ root, connection: endpoint.reference });
160
+ const snapshot = await cache.get();
161
+ if (snapshot.catalogs.length === 0) {
162
+ return {
163
+ fqns: [],
164
+ note: `empty catalog cache at ${cache.path} \u2014 run \`ddt catalog refresh --connection ${endpoint.reference}\` first`
165
+ };
166
+ }
167
+ return { fqns: fqnsFromSnapshot(snapshot, endpoint.catalog, endpoint.schema) };
168
+ }
169
+ if (endpoint.kind === "pac") {
170
+ const contents = await pac.readPac(endpoint.reference);
171
+ return { fqns: fqnsFromObjects(contents.model, endpoint.catalog, endpoint.schema) };
172
+ }
173
+ const loaded = await loadProject(endpoint.reference);
174
+ const model = await parseProjectModel(loaded);
175
+ return { fqns: fqnsFromObjects(model, endpoint.catalog, endpoint.schema) };
176
+ }
177
+ function fqnsFromSnapshot(snapshot, catalogScope, schemaScope) {
178
+ const sameId = (a, b) => !!a && !!b && a.toUpperCase() === b.toUpperCase();
179
+ const out = [];
180
+ for (const cat of snapshot.catalogs) {
181
+ if (catalogScope && !sameId(cat.catalog, catalogScope)) continue;
182
+ for (const sc of cat.schemas) {
183
+ if (schemaScope && !sameId(sc.schema, schemaScope)) continue;
184
+ for (const obj of sc.objects) {
185
+ out.push({ database: obj.catalog, schema: obj.schema, name: obj.name });
186
+ }
187
+ }
188
+ }
189
+ return out;
190
+ }
191
+ function fqnsFromObjects(model, catalogScope, schemaScope) {
192
+ const sameId = (a, b) => !!a && !!b && a.toUpperCase() === b.toUpperCase();
193
+ const out = [];
194
+ for (const obj of model) {
195
+ if (catalogScope && !sameId(obj.fqn.database, catalogScope)) continue;
196
+ if (schemaScope && !sameId(obj.fqn.schema, schemaScope)) continue;
197
+ out.push(obj.fqn);
198
+ }
199
+ return out;
200
+ }
201
+ function printBucket(label, items, exampleN) {
202
+ if (items.length === 0) return;
203
+ console.log("");
204
+ console.log(
205
+ ` ${label} examples (showing ${Math.min(exampleN, items.length)} of ${items.length}):`
206
+ );
207
+ for (const fqn of items.slice(0, exampleN)) console.log(` ${fqn}`);
208
+ }
209
+ async function readStdin() {
210
+ const chunks = [];
211
+ for await (const chunk of process.stdin) {
212
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
213
+ }
214
+ return Buffer.concat(chunks).toString("utf8");
215
+ }
216
+ export {
217
+ compareProfilesCommand
218
+ };
219
+ //# sourceMappingURL=compare-profiles-H33CXZPD.js.map