@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,200 @@
1
+ import "./chunk-DGUM43GV.js";
2
+
3
+ // src/commands/perf.ts
4
+ import { promises as fs } from "fs";
5
+ import path from "path";
6
+ import { Command } from "commander";
7
+ import { loadProject, parseProjectModel } from "@ddt-tools/core/project";
8
+ import {
9
+ findMaterializedViewCandidates,
10
+ findUnusedObjects,
11
+ recommendWarehouseSize,
12
+ renderMvCandidatesMarkdown,
13
+ renderUnusedObjectsMarkdown,
14
+ renderWarehouseSizingMarkdown
15
+ } from "@ddt-tools/core/perf";
16
+ var COLD_START_BUDGET_MS = 200;
17
+ function perfCommand() {
18
+ const cmd = new Command("perf");
19
+ cmd.description("CLI performance diagnostics (cold-start audit, hot-path profiling).");
20
+ const coldStart = new Command("cold-start");
21
+ coldStart.description("Measure CLI cold-start latency and compare against the 200ms budget.").option("--json", "Emit JSON instead of human-readable output.", false).option(
22
+ "--budget <ms>",
23
+ "Override the pass/fail threshold in ms.",
24
+ String(COLD_START_BUDGET_MS)
25
+ ).action((opts) => {
26
+ const startNs = globalThis.__DDT_START_NS__;
27
+ const nowNs = process.hrtime.bigint();
28
+ const budgetMs = Number(opts.budget ?? COLD_START_BUDGET_MS);
29
+ let elapsedMs = null;
30
+ if (startNs !== void 0) {
31
+ elapsedMs = Number(nowNs - startNs) / 1e6;
32
+ }
33
+ const pass = elapsedMs !== null && elapsedMs <= budgetMs;
34
+ const note = elapsedMs === null ? "Start timestamp not recorded \u2014 add `globalThis.__DDT_START_NS__ = process.hrtime.bigint()` to the CLI entry point." : pass ? `\u2713 ${elapsedMs.toFixed(1)} ms \u2264 ${budgetMs} ms budget` : `\u2717 ${elapsedMs.toFixed(1)} ms > ${budgetMs} ms budget \u2014 see docs/CONTRIBUTING.md for lazy-load patterns`;
35
+ if (opts.json) {
36
+ process.stdout.write(JSON.stringify({ elapsedMs, budgetMs, pass, note }, null, 2) + "\n");
37
+ } else {
38
+ console.log(`ddt cold-start: ${note}`);
39
+ if (elapsedMs !== null) {
40
+ const bar = "\u2588".repeat(Math.min(40, Math.round(elapsedMs / budgetMs * 40)));
41
+ const pct = (elapsedMs / budgetMs * 100).toFixed(0);
42
+ console.log(` ${bar} ${pct}% of ${budgetMs}ms budget`);
43
+ }
44
+ }
45
+ process.exitCode = pass ? 0 : 1;
46
+ });
47
+ cmd.addCommand(coldStart);
48
+ const unused = new Command("unused");
49
+ unused.description(
50
+ "Find project objects unreferenced for \u2265 --threshold-days in a query-history snapshot (PERF.6). Offline \u2014 feeds a JSON array of QueryHistoryEntry via --history-file."
51
+ ).requiredOption("-p, --project <path>", "Path to the .ddtproj file.").requiredOption(
52
+ "--history-file <path>",
53
+ "Path to a JSON file containing a QueryHistoryEntry[] (e.g. saved from `ddt history --last 1000 --format json`)."
54
+ ).option("--threshold-days <n>", "Days-of-inactivity cutoff (default 30).", "30").option(
55
+ "--reference-date <iso>",
56
+ 'ISO-8601 timestamp used as "now" (default: most recent entry).'
57
+ ).option("--format <fmt>", "Output format: text | json. Default text.", "text").option("-o, --out <path>", "Write report to a file instead of stdout.").action(async (opts) => {
58
+ const loaded = await loadProject(String(opts.project));
59
+ const model = await parseProjectModel(loaded);
60
+ const testable = /* @__PURE__ */ new Set([
61
+ "TABLE",
62
+ "MANAGED_TABLE",
63
+ "EXTERNAL_TABLE",
64
+ "VIEW",
65
+ "MATERIALIZED_VIEW",
66
+ "STREAMING_TABLE"
67
+ ]);
68
+ const fqns = [];
69
+ for (const obj of model) {
70
+ if (!testable.has(obj.objectType)) continue;
71
+ const cat = obj.fqn.database;
72
+ const sc = obj.fqn.schema;
73
+ if (!cat || !sc) continue;
74
+ fqns.push(`${cat}.${sc}.${obj.fqn.name}`);
75
+ }
76
+ const historyAbs = path.resolve(String(opts.historyFile));
77
+ const historyRaw = await fs.readFile(historyAbs, "utf8");
78
+ const entries = JSON.parse(historyRaw);
79
+ if (!Array.isArray(entries)) {
80
+ throw new Error(
81
+ `--history-file ${historyAbs} did not parse to an array of QueryHistoryEntry.`
82
+ );
83
+ }
84
+ const thresholdDays = Number(opts.thresholdDays ?? "30");
85
+ const report = findUnusedObjects(entries, fqns, {
86
+ thresholdDays,
87
+ referenceDate: opts.referenceDate ? String(opts.referenceDate) : void 0
88
+ });
89
+ const output = String(opts.format).toLowerCase() === "json" ? JSON.stringify(report, null, 2) : renderUnusedObjectsMarkdown(report);
90
+ if (opts.out) {
91
+ const out = path.resolve(String(opts.out));
92
+ await fs.mkdir(path.dirname(out), { recursive: true });
93
+ await fs.writeFile(out, output, "utf8");
94
+ console.error(
95
+ `Wrote ${out} \u2014 ${report.unusedCount}/${report.totalObjects} object(s) unreferenced \u2265 ${thresholdDays}d.`
96
+ );
97
+ } else {
98
+ process.stdout.write(output + "\n");
99
+ }
100
+ });
101
+ cmd.addCommand(unused);
102
+ const mvCandidates = new Command("mv-candidates");
103
+ mvCandidates.description(
104
+ "Identify recurring SELECT shapes that may benefit from caching as a materialized view (PERF.4). Offline \u2014 feeds a JSON array of QueryHistoryEntry via --history-file. Suggests CREATE MATERIALIZED VIEW DDL."
105
+ ).requiredOption(
106
+ "--history-file <path>",
107
+ "Path to a JSON file containing a QueryHistoryEntry[] (e.g. saved from `ddt history --last 1000 --format json`)."
108
+ ).option("--threshold-runs <n>", "Min run count to qualify as a candidate (default 5).", "5").option(
109
+ "--threshold-total-ms <n>",
110
+ "Min sum of durationMs across runs to qualify (default 60000).",
111
+ "60000"
112
+ ).option("--top-n <n>", "Cap on candidates returned (default 25).", "25").option("--mv-schema <name>", "Schema to prefix on the suggested MV name (e.g. `analytics`).").option("--format <fmt>", "Output format: text | json. Default text.", "text").option("-o, --out <path>", "Write report to a file instead of stdout.").action(async (opts) => {
113
+ const historyAbs = path.resolve(String(opts.historyFile));
114
+ const historyRaw = await fs.readFile(historyAbs, "utf8");
115
+ const entries = JSON.parse(historyRaw);
116
+ if (!Array.isArray(entries)) {
117
+ throw new Error(
118
+ `--history-file ${historyAbs} did not parse to an array of QueryHistoryEntry.`
119
+ );
120
+ }
121
+ const report = findMaterializedViewCandidates(entries, {
122
+ thresholdRuns: Number(opts.thresholdRuns ?? "5"),
123
+ thresholdTotalMs: Number(opts.thresholdTotalMs ?? "60000"),
124
+ topN: Number(opts.topN ?? "25"),
125
+ mvSchema: opts.mvSchema ? String(opts.mvSchema) : void 0
126
+ });
127
+ const output = String(opts.format).toLowerCase() === "json" ? JSON.stringify(report, null, 2) : renderMvCandidatesMarkdown(report);
128
+ if (opts.out) {
129
+ const out = path.resolve(String(opts.out));
130
+ await fs.mkdir(path.dirname(out), { recursive: true });
131
+ await fs.writeFile(out, output, "utf8");
132
+ console.error(
133
+ `Wrote ${out} \u2014 ${report.candidates.length} MV candidate(s) (scanned ${report.entryCount} entries).`
134
+ );
135
+ } else {
136
+ process.stdout.write(output + "\n");
137
+ }
138
+ });
139
+ cmd.addCommand(mvCandidates);
140
+ const warehouseSize = new Command("warehouse-size");
141
+ warehouseSize.description(
142
+ "Per-SQL-warehouse sizing recommendation from a query-history snapshot (PERF.5). Offline \u2014 feeds a JSON QueryHistoryEntry[] via --history-file. Suggests size-up / size-down / multi-cluster / keep."
143
+ ).requiredOption(
144
+ "--history-file <path>",
145
+ "Path to a JSON file containing a QueryHistoryEntry[]."
146
+ ).option(
147
+ "--slow-query-ms <n>",
148
+ 'Threshold above which a query counts as "slow" (default 30000).',
149
+ "30000"
150
+ ).option(
151
+ "--size-up-fraction <p>",
152
+ "Fraction of slow queries that triggers size-up (default 0.2).",
153
+ "0.2"
154
+ ).option(
155
+ "--size-down-mean-ms <n>",
156
+ "Mean-duration cap below which size-down may trigger (default 1000).",
157
+ "1000"
158
+ ).option(
159
+ "--low-use-runs <n>",
160
+ "Run-count cap below which size-down may trigger (default 20).",
161
+ "20"
162
+ ).option(
163
+ "--multi-cluster-runs <n>",
164
+ "Run-count floor above which size-up upgrades to multi-cluster (default 500).",
165
+ "500"
166
+ ).option("--format <fmt>", "Output format: text | json. Default text.", "text").option("-o, --out <path>", "Write report to a file instead of stdout.").action(async (opts) => {
167
+ const historyAbs = path.resolve(String(opts.historyFile));
168
+ const historyRaw = await fs.readFile(historyAbs, "utf8");
169
+ const entries = JSON.parse(historyRaw);
170
+ if (!Array.isArray(entries)) {
171
+ throw new Error(
172
+ `--history-file ${historyAbs} did not parse to an array of QueryHistoryEntry.`
173
+ );
174
+ }
175
+ const report = recommendWarehouseSize(entries, {
176
+ slowQueryThresholdMs: Number(opts.slowQueryMs ?? "30000"),
177
+ sizeUpSlowFraction: Number(opts.sizeUpFraction ?? "0.2"),
178
+ sizeDownMeanMaxMs: Number(opts.sizeDownMeanMs ?? "1000"),
179
+ lowUseRunThreshold: Number(opts.lowUseRuns ?? "20"),
180
+ multiClusterRunThreshold: Number(opts.multiClusterRuns ?? "500")
181
+ });
182
+ const output = String(opts.format).toLowerCase() === "json" ? JSON.stringify(report, null, 2) : renderWarehouseSizingMarkdown(report);
183
+ if (opts.out) {
184
+ const out = path.resolve(String(opts.out));
185
+ await fs.mkdir(path.dirname(out), { recursive: true });
186
+ await fs.writeFile(out, output, "utf8");
187
+ console.error(
188
+ `Wrote ${out} \u2014 ${report.recommendations.length} warehouse(s) analysed (scanned ${report.entryCount} entries).`
189
+ );
190
+ } else {
191
+ process.stdout.write(output + "\n");
192
+ }
193
+ });
194
+ cmd.addCommand(warehouseSize);
195
+ return cmd;
196
+ }
197
+ export {
198
+ perfCommand
199
+ };
200
+ //# sourceMappingURL=perf-UULZSREY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/perf.ts"],"sourcesContent":["/**\n * `ddt perf` — CLI performance diagnostics + recommendations.\n *\n * Subcommands:\n * - `cold-start` (RES.5): CLI startup latency vs the 200ms budget.\n * - `unused` (PERF.6): given a project model + query-history snapshot,\n * identify objects unreferenced for ≥ N days.\n *\n * Mirrors `Snowflake/packages/cli/src/commands/perf.ts`.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\n// Subpath import, NOT the `@ddt-tools/core` barrel — keeps `ddt perf cold-start`\n// itself off the all-modules import path it exists to measure (RH3.3).\nimport { loadProject, parseProjectModel } from '@ddt-tools/core/project';\nimport {\n findMaterializedViewCandidates,\n findUnusedObjects,\n recommendWarehouseSize,\n renderMvCandidatesMarkdown,\n renderUnusedObjectsMarkdown,\n renderWarehouseSizingMarkdown,\n} from '@ddt-tools/core/perf';\nimport type { QueryHistoryEntry } from '@ddt-tools/core/queryHistory';\n\nconst COLD_START_BUDGET_MS = 200;\n\nexport function perfCommand(): Command {\n const cmd = new Command('perf');\n cmd.description('CLI performance diagnostics (cold-start audit, hot-path profiling).');\n\n const coldStart = new Command('cold-start');\n coldStart\n .description('Measure CLI cold-start latency and compare against the 200ms budget.')\n .option('--json', 'Emit JSON instead of human-readable output.', false)\n .option(\n '--budget <ms>',\n 'Override the pass/fail threshold in ms.',\n String(COLD_START_BUDGET_MS),\n )\n .action((opts) => {\n const startNs: bigint | undefined = (globalThis as { __DDT_START_NS__?: bigint })\n .__DDT_START_NS__;\n const nowNs = process.hrtime.bigint();\n const budgetMs = Number(opts.budget ?? COLD_START_BUDGET_MS);\n\n let elapsedMs: number | null = null;\n if (startNs !== undefined) {\n elapsedMs = Number(nowNs - startNs) / 1e6;\n }\n\n const pass = elapsedMs !== null && elapsedMs <= budgetMs;\n const note =\n elapsedMs === null\n ? 'Start timestamp not recorded — add `globalThis.__DDT_START_NS__ = process.hrtime.bigint()` to the CLI entry point.'\n : pass\n ? `✓ ${elapsedMs.toFixed(1)} ms ≤ ${budgetMs} ms budget`\n : `✗ ${elapsedMs.toFixed(1)} ms > ${budgetMs} ms budget — see docs/CONTRIBUTING.md for lazy-load patterns`;\n\n if (opts.json) {\n process.stdout.write(JSON.stringify({ elapsedMs, budgetMs, pass, note }, null, 2) + '\\n');\n } else {\n console.log(`ddt cold-start: ${note}`);\n if (elapsedMs !== null) {\n const bar = '█'.repeat(Math.min(40, Math.round((elapsedMs / budgetMs) * 40)));\n const pct = ((elapsedMs / budgetMs) * 100).toFixed(0);\n console.log(` ${bar} ${pct}% of ${budgetMs}ms budget`);\n }\n }\n process.exitCode = pass ? 0 : 1;\n });\n\n cmd.addCommand(coldStart);\n\n const unused = new Command('unused');\n unused\n .description(\n 'Find project objects unreferenced for ≥ --threshold-days in a query-history snapshot (PERF.6). ' +\n 'Offline — feeds a JSON array of QueryHistoryEntry via --history-file.',\n )\n .requiredOption('-p, --project <path>', 'Path to the .ddtproj file.')\n .requiredOption(\n '--history-file <path>',\n 'Path to a JSON file containing a QueryHistoryEntry[] (e.g. saved from `ddt history --last 1000 --format json`).',\n )\n .option('--threshold-days <n>', 'Days-of-inactivity cutoff (default 30).', '30')\n .option(\n '--reference-date <iso>',\n 'ISO-8601 timestamp used as \"now\" (default: most recent entry).',\n )\n .option('--format <fmt>', 'Output format: text | json. Default text.', 'text')\n .option('-o, --out <path>', 'Write report to a file instead of stdout.')\n .action(async (opts) => {\n const loaded = await loadProject(String(opts.project));\n const model = await parseProjectModel(loaded);\n const testable = new Set<string>([\n 'TABLE',\n 'MANAGED_TABLE',\n 'EXTERNAL_TABLE',\n 'VIEW',\n 'MATERIALIZED_VIEW',\n 'STREAMING_TABLE',\n ]);\n const fqns: string[] = [];\n for (const obj of model) {\n if (!testable.has(obj.objectType)) continue;\n const cat = obj.fqn.database;\n const sc = obj.fqn.schema;\n if (!cat || !sc) continue;\n fqns.push(`${cat}.${sc}.${obj.fqn.name}`);\n }\n const historyAbs = path.resolve(String(opts.historyFile));\n const historyRaw = await fs.readFile(historyAbs, 'utf8');\n const entries = JSON.parse(historyRaw) as QueryHistoryEntry[];\n if (!Array.isArray(entries)) {\n throw new Error(\n `--history-file ${historyAbs} did not parse to an array of QueryHistoryEntry.`,\n );\n }\n const thresholdDays = Number(opts.thresholdDays ?? '30');\n const report = findUnusedObjects(entries, fqns, {\n thresholdDays,\n referenceDate: opts.referenceDate ? String(opts.referenceDate) : undefined,\n });\n const output =\n String(opts.format).toLowerCase() === 'json'\n ? JSON.stringify(report, null, 2)\n : renderUnusedObjectsMarkdown(report);\n if (opts.out) {\n const out = path.resolve(String(opts.out));\n await fs.mkdir(path.dirname(out), { recursive: true });\n await fs.writeFile(out, output, 'utf8');\n console.error(\n `Wrote ${out} — ${report.unusedCount}/${report.totalObjects} object(s) unreferenced ≥ ${thresholdDays}d.`,\n );\n } else {\n process.stdout.write(output + '\\n');\n }\n });\n cmd.addCommand(unused);\n\n const mvCandidates = new Command('mv-candidates');\n mvCandidates\n .description(\n 'Identify recurring SELECT shapes that may benefit from caching as a materialized view (PERF.4). ' +\n 'Offline — feeds a JSON array of QueryHistoryEntry via --history-file. Suggests CREATE MATERIALIZED VIEW DDL.',\n )\n .requiredOption(\n '--history-file <path>',\n 'Path to a JSON file containing a QueryHistoryEntry[] (e.g. saved from `ddt history --last 1000 --format json`).',\n )\n .option('--threshold-runs <n>', 'Min run count to qualify as a candidate (default 5).', '5')\n .option(\n '--threshold-total-ms <n>',\n 'Min sum of durationMs across runs to qualify (default 60000).',\n '60000',\n )\n .option('--top-n <n>', 'Cap on candidates returned (default 25).', '25')\n .option('--mv-schema <name>', 'Schema to prefix on the suggested MV name (e.g. `analytics`).')\n .option('--format <fmt>', 'Output format: text | json. Default text.', 'text')\n .option('-o, --out <path>', 'Write report to a file instead of stdout.')\n .action(async (opts) => {\n const historyAbs = path.resolve(String(opts.historyFile));\n const historyRaw = await fs.readFile(historyAbs, 'utf8');\n const entries = JSON.parse(historyRaw) as QueryHistoryEntry[];\n if (!Array.isArray(entries)) {\n throw new Error(\n `--history-file ${historyAbs} did not parse to an array of QueryHistoryEntry.`,\n );\n }\n const report = findMaterializedViewCandidates(entries, {\n thresholdRuns: Number(opts.thresholdRuns ?? '5'),\n thresholdTotalMs: Number(opts.thresholdTotalMs ?? '60000'),\n topN: Number(opts.topN ?? '25'),\n mvSchema: opts.mvSchema ? String(opts.mvSchema) : undefined,\n });\n const output =\n String(opts.format).toLowerCase() === 'json'\n ? JSON.stringify(report, null, 2)\n : renderMvCandidatesMarkdown(report);\n if (opts.out) {\n const out = path.resolve(String(opts.out));\n await fs.mkdir(path.dirname(out), { recursive: true });\n await fs.writeFile(out, output, 'utf8');\n console.error(\n `Wrote ${out} — ${report.candidates.length} MV candidate(s) (scanned ${report.entryCount} entries).`,\n );\n } else {\n process.stdout.write(output + '\\n');\n }\n });\n cmd.addCommand(mvCandidates);\n\n const warehouseSize = new Command('warehouse-size');\n warehouseSize\n .description(\n 'Per-SQL-warehouse sizing recommendation from a query-history snapshot (PERF.5). ' +\n 'Offline — feeds a JSON QueryHistoryEntry[] via --history-file. Suggests size-up / size-down / multi-cluster / keep.',\n )\n .requiredOption(\n '--history-file <path>',\n 'Path to a JSON file containing a QueryHistoryEntry[].',\n )\n .option(\n '--slow-query-ms <n>',\n 'Threshold above which a query counts as \"slow\" (default 30000).',\n '30000',\n )\n .option(\n '--size-up-fraction <p>',\n 'Fraction of slow queries that triggers size-up (default 0.2).',\n '0.2',\n )\n .option(\n '--size-down-mean-ms <n>',\n 'Mean-duration cap below which size-down may trigger (default 1000).',\n '1000',\n )\n .option(\n '--low-use-runs <n>',\n 'Run-count cap below which size-down may trigger (default 20).',\n '20',\n )\n .option(\n '--multi-cluster-runs <n>',\n 'Run-count floor above which size-up upgrades to multi-cluster (default 500).',\n '500',\n )\n .option('--format <fmt>', 'Output format: text | json. Default text.', 'text')\n .option('-o, --out <path>', 'Write report to a file instead of stdout.')\n .action(async (opts) => {\n const historyAbs = path.resolve(String(opts.historyFile));\n const historyRaw = await fs.readFile(historyAbs, 'utf8');\n const entries = JSON.parse(historyRaw) as QueryHistoryEntry[];\n if (!Array.isArray(entries)) {\n throw new Error(\n `--history-file ${historyAbs} did not parse to an array of QueryHistoryEntry.`,\n );\n }\n const report = recommendWarehouseSize(entries, {\n slowQueryThresholdMs: Number(opts.slowQueryMs ?? '30000'),\n sizeUpSlowFraction: Number(opts.sizeUpFraction ?? '0.2'),\n sizeDownMeanMaxMs: Number(opts.sizeDownMeanMs ?? '1000'),\n lowUseRunThreshold: Number(opts.lowUseRuns ?? '20'),\n multiClusterRunThreshold: Number(opts.multiClusterRuns ?? '500'),\n });\n const output =\n String(opts.format).toLowerCase() === 'json'\n ? JSON.stringify(report, null, 2)\n : renderWarehouseSizingMarkdown(report);\n if (opts.out) {\n const out = path.resolve(String(opts.out));\n await fs.mkdir(path.dirname(out), { recursive: true });\n await fs.writeFile(out, output, 'utf8');\n console.error(\n `Wrote ${out} — ${report.recommendations.length} warehouse(s) analysed (scanned ${report.entryCount} entries).`,\n );\n } else {\n process.stdout.write(output + '\\n');\n }\n });\n cmd.addCommand(warehouseSize);\n return cmd;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,146 @@
1
+ import "./chunk-DGUM43GV.js";
2
+
3
+ // src/commands/pii.ts
4
+ import { promises as fs } from "fs";
5
+ import path from "path";
6
+ import { Command } from "commander";
7
+ import {
8
+ ai,
9
+ loadProject,
10
+ pac,
11
+ parseProjectModel,
12
+ pii
13
+ } from "@ddt-tools/core";
14
+ function piiCommand() {
15
+ const cmd = new Command("pii");
16
+ cmd.description(
17
+ "Detect PII columns + render Unity-Catalog column-mask DDL. See `ddt pii scan` and `ddt pii mask`."
18
+ );
19
+ cmd.command("scan").description("Scan the project for PII column candidates.").requiredOption("--source <path>", ".ddtproj or .ddtpac to scan.").option("--format <fmt>", "table | json | markdown. Default table.", "table").option(
20
+ "--ai",
21
+ "Route low/medium-confidence candidates through the configured AI provider for verification.",
22
+ false
23
+ ).option(
24
+ "--verify-at-or-below <tier>",
25
+ "When --ai is on, verify candidates at or below this tier (high | medium | low).",
26
+ "medium"
27
+ ).option("-o, --out <path>", "Output file path. Defaults to stdout.").action(
28
+ async (opts) => {
29
+ const sourcePath = String(opts.source);
30
+ const model = await loadModel(sourcePath);
31
+ let candidates = pii.detectPiiCandidates(model);
32
+ if (opts.ai) {
33
+ candidates = await pii.verifyWithAi(candidates, {
34
+ completeFn: async (prompt) => {
35
+ const r = await ai.complete([{ role: "user", content: prompt }], {
36
+ feature: "pii.verify"
37
+ });
38
+ return r.text;
39
+ },
40
+ verifyAtOrBelow: opts.verifyAtOrBelow ?? "medium"
41
+ });
42
+ }
43
+ const fmt = (opts.format ?? "table").toLowerCase();
44
+ let payload;
45
+ if (fmt === "json") {
46
+ payload = JSON.stringify(candidates, null, 2);
47
+ } else if (fmt === "markdown") {
48
+ payload = renderMarkdown(candidates);
49
+ } else {
50
+ payload = renderTable(candidates);
51
+ }
52
+ await emit(payload, opts.out);
53
+ }
54
+ );
55
+ cmd.command("mask").description("Render Unity-Catalog column-mask DDL for detected PII columns.").requiredOption("--source <path>", ".ddtproj or .ddtpac to scan.").option(
56
+ "--unmasked-group <name>",
57
+ "Group that sees the unmasked value. Default 'pii-readers'.",
58
+ "pii-readers"
59
+ ).option(
60
+ "--mask-function-schema <fqn>",
61
+ "Where mask functions live. Default `governance`.",
62
+ "governance"
63
+ ).option("--ai", "Route low/medium-confidence candidates through the AI verifier first.", false).option("-o, --out <path>", "Output file path. Default: ./anonymize.sql.").action(
64
+ async (opts) => {
65
+ const sourcePath = String(opts.source);
66
+ const model = await loadModel(sourcePath);
67
+ let candidates = pii.detectPiiCandidates(model);
68
+ if (opts.ai) {
69
+ candidates = await pii.verifyWithAi(candidates, {
70
+ completeFn: async (prompt) => {
71
+ const r = await ai.complete([{ role: "user", content: prompt }], {
72
+ feature: "pii.mask.verify"
73
+ });
74
+ return r.text;
75
+ }
76
+ });
77
+ }
78
+ if (candidates.length === 0) {
79
+ console.error("No PII columns detected.");
80
+ return;
81
+ }
82
+ const sql = pii.renderAnonymizeScript(candidates, {
83
+ unmaskedGroup: String(opts.unmaskedGroup ?? "pii-readers"),
84
+ maskFunctionSchema: opts.maskFunctionSchema
85
+ });
86
+ const outPath = opts.out ? path.resolve(String(opts.out)) : path.resolve("anonymize.sql");
87
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
88
+ await fs.writeFile(outPath, sql, "utf8");
89
+ console.error(`Wrote ${outPath} (${candidates.length} candidate(s), ${sql.length} bytes).`);
90
+ }
91
+ );
92
+ return cmd;
93
+ }
94
+ function renderTable(cands) {
95
+ if (cands.length === 0) return "No PII candidates detected.\n";
96
+ const lines = [];
97
+ const fqnW = Math.max(8, ...cands.map((c) => c.fqn.length));
98
+ const colW = Math.max(8, ...cands.map((c) => c.column.length));
99
+ const catW = Math.max(8, ...cands.map((c) => c.category.length));
100
+ lines.push(
101
+ `${"fqn".padEnd(fqnW)} ${"column".padEnd(colW)} ${"category".padEnd(catW)} confidence reason`
102
+ );
103
+ lines.push(`${"-".repeat(fqnW)} ${"-".repeat(colW)} ${"-".repeat(catW)} ---------- ------`);
104
+ for (const c of cands) {
105
+ lines.push(
106
+ `${c.fqn.padEnd(fqnW)} ${c.column.padEnd(colW)} ${c.category.padEnd(catW)} ${c.confidence.padEnd(10)} ${c.reason}`
107
+ );
108
+ }
109
+ return lines.join("\n");
110
+ }
111
+ function renderMarkdown(cands) {
112
+ if (cands.length === 0) return "_No PII candidates detected._";
113
+ const lines = [];
114
+ lines.push(`# PII candidates (${cands.length})`);
115
+ lines.push("");
116
+ lines.push("| FQN | Column | Category | Confidence | Reason |");
117
+ lines.push("|---|---|---|---|---|");
118
+ for (const c of cands) {
119
+ lines.push(
120
+ `| \`${c.fqn}\` | \`${c.column}\` | ${c.category} | ${c.confidence} | ${c.reason} |`
121
+ );
122
+ }
123
+ return lines.join("\n");
124
+ }
125
+ async function emit(payload, out) {
126
+ if (out) {
127
+ const p = path.resolve(String(out));
128
+ await fs.mkdir(path.dirname(p), { recursive: true });
129
+ await fs.writeFile(p, payload + (payload.endsWith("\n") ? "" : "\n"), "utf8");
130
+ console.error(`Wrote ${p}.`);
131
+ } else {
132
+ process.stdout.write(payload + (payload.endsWith("\n") ? "" : "\n"));
133
+ }
134
+ }
135
+ async function loadModel(sourcePath) {
136
+ if (sourcePath.endsWith(".ddtpac")) {
137
+ const c = await pac.readPac(sourcePath);
138
+ return c.model;
139
+ }
140
+ const loaded = await loadProject(sourcePath);
141
+ return await parseProjectModel(loaded);
142
+ }
143
+ export {
144
+ piiCommand
145
+ };
146
+ //# sourceMappingURL=pii-QHU32VML.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/pii.ts"],"sourcesContent":["/**\n * `ddt pii` — scan a project for PII columns and optionally generate\n * Unity-Catalog column-mask DDL. Wraps `@ddt-tools/core/pii`.\n *\n * Mirrors `sdt pii`.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport {\n ai,\n loadProject,\n pac,\n parseProjectModel,\n pii,\n type DatabricksObject,\n} from '@ddt-tools/core';\n\nexport function piiCommand(): Command {\n const cmd = new Command('pii');\n cmd.description(\n 'Detect PII columns + render Unity-Catalog column-mask DDL. See `ddt pii scan` and `ddt pii mask`.',\n );\n\n cmd\n .command('scan')\n .description('Scan the project for PII column candidates.')\n .requiredOption('--source <path>', '.ddtproj or .ddtpac to scan.')\n .option('--format <fmt>', 'table | json | markdown. Default table.', 'table')\n .option(\n '--ai',\n 'Route low/medium-confidence candidates through the configured AI provider for verification.',\n false,\n )\n .option(\n '--verify-at-or-below <tier>',\n 'When --ai is on, verify candidates at or below this tier (high | medium | low).',\n 'medium',\n )\n .option('-o, --out <path>', 'Output file path. Defaults to stdout.')\n .action(\n async (opts: {\n source: string;\n format?: string;\n ai?: boolean;\n verifyAtOrBelow?: string;\n out?: string;\n }) => {\n const sourcePath = String(opts.source);\n const model = await loadModel(sourcePath);\n let candidates = pii.detectPiiCandidates(model);\n if (opts.ai) {\n candidates = await pii.verifyWithAi(candidates, {\n completeFn: async (prompt: string) => {\n const r = await ai.complete([{ role: 'user', content: prompt }], {\n feature: 'pii.verify',\n });\n return r.text;\n },\n verifyAtOrBelow: (opts.verifyAtOrBelow ?? 'medium') as 'high' | 'medium' | 'low',\n });\n }\n const fmt = (opts.format ?? 'table').toLowerCase();\n let payload: string;\n if (fmt === 'json') {\n payload = JSON.stringify(candidates, null, 2);\n } else if (fmt === 'markdown') {\n payload = renderMarkdown(candidates);\n } else {\n payload = renderTable(candidates);\n }\n await emit(payload, opts.out);\n },\n );\n\n cmd\n .command('mask')\n .description('Render Unity-Catalog column-mask DDL for detected PII columns.')\n .requiredOption('--source <path>', '.ddtproj or .ddtpac to scan.')\n .option(\n '--unmasked-group <name>',\n \"Group that sees the unmasked value. Default 'pii-readers'.\",\n 'pii-readers',\n )\n .option(\n '--mask-function-schema <fqn>',\n 'Where mask functions live. Default `governance`.',\n 'governance',\n )\n .option('--ai', 'Route low/medium-confidence candidates through the AI verifier first.', false)\n .option('-o, --out <path>', 'Output file path. Default: ./anonymize.sql.')\n .action(\n async (opts: {\n source: string;\n unmaskedGroup?: string;\n maskFunctionSchema?: string;\n ai?: boolean;\n out?: string;\n }) => {\n const sourcePath = String(opts.source);\n const model = await loadModel(sourcePath);\n let candidates = pii.detectPiiCandidates(model);\n if (opts.ai) {\n candidates = await pii.verifyWithAi(candidates, {\n completeFn: async (prompt: string) => {\n const r = await ai.complete([{ role: 'user', content: prompt }], {\n feature: 'pii.mask.verify',\n });\n return r.text;\n },\n });\n }\n if (candidates.length === 0) {\n console.error('No PII columns detected.');\n return;\n }\n const sql = pii.renderAnonymizeScript(candidates, {\n unmaskedGroup: String(opts.unmaskedGroup ?? 'pii-readers'),\n maskFunctionSchema: opts.maskFunctionSchema,\n });\n const outPath = opts.out ? path.resolve(String(opts.out)) : path.resolve('anonymize.sql');\n await fs.mkdir(path.dirname(outPath), { recursive: true });\n await fs.writeFile(outPath, sql, 'utf8');\n console.error(`Wrote ${outPath} (${candidates.length} candidate(s), ${sql.length} bytes).`);\n },\n );\n\n return cmd;\n}\n\nfunction renderTable(cands: readonly pii.PiiCandidate[]): string {\n if (cands.length === 0) return 'No PII candidates detected.\\n';\n const lines: string[] = [];\n const fqnW = Math.max(8, ...cands.map((c) => c.fqn.length));\n const colW = Math.max(8, ...cands.map((c) => c.column.length));\n const catW = Math.max(8, ...cands.map((c) => c.category.length));\n lines.push(\n `${'fqn'.padEnd(fqnW)} ${'column'.padEnd(colW)} ${'category'.padEnd(catW)} confidence reason`,\n );\n lines.push(`${'-'.repeat(fqnW)} ${'-'.repeat(colW)} ${'-'.repeat(catW)} ---------- ------`);\n for (const c of cands) {\n lines.push(\n `${c.fqn.padEnd(fqnW)} ${c.column.padEnd(colW)} ${c.category.padEnd(catW)} ${c.confidence.padEnd(10)} ${c.reason}`,\n );\n }\n return lines.join('\\n');\n}\n\nfunction renderMarkdown(cands: readonly pii.PiiCandidate[]): string {\n if (cands.length === 0) return '_No PII candidates detected._';\n const lines: string[] = [];\n lines.push(`# PII candidates (${cands.length})`);\n lines.push('');\n lines.push('| FQN | Column | Category | Confidence | Reason |');\n lines.push('|---|---|---|---|---|');\n for (const c of cands) {\n lines.push(\n `| \\`${c.fqn}\\` | \\`${c.column}\\` | ${c.category} | ${c.confidence} | ${c.reason} |`,\n );\n }\n return lines.join('\\n');\n}\n\nasync function emit(payload: string, out: unknown): Promise<void> {\n if (out) {\n const p = path.resolve(String(out));\n await fs.mkdir(path.dirname(p), { recursive: true });\n await fs.writeFile(p, payload + (payload.endsWith('\\n') ? '' : '\\n'), 'utf8');\n console.error(`Wrote ${p}.`);\n } else {\n process.stdout.write(payload + (payload.endsWith('\\n') ? '' : '\\n'));\n }\n}\n\nasync function loadModel(sourcePath: string): Promise<DatabricksObject[]> {\n if (sourcePath.endsWith('.ddtpac')) {\n const c = await pac.readPac(sourcePath);\n return c.model;\n }\n const loaded = await loadProject(sourcePath);\n return await parseProjectModel(loaded);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,29 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-VM2H4LAO.js";
4
+ import "./chunk-DGUM43GV.js";
5
+
6
+ // src/commands/pilot.ts
7
+ import { Command } from "commander";
8
+ import { license } from "@ddt-tools/core";
9
+ function pilotCommand() {
10
+ const cmd = new Command("pilot").description(
11
+ "Join or check the DDT pilot program (90-day Pro access for early adopters)."
12
+ );
13
+ cmd.command("register").description("Register for the DDT pilot program.").requiredOption("--email <addr>", "Your email address.").action(async (opts) => {
14
+ logger.info("Registering for the DDT pilot program\u2026");
15
+ const meta = await license.registerPilot(opts.email);
16
+ logger.info(`Registered! Email: ${meta.email}`);
17
+ logger.info("Your pilot JWT has been stored at ~/.ddt/license.jwt.");
18
+ logger.info("Run `ddt pilot status` to confirm.");
19
+ });
20
+ cmd.command("status").description("Show pilot program registration status.").action(async () => {
21
+ const status = await license.getPilotStatus();
22
+ logger.info(license.describePilotStatus(status));
23
+ });
24
+ return cmd;
25
+ }
26
+ export {
27
+ pilotCommand
28
+ };
29
+ //# sourceMappingURL=pilot-BR6GVK32.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/pilot.ts"],"sourcesContent":["/**\n * `ddt pilot register|status` — manage pilot program registration.\n * Mirrors `sdt pilot` in shape (PIL.2).\n */\nimport { Command } from 'commander';\nimport { license } from '@ddt-tools/core';\nimport { logger } from '../util/logger.js';\n\nexport function pilotCommand(): Command {\n const cmd = new Command('pilot').description(\n 'Join or check the DDT pilot program (90-day Pro access for early adopters).',\n );\n\n cmd\n .command('register')\n .description('Register for the DDT pilot program.')\n .requiredOption('--email <addr>', 'Your email address.')\n .action(async (opts: { email: string }) => {\n logger.info('Registering for the DDT pilot program…');\n const meta = await license.registerPilot(opts.email);\n logger.info(`Registered! Email: ${meta.email}`);\n logger.info('Your pilot JWT has been stored at ~/.ddt/license.jwt.');\n logger.info('Run `ddt pilot status` to confirm.');\n });\n\n cmd\n .command('status')\n .description('Show pilot program registration status.')\n .action(async () => {\n const status = await license.getPilotStatus();\n logger.info(license.describePilotStatus(status));\n });\n\n return cmd;\n}\n"],"mappings":";;;;;;AAIA,SAAS,eAAe;AACxB,SAAS,eAAe;AAGjB,SAAS,eAAwB;AACtC,QAAM,MAAM,IAAI,QAAQ,OAAO,EAAE;AAAA,IAC/B;AAAA,EACF;AAEA,MACG,QAAQ,UAAU,EAClB,YAAY,qCAAqC,EACjD,eAAe,kBAAkB,qBAAqB,EACtD,OAAO,OAAO,SAA4B;AACzC,WAAO,KAAK,6CAAwC;AACpD,UAAM,OAAO,MAAM,QAAQ,cAAc,KAAK,KAAK;AACnD,WAAO,KAAK,sBAAsB,KAAK,KAAK,EAAE;AAC9C,WAAO,KAAK,uDAAuD;AACnE,WAAO,KAAK,oCAAoC;AAAA,EAClD,CAAC;AAEH,MACG,QAAQ,QAAQ,EAChB,YAAY,yCAAyC,EACrD,OAAO,YAAY;AAClB,UAAM,SAAS,MAAM,QAAQ,eAAe;AAC5C,WAAO,KAAK,QAAQ,oBAAoB,MAAM,CAAC;AAAA,EACjD,CAAC;AAEH,SAAO;AACT;","names":[]}
@@ -0,0 +1,81 @@
1
+ import {
2
+ attachExplainFlag,
3
+ runExplain
4
+ } from "./chunk-XFXG347C.js";
5
+ import {
6
+ attachRelatedOptions
7
+ } from "./chunk-DL3V7UJ2.js";
8
+ import "./chunk-DGUM43GV.js";
9
+
10
+ // src/commands/pr-comment.ts
11
+ import { promises as fs } from "fs";
12
+ import path from "path";
13
+ import { Command } from "commander";
14
+ import {
15
+ CompareEngine,
16
+ PacSource,
17
+ ProjectSource,
18
+ loadProject,
19
+ pac,
20
+ parseProjectModel,
21
+ review,
22
+ safety
23
+ } from "@ddt-tools/core";
24
+ function prCommentCommand() {
25
+ const cmd = new Command("pr-comment");
26
+ cmd.description(
27
+ "Generate a Markdown PR comment from a source\u2194target compare (diff + safety + health)."
28
+ ).requiredOption("--source <path>", ".ddtproj or .ddtpac (the desired state).").requiredOption("--target <path>", ".ddtproj or .ddtpac (the current state).").option("-o, --out <path>", "Output file path. Defaults to stdout.").action(async (opts) => {
29
+ const sourcePath = String(opts.source);
30
+ const targetPath = String(opts.target);
31
+ const engine = new CompareEngine();
32
+ const src = sourcePath.endsWith(".ddtpac") ? new PacSource(sourcePath, "source") : new ProjectSource(sourcePath, "source");
33
+ const tgt = targetPath.endsWith(".ddtpac") ? new PacSource(targetPath, "target") : new ProjectSource(targetPath, "target");
34
+ const result = await engine.compare(src, tgt);
35
+ const assessment = safety.assess(result);
36
+ const model = await loadModel(sourcePath);
37
+ const md = review.renderPrComment(model, result, assessment, { source: sourcePath });
38
+ if (opts.out) {
39
+ const p = path.resolve(String(opts.out));
40
+ await fs.mkdir(path.dirname(p), { recursive: true });
41
+ await fs.writeFile(p, md + (md.endsWith("\n") ? "" : "\n"), "utf8");
42
+ console.error(`Wrote ${p} (${md.length} bytes).`);
43
+ } else {
44
+ process.stdout.write(md + (md.endsWith("\n") ? "" : "\n"));
45
+ }
46
+ await runExplain(
47
+ {
48
+ feature: "pr-comment.explain",
49
+ systemPrompt: "You are a release manager preparing the human summary for a code-review comment. Distill the structured PR comment into 3-5 sentences a reviewer can read in 30 seconds. Lead with the riskiest change."
50
+ },
51
+ opts,
52
+ () => `PR comment payload follows:
53
+
54
+ ${md}
55
+
56
+ Write a tight executive summary a busy reviewer will appreciate.`
57
+ );
58
+ });
59
+ attachExplainFlag(cmd);
60
+ attachRelatedOptions(cmd, [
61
+ "compare.ignoreCase",
62
+ "compare.ignoreComments",
63
+ "compare.ignoreFormattingDifferences",
64
+ "compare.excludeObjectTypes",
65
+ "compare.excludeObjectPatterns",
66
+ "compare.includeObjectPatterns"
67
+ ]);
68
+ return cmd;
69
+ }
70
+ async function loadModel(sourcePath) {
71
+ if (sourcePath.endsWith(".ddtpac")) {
72
+ const c = await pac.readPac(sourcePath);
73
+ return c.model;
74
+ }
75
+ const loaded = await loadProject(sourcePath);
76
+ return await parseProjectModel(loaded);
77
+ }
78
+ export {
79
+ prCommentCommand
80
+ };
81
+ //# sourceMappingURL=pr-comment-2FOA3EXG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/pr-comment.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport {\n CompareEngine,\n PacSource,\n ProjectSource,\n loadProject,\n pac,\n parseProjectModel,\n review,\n safety,\n type CompareSource,\n type DatabricksObject,\n} from '@ddt-tools/core';\nimport { attachExplainFlag, runExplain } from '../util/ai-explain.js';\nimport { attachRelatedOptions } from '../util/help-catalog.js';\n\n/**\n * `ddt pr-comment` — Markdown PR sticky-comment from a source↔target\n * compare. Mirrors `sdt pr-comment`.\n */\nexport function prCommentCommand(): Command {\n const cmd = new Command('pr-comment');\n cmd\n .description(\n 'Generate a Markdown PR comment from a source↔target compare (diff + safety + health).',\n )\n .requiredOption('--source <path>', '.ddtproj or .ddtpac (the desired state).')\n .requiredOption('--target <path>', '.ddtproj or .ddtpac (the current state).')\n .option('-o, --out <path>', 'Output file path. Defaults to stdout.')\n .action(async (opts: { source: string; target: string; out?: string; explain?: boolean }) => {\n const sourcePath = String(opts.source);\n const targetPath = String(opts.target);\n const engine = new CompareEngine();\n const src: CompareSource = sourcePath.endsWith('.ddtpac')\n ? new PacSource(sourcePath, 'source')\n : new ProjectSource(sourcePath, 'source');\n const tgt: CompareSource = targetPath.endsWith('.ddtpac')\n ? new PacSource(targetPath, 'target')\n : new ProjectSource(targetPath, 'target');\n const result = await engine.compare(src, tgt);\n const assessment = safety.assess(result);\n const model = await loadModel(sourcePath);\n const md = review.renderPrComment(model, result, assessment, { source: sourcePath });\n if (opts.out) {\n const p = path.resolve(String(opts.out));\n await fs.mkdir(path.dirname(p), { recursive: true });\n await fs.writeFile(p, md + (md.endsWith('\\n') ? '' : '\\n'), 'utf8');\n console.error(`Wrote ${p} (${md.length} bytes).`);\n } else {\n process.stdout.write(md + (md.endsWith('\\n') ? '' : '\\n'));\n }\n await runExplain(\n {\n feature: 'pr-comment.explain',\n systemPrompt:\n 'You are a release manager preparing the human summary for a code-review comment. Distill the structured PR comment into 3-5 sentences a reviewer can read in 30 seconds. Lead with the riskiest change.',\n },\n opts,\n () =>\n `PR comment payload follows:\\n\\n${md}\\n\\nWrite a tight executive summary a busy reviewer will appreciate.`,\n );\n });\n attachExplainFlag(cmd);\n attachRelatedOptions(cmd, [\n 'compare.ignoreCase',\n 'compare.ignoreComments',\n 'compare.ignoreFormattingDifferences',\n 'compare.excludeObjectTypes',\n 'compare.excludeObjectPatterns',\n 'compare.includeObjectPatterns',\n ]);\n return cmd;\n}\n\nasync function loadModel(sourcePath: string): Promise<DatabricksObject[]> {\n if (sourcePath.endsWith('.ddtpac')) {\n const c = await pac.readPac(sourcePath);\n return c.model;\n }\n const loaded = await loadProject(sourcePath);\n return await parseProjectModel(loaded);\n}\n"],"mappings":";;;;;;;;;;AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAQA,SAAS,mBAA4B;AAC1C,QAAM,MAAM,IAAI,QAAQ,YAAY;AACpC,MACG;AAAA,IACC;AAAA,EACF,EACC,eAAe,mBAAmB,0CAA0C,EAC5E,eAAe,mBAAmB,0CAA0C,EAC5E,OAAO,oBAAoB,uCAAuC,EAClE,OAAO,OAAO,SAA8E;AAC3F,UAAM,aAAa,OAAO,KAAK,MAAM;AACrC,UAAM,aAAa,OAAO,KAAK,MAAM;AACrC,UAAM,SAAS,IAAI,cAAc;AACjC,UAAM,MAAqB,WAAW,SAAS,SAAS,IACpD,IAAI,UAAU,YAAY,QAAQ,IAClC,IAAI,cAAc,YAAY,QAAQ;AAC1C,UAAM,MAAqB,WAAW,SAAS,SAAS,IACpD,IAAI,UAAU,YAAY,QAAQ,IAClC,IAAI,cAAc,YAAY,QAAQ;AAC1C,UAAM,SAAS,MAAM,OAAO,QAAQ,KAAK,GAAG;AAC5C,UAAM,aAAa,OAAO,OAAO,MAAM;AACvC,UAAM,QAAQ,MAAM,UAAU,UAAU;AACxC,UAAM,KAAK,OAAO,gBAAgB,OAAO,QAAQ,YAAY,EAAE,QAAQ,WAAW,CAAC;AACnF,QAAI,KAAK,KAAK;AACZ,YAAM,IAAI,KAAK,QAAQ,OAAO,KAAK,GAAG,CAAC;AACvC,YAAM,GAAG,MAAM,KAAK,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AACnD,YAAM,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS,IAAI,IAAI,KAAK,OAAO,MAAM;AAClE,cAAQ,MAAM,SAAS,CAAC,KAAK,GAAG,MAAM,UAAU;AAAA,IAClD,OAAO;AACL,cAAQ,OAAO,MAAM,MAAM,GAAG,SAAS,IAAI,IAAI,KAAK,KAAK;AAAA,IAC3D;AACA,UAAM;AAAA,MACJ;AAAA,QACE,SAAS;AAAA,QACT,cACE;AAAA,MACJ;AAAA,MACA;AAAA,MACA,MACE;AAAA;AAAA,EAAkC,EAAE;AAAA;AAAA;AAAA,IACxC;AAAA,EACF,CAAC;AACH,oBAAkB,GAAG;AACrB,uBAAqB,KAAK;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAEA,eAAe,UAAU,YAAiD;AACxE,MAAI,WAAW,SAAS,SAAS,GAAG;AAClC,UAAM,IAAI,MAAM,IAAI,QAAQ,UAAU;AACtC,WAAO,EAAE;AAAA,EACX;AACA,QAAM,SAAS,MAAM,YAAY,UAAU;AAC3C,SAAO,MAAM,kBAAkB,MAAM;AACvC;","names":[]}
@@ -0,0 +1,46 @@
1
+ import "./chunk-DGUM43GV.js";
2
+
3
+ // src/commands/preview.ts
4
+ import { promises as fs } from "fs";
5
+ import path from "path";
6
+ import { Command } from "commander";
7
+ import { preview, createConnection, getProfile } from "@ddt-tools/core";
8
+ function previewCommand() {
9
+ return new Command("preview").description(
10
+ "Preview rows from a table (SELECT * FROM <fqn> LIMIT <n>). Read-only \u2014 requires a connection profile."
11
+ ).argument("<fqn>", "Fully-qualified object name (catalog.schema.table).").requiredOption("--connection <profile>", "Connection profile to query.").option("--limit <n>", "Max rows to return. Default 100, max 10000.", "100").option("--where <expr>", "WHERE clause body (without the WHERE keyword).").option("--format <fmt>", "Output format: table | json. Default table.", "table").option("-o, --output <path>", "Write output to a file instead of stdout.").option("--sql-only", "Print the SELECT statement without executing it.", false).action(async (fqn, opts) => {
12
+ const limit = Number(opts.limit ?? "100") || 100;
13
+ const where = opts.where ? String(opts.where) : void 0;
14
+ const sql = preview.buildPreviewSql(fqn, { limit, ...where ? { where } : {} });
15
+ if (opts.sqlOnly === true) {
16
+ process.stdout.write(sql + "\n");
17
+ return;
18
+ }
19
+ const profile = await getProfile(String(opts.connection));
20
+ if (!profile) {
21
+ throw new Error(`Connection profile "${String(opts.connection)}" not found.`);
22
+ }
23
+ const conn = createConnection(profile);
24
+ await conn.connect();
25
+ try {
26
+ const result = await preview.previewRows(conn, fqn, {
27
+ limit,
28
+ ...where ? { where } : {}
29
+ });
30
+ const text = String(opts.format ?? "table").toLowerCase() === "json" ? JSON.stringify(result, null, 2) : preview.renderPreviewTable(result);
31
+ if (opts.output) {
32
+ const outPath = path.resolve(String(opts.output));
33
+ await fs.writeFile(outPath, text + "\n", "utf8");
34
+ console.error(`preview: wrote ${result.rows.length} row(s) to ${outPath}`);
35
+ } else {
36
+ process.stdout.write(text + "\n");
37
+ }
38
+ } finally {
39
+ await conn.disconnect();
40
+ }
41
+ });
42
+ }
43
+ export {
44
+ previewCommand
45
+ };
46
+ //# sourceMappingURL=preview-XNY422OU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/preview.ts"],"sourcesContent":["/**\n * `ddt preview <fqn>` — DCM compatibility item 3 (mirrors `sdt preview`).\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { preview, createConnection, getProfile } from '@ddt-tools/core';\n\nexport function previewCommand(): Command {\n return new Command('preview')\n .description(\n 'Preview rows from a table (SELECT * FROM <fqn> LIMIT <n>). ' +\n 'Read-only — requires a connection profile.',\n )\n .argument('<fqn>', 'Fully-qualified object name (catalog.schema.table).')\n .requiredOption('--connection <profile>', 'Connection profile to query.')\n .option('--limit <n>', 'Max rows to return. Default 100, max 10000.', '100')\n .option('--where <expr>', 'WHERE clause body (without the WHERE keyword).')\n .option('--format <fmt>', 'Output format: table | json. Default table.', 'table')\n .option('-o, --output <path>', 'Write output to a file instead of stdout.')\n .option('--sql-only', 'Print the SELECT statement without executing it.', false)\n .action(async (fqn: string, opts: Record<string, unknown>) => {\n const limit = Number(opts.limit ?? '100') || 100;\n const where = opts.where ? String(opts.where) : undefined;\n const sql = preview.buildPreviewSql(fqn, { limit, ...(where ? { where } : {}) });\n\n if (opts.sqlOnly === true) {\n process.stdout.write(sql + '\\n');\n return;\n }\n\n const profile = await getProfile(String(opts.connection));\n if (!profile) {\n throw new Error(`Connection profile \"${String(opts.connection)}\" not found.`);\n }\n const conn = createConnection(profile);\n await conn.connect();\n try {\n const result = await preview.previewRows(conn as preview.PreviewRunner, fqn, {\n limit,\n ...(where ? { where } : {}),\n });\n const text =\n String(opts.format ?? 'table').toLowerCase() === 'json'\n ? JSON.stringify(result, null, 2)\n : preview.renderPreviewTable(result);\n if (opts.output) {\n const outPath = path.resolve(String(opts.output));\n await fs.writeFile(outPath, text + '\\n', 'utf8');\n console.error(`preview: wrote ${result.rows.length} row(s) to ${outPath}`);\n } else {\n process.stdout.write(text + '\\n');\n }\n } finally {\n await conn.disconnect();\n }\n });\n}\n"],"mappings":";;;AAGA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB,SAAS,SAAS,kBAAkB,kBAAkB;AAE/C,SAAS,iBAA0B;AACxC,SAAO,IAAI,QAAQ,SAAS,EACzB;AAAA,IACC;AAAA,EAEF,EACC,SAAS,SAAS,qDAAqD,EACvE,eAAe,0BAA0B,8BAA8B,EACvE,OAAO,eAAe,+CAA+C,KAAK,EAC1E,OAAO,kBAAkB,gDAAgD,EACzE,OAAO,kBAAkB,+CAA+C,OAAO,EAC/E,OAAO,uBAAuB,2CAA2C,EACzE,OAAO,cAAc,oDAAoD,KAAK,EAC9E,OAAO,OAAO,KAAa,SAAkC;AAC5D,UAAM,QAAQ,OAAO,KAAK,SAAS,KAAK,KAAK;AAC7C,UAAM,QAAQ,KAAK,QAAQ,OAAO,KAAK,KAAK,IAAI;AAChD,UAAM,MAAM,QAAQ,gBAAgB,KAAK,EAAE,OAAO,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC,EAAG,CAAC;AAE/E,QAAI,KAAK,YAAY,MAAM;AACzB,cAAQ,OAAO,MAAM,MAAM,IAAI;AAC/B;AAAA,IACF;AAEA,UAAM,UAAU,MAAM,WAAW,OAAO,KAAK,UAAU,CAAC;AACxD,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,MAAM,uBAAuB,OAAO,KAAK,UAAU,CAAC,cAAc;AAAA,IAC9E;AACA,UAAM,OAAO,iBAAiB,OAAO;AACrC,UAAM,KAAK,QAAQ;AACnB,QAAI;AACF,YAAM,SAAS,MAAM,QAAQ,YAAY,MAA+B,KAAK;AAAA,QAC3E;AAAA,QACA,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,MAC3B,CAAC;AACD,YAAM,OACJ,OAAO,KAAK,UAAU,OAAO,EAAE,YAAY,MAAM,SAC7C,KAAK,UAAU,QAAQ,MAAM,CAAC,IAC9B,QAAQ,mBAAmB,MAAM;AACvC,UAAI,KAAK,QAAQ;AACf,cAAM,UAAU,KAAK,QAAQ,OAAO,KAAK,MAAM,CAAC;AAChD,cAAM,GAAG,UAAU,SAAS,OAAO,MAAM,MAAM;AAC/C,gBAAQ,MAAM,kBAAkB,OAAO,KAAK,MAAM,cAAc,OAAO,EAAE;AAAA,MAC3E,OAAO;AACL,gBAAQ,OAAO,MAAM,OAAO,IAAI;AAAA,MAClC;AAAA,IACF,UAAE;AACA,YAAM,KAAK,WAAW;AAAA,IACxB;AAAA,EACF,CAAC;AACL;","names":[]}