@sdt-tools/cli 0.2.0 → 0.2.6

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 (207) hide show
  1. package/dist/advise-tests-6DRSZMBL.js +87 -0
  2. package/dist/advise-tests-6DRSZMBL.js.map +1 -0
  3. package/dist/ai-G4MJWHTM.js +89 -0
  4. package/dist/ai-G4MJWHTM.js.map +1 -0
  5. package/dist/anonymize-QR6JGXA7.js +123 -0
  6. package/dist/anonymize-QR6JGXA7.js.map +1 -0
  7. package/dist/approval-YVHYTV53.js +73 -0
  8. package/dist/approval-YVHYTV53.js.map +1 -0
  9. package/dist/approval-chain-54KKJZS3.js +120 -0
  10. package/dist/approval-chain-54KKJZS3.js.map +1 -0
  11. package/dist/audit-log-QZFH7LUX.js +159 -0
  12. package/dist/audit-log-QZFH7LUX.js.map +1 -0
  13. package/dist/backlog-V2YUIQDL.js +76 -0
  14. package/dist/backlog-V2YUIQDL.js.map +1 -0
  15. package/dist/bisect-GEVYAVL5.js +111 -0
  16. package/dist/bisect-GEVYAVL5.js.map +1 -0
  17. package/dist/bookmarks-57LKS7P6.js +107 -0
  18. package/dist/bookmarks-57LKS7P6.js.map +1 -0
  19. package/dist/branch-W2MGMPSH.js +88 -0
  20. package/dist/branch-W2MGMPSH.js.map +1 -0
  21. package/dist/build-VNIQFKSP.js +23 -0
  22. package/dist/build-VNIQFKSP.js.map +1 -0
  23. package/dist/catalog-JLB5VCEV.js +137 -0
  24. package/dist/catalog-JLB5VCEV.js.map +1 -0
  25. package/dist/changelog-M7XGDYSY.js +220 -0
  26. package/dist/changelog-M7XGDYSY.js.map +1 -0
  27. package/dist/chunk-DGUM43GV.js +11 -0
  28. package/dist/chunk-DGUM43GV.js.map +1 -0
  29. package/dist/chunk-EWXM4KJN.js +25 -0
  30. package/dist/chunk-EWXM4KJN.js.map +1 -0
  31. package/dist/chunk-JP2EZLR5.js +50 -0
  32. package/dist/chunk-JP2EZLR5.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-ZWY4ZRHL.js +44 -0
  36. package/dist/chunk-ZWY4ZRHL.js.map +1 -0
  37. package/dist/cli.js +511 -19014
  38. package/dist/cli.js.map +1 -1
  39. package/dist/compare-5O6UTWPJ.js +405 -0
  40. package/dist/compare-5O6UTWPJ.js.map +1 -0
  41. package/dist/compare-profiles-7ZSNIW7B.js +218 -0
  42. package/dist/compare-profiles-7ZSNIW7B.js.map +1 -0
  43. package/dist/completion-I5U5VVAX.js +82 -0
  44. package/dist/completion-I5U5VVAX.js.map +1 -0
  45. package/dist/connection-GNTZDHXF.js +133 -0
  46. package/dist/connection-GNTZDHXF.js.map +1 -0
  47. package/dist/cost-estimate-TJDDH6TO.js +328 -0
  48. package/dist/cost-estimate-TJDDH6TO.js.map +1 -0
  49. package/dist/data-compare-UK2UXAS3.js +134 -0
  50. package/dist/data-compare-UK2UXAS3.js.map +1 -0
  51. package/dist/data-fit-Q45ENBRL.js +125 -0
  52. package/dist/data-fit-Q45ENBRL.js.map +1 -0
  53. package/dist/deploy-status-UUHKVDTI.js +58 -0
  54. package/dist/deploy-status-UUHKVDTI.js.map +1 -0
  55. package/dist/design-PO6UPBL7.js +138 -0
  56. package/dist/design-PO6UPBL7.js.map +1 -0
  57. package/dist/diagnose-6IFMELFR.js +145 -0
  58. package/dist/diagnose-6IFMELFR.js.map +1 -0
  59. package/dist/discover-A7OSZAHK.js +78 -0
  60. package/dist/discover-A7OSZAHK.js.map +1 -0
  61. package/dist/docs-CVRKGUSW.js +177 -0
  62. package/dist/docs-CVRKGUSW.js.map +1 -0
  63. package/dist/drift-XDA3BDYN.js +226 -0
  64. package/dist/drift-XDA3BDYN.js.map +1 -0
  65. package/dist/drift-gate-V7QSIOGZ.js +94 -0
  66. package/dist/drift-gate-V7QSIOGZ.js.map +1 -0
  67. package/dist/error-lookup-7ZWCZJ44.js +56 -0
  68. package/dist/error-lookup-7ZWCZJ44.js.map +1 -0
  69. package/dist/errorReporting-AQXKKGZH.js +109 -0
  70. package/dist/errorReporting-AQXKKGZH.js.map +1 -0
  71. package/dist/exec-PKBHLI7T.js +121 -0
  72. package/dist/exec-PKBHLI7T.js.map +1 -0
  73. package/dist/explain-LWKJOTL7.js +192 -0
  74. package/dist/explain-LWKJOTL7.js.map +1 -0
  75. package/dist/explorer-QOVM6VBD.js +61 -0
  76. package/dist/explorer-QOVM6VBD.js.map +1 -0
  77. package/dist/export-IYYBZ5HE.js +42 -0
  78. package/dist/export-IYYBZ5HE.js.map +1 -0
  79. package/dist/extract-VMMVRQVT.js +102 -0
  80. package/dist/extract-VMMVRQVT.js.map +1 -0
  81. package/dist/features-LE6BDZ2S.js +59 -0
  82. package/dist/features-LE6BDZ2S.js.map +1 -0
  83. package/dist/feedback-M7DM2EQC.js +161 -0
  84. package/dist/feedback-M7DM2EQC.js.map +1 -0
  85. package/dist/find-EME2JG2I.js +176 -0
  86. package/dist/find-EME2JG2I.js.map +1 -0
  87. package/dist/format-TRLWLMGS.js +141 -0
  88. package/dist/format-TRLWLMGS.js.map +1 -0
  89. package/dist/generate-6NAZGZDV.js +152 -0
  90. package/dist/generate-6NAZGZDV.js.map +1 -0
  91. package/dist/graph-QNQDAUO7.js +161 -0
  92. package/dist/graph-QNQDAUO7.js.map +1 -0
  93. package/dist/history-RONA7ZTI.js +199 -0
  94. package/dist/history-RONA7ZTI.js.map +1 -0
  95. package/dist/hosts-YBXY2ZG5.js +49 -0
  96. package/dist/hosts-YBXY2ZG5.js.map +1 -0
  97. package/dist/impact-T2JSANHS.js +59 -0
  98. package/dist/impact-T2JSANHS.js.map +1 -0
  99. package/dist/import-AELYLY6A.js +32 -0
  100. package/dist/import-AELYLY6A.js.map +1 -0
  101. package/dist/import-script-2OF5BI6A.js +83 -0
  102. package/dist/import-script-2OF5BI6A.js.map +1 -0
  103. package/dist/index.cjs +71 -12
  104. package/dist/index.cjs.map +1 -1
  105. package/dist/index.js +95 -31
  106. package/dist/index.js.map +1 -1
  107. package/dist/init-SWRRJMGI.js +57 -0
  108. package/dist/init-SWRRJMGI.js.map +1 -0
  109. package/dist/install-hooks-6SIAGTAF.js +109 -0
  110. package/dist/install-hooks-6SIAGTAF.js.map +1 -0
  111. package/dist/license-OAF22PLZ.js +46 -0
  112. package/dist/license-OAF22PLZ.js.map +1 -0
  113. package/dist/lineage-EW66XJ6O.js +552 -0
  114. package/dist/lineage-EW66XJ6O.js.map +1 -0
  115. package/dist/lint-FQ2OTYTQ.js +143 -0
  116. package/dist/lint-FQ2OTYTQ.js.map +1 -0
  117. package/dist/mcp-SARDMCDV.js +344 -0
  118. package/dist/mcp-SARDMCDV.js.map +1 -0
  119. package/dist/migrate-from-dbt-JVTXPWKQ.js +156 -0
  120. package/dist/migrate-from-dbt-JVTXPWKQ.js.map +1 -0
  121. package/dist/migrate-platform-NTRTOGNR.js +91 -0
  122. package/dist/migrate-platform-NTRTOGNR.js.map +1 -0
  123. package/dist/optimize-CJYWMAWA.js +105 -0
  124. package/dist/optimize-CJYWMAWA.js.map +1 -0
  125. package/dist/perf-LL2CPCJF.js +205 -0
  126. package/dist/perf-LL2CPCJF.js.map +1 -0
  127. package/dist/pii-FBDRDQ2E.js +136 -0
  128. package/dist/pii-FBDRDQ2E.js.map +1 -0
  129. package/dist/pilot-CCQERKPH.js +29 -0
  130. package/dist/pilot-CCQERKPH.js.map +1 -0
  131. package/dist/pr-comment-S5FF4QRX.js +79 -0
  132. package/dist/pr-comment-S5FF4QRX.js.map +1 -0
  133. package/dist/preview-5U4YVCRM.js +47 -0
  134. package/dist/preview-5U4YVCRM.js.map +1 -0
  135. package/dist/profile-7VC57KD2.js +101 -0
  136. package/dist/profile-7VC57KD2.js.map +1 -0
  137. package/dist/promote-AASEFTIA.js +408 -0
  138. package/dist/promote-AASEFTIA.js.map +1 -0
  139. package/dist/publish-UMVIWH6H.js +721 -0
  140. package/dist/publish-UMVIWH6H.js.map +1 -0
  141. package/dist/purge-QMXZKCMD.js +57 -0
  142. package/dist/purge-QMXZKCMD.js.map +1 -0
  143. package/dist/query-log-6OM4GI7W.js +112 -0
  144. package/dist/query-log-6OM4GI7W.js.map +1 -0
  145. package/dist/refactor-LTZQLJ35.js +5799 -0
  146. package/dist/refactor-LTZQLJ35.js.map +1 -0
  147. package/dist/refresh-4TY2AGOU.js +38 -0
  148. package/dist/refresh-4TY2AGOU.js.map +1 -0
  149. package/dist/replay-OOC25FZN.js +117 -0
  150. package/dist/replay-OOC25FZN.js.map +1 -0
  151. package/dist/revert-ODMUVJW6.js +110 -0
  152. package/dist/revert-ODMUVJW6.js.map +1 -0
  153. package/dist/review-XXPWOBFP.js +158 -0
  154. package/dist/review-XXPWOBFP.js.map +1 -0
  155. package/dist/rollback-suggest-6G2HEKFR.js +79 -0
  156. package/dist/rollback-suggest-6G2HEKFR.js.map +1 -0
  157. package/dist/safer-alternative-QFVNLG3L.js +89 -0
  158. package/dist/safer-alternative-QFVNLG3L.js.map +1 -0
  159. package/dist/safety-7QWRSUEZ.js +168 -0
  160. package/dist/safety-7QWRSUEZ.js.map +1 -0
  161. package/dist/savings-RHIXP6IT.js +95 -0
  162. package/dist/savings-RHIXP6IT.js.map +1 -0
  163. package/dist/scan-secrets-5YCQ4UCU.js +54 -0
  164. package/dist/scan-secrets-5YCQ4UCU.js.map +1 -0
  165. package/dist/schema-CIZXCQD2.js +429 -0
  166. package/dist/schema-CIZXCQD2.js.map +1 -0
  167. package/dist/script-K7CIN2P6.js +153 -0
  168. package/dist/script-K7CIN2P6.js.map +1 -0
  169. package/dist/search-BUZ5NXZZ.js +151 -0
  170. package/dist/search-BUZ5NXZZ.js.map +1 -0
  171. package/dist/seed-76QAK276.js +96 -0
  172. package/dist/seed-76QAK276.js.map +1 -0
  173. package/dist/sketch-PTLKDIK3.js +88 -0
  174. package/dist/sketch-PTLKDIK3.js.map +1 -0
  175. package/dist/snapshot-XLPR2OZ5.js +177 -0
  176. package/dist/snapshot-XLPR2OZ5.js.map +1 -0
  177. package/dist/snippets-EK4DK5CN.js +74 -0
  178. package/dist/snippets-EK4DK5CN.js.map +1 -0
  179. package/dist/standards-7T2UY6DD.js +241 -0
  180. package/dist/standards-7T2UY6DD.js.map +1 -0
  181. package/dist/suggest-VGRYSAR6.js +39 -0
  182. package/dist/suggest-VGRYSAR6.js.map +1 -0
  183. package/dist/suggest-constraints-MY5WKUHA.js +160 -0
  184. package/dist/suggest-constraints-MY5WKUHA.js.map +1 -0
  185. package/dist/suite-TRNGZWQM.js +88 -0
  186. package/dist/suite-TRNGZWQM.js.map +1 -0
  187. package/dist/telemetry-3U2QLA2S.js +75 -0
  188. package/dist/telemetry-3U2QLA2S.js.map +1 -0
  189. package/dist/template-ZERIXVXF.js +403 -0
  190. package/dist/template-ZERIXVXF.js.map +1 -0
  191. package/dist/test-5M2ED3WT.js +169 -0
  192. package/dist/test-5M2ED3WT.js.map +1 -0
  193. package/dist/trial-U732FONV.js +31 -0
  194. package/dist/trial-U732FONV.js.map +1 -0
  195. package/dist/validate-T6D2WCOK.js +106 -0
  196. package/dist/validate-T6D2WCOK.js.map +1 -0
  197. package/dist/verify-KXVASEEG.js +76 -0
  198. package/dist/verify-KXVASEEG.js.map +1 -0
  199. package/dist/watch-I6K4BNMA.js +80 -0
  200. package/dist/watch-I6K4BNMA.js.map +1 -0
  201. package/dist/xcompare-TPFLQO6W.js +87 -0
  202. package/dist/xcompare-TPFLQO6W.js.map +1 -0
  203. package/package.json +2 -2
  204. package/dist/cli.cjs +0 -19040
  205. package/dist/cli.cjs.map +0 -1
  206. package/dist/cli.d.cts +0 -1
  207. package/dist/cli.d.ts +0 -1
@@ -0,0 +1,205 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-VM2H4LAO.js";
4
+ import "./chunk-DGUM43GV.js";
5
+
6
+ // src/commands/perf.ts
7
+ import { promises as fs } from "fs";
8
+ import path from "path";
9
+ import { Command } from "commander";
10
+ import { loadProject, parseProjectModel } from "@sdt-tools/core/project";
11
+ import {
12
+ findMaterializedViewCandidates,
13
+ findUnusedObjects,
14
+ recommendWarehouseSize,
15
+ renderMvCandidatesMarkdown,
16
+ renderUnusedObjectsMarkdown,
17
+ renderWarehouseSizingMarkdown
18
+ } from "@sdt-tools/core/perf";
19
+ var COLD_START_BUDGET_MS = 200;
20
+ function perfCommand() {
21
+ const cmd = new Command("perf");
22
+ cmd.description("CLI performance diagnostics (cold-start audit, hot-path profiling).");
23
+ const coldStart = new Command("cold-start");
24
+ coldStart.description("Measure CLI cold-start latency and compare against the 200ms budget.").option("--json", "Emit JSON instead of human-readable output.", false).option(
25
+ "--budget <ms>",
26
+ "Override the pass/fail threshold in ms.",
27
+ String(COLD_START_BUDGET_MS)
28
+ ).action((opts) => {
29
+ const startNs = globalThis.__SDT_START_NS__;
30
+ const nowNs = process.hrtime.bigint();
31
+ const budgetMs = Number(opts.budget ?? COLD_START_BUDGET_MS);
32
+ let elapsedMs = null;
33
+ if (startNs !== void 0) {
34
+ elapsedMs = Number(nowNs - startNs) / 1e6;
35
+ }
36
+ const pass = elapsedMs !== null && elapsedMs <= budgetMs;
37
+ const note = elapsedMs === null ? "Start timestamp not recorded \u2014 add `globalThis.__SDT_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`;
38
+ if (opts.json) {
39
+ process.stdout.write(JSON.stringify({ elapsedMs, budgetMs, pass, note }, null, 2) + "\n");
40
+ } else {
41
+ console.log(`sdt cold-start: ${note}`);
42
+ if (elapsedMs !== null) {
43
+ const bar = "\u2588".repeat(Math.min(40, Math.round(elapsedMs / budgetMs * 40)));
44
+ const pct = (elapsedMs / budgetMs * 100).toFixed(0);
45
+ console.log(` ${bar} ${pct}% of ${budgetMs}ms budget`);
46
+ }
47
+ }
48
+ process.exitCode = pass ? 0 : 1;
49
+ });
50
+ cmd.addCommand(coldStart);
51
+ const unused = new Command("unused");
52
+ unused.description(
53
+ "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."
54
+ ).requiredOption("-p, --project <path>", "Path to the .sdtproj file.").requiredOption(
55
+ "--history-file <path>",
56
+ "Path to a JSON file containing a QueryHistoryEntry[] (e.g. saved from `sdt history --last 1000 --format json`)."
57
+ ).option("--threshold-days <n>", "Days-of-inactivity cutoff (default 30).", "30").option(
58
+ "--reference-date <iso>",
59
+ 'ISO-8601 timestamp used as "now" (default: most recent entry).'
60
+ ).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) => {
61
+ const loaded = await loadProject(String(opts.project));
62
+ const model = await parseProjectModel(loaded);
63
+ const testable = /* @__PURE__ */ new Set([
64
+ "TABLE",
65
+ "EXTERNAL_TABLE",
66
+ "ICEBERG_TABLE",
67
+ "HYBRID_TABLE",
68
+ "DYNAMIC_TABLE",
69
+ "EVENT_TABLE",
70
+ "VIEW",
71
+ "MATERIALIZED_VIEW"
72
+ ]);
73
+ const fqns = [];
74
+ for (const obj of model) {
75
+ if (!testable.has(obj.objectType)) continue;
76
+ const db = obj.fqn.database;
77
+ const sc = obj.fqn.schema;
78
+ if (!db || !sc) continue;
79
+ fqns.push(`${db}.${sc}.${obj.fqn.name}`);
80
+ }
81
+ const historyAbs = path.resolve(String(opts.historyFile));
82
+ const historyRaw = await fs.readFile(historyAbs, "utf8");
83
+ const entries = JSON.parse(historyRaw);
84
+ if (!Array.isArray(entries)) {
85
+ throw new Error(
86
+ `--history-file ${historyAbs} did not parse to an array of QueryHistoryEntry.`
87
+ );
88
+ }
89
+ const thresholdDays = Number(opts.thresholdDays ?? "30");
90
+ const report = findUnusedObjects(entries, fqns, {
91
+ thresholdDays,
92
+ referenceDate: opts.referenceDate ? String(opts.referenceDate) : void 0
93
+ });
94
+ const output = String(opts.format).toLowerCase() === "json" ? JSON.stringify(report, null, 2) : renderUnusedObjectsMarkdown(report);
95
+ if (opts.out) {
96
+ const out = path.resolve(String(opts.out));
97
+ await fs.mkdir(path.dirname(out), { recursive: true });
98
+ await fs.writeFile(out, output, "utf8");
99
+ logger.info(
100
+ `Wrote ${out} \u2014 ${report.unusedCount}/${report.totalObjects} object(s) unreferenced \u2265 ${thresholdDays}d.`
101
+ );
102
+ } else {
103
+ process.stdout.write(output + "\n");
104
+ }
105
+ });
106
+ cmd.addCommand(unused);
107
+ const mvCandidates = new Command("mv-candidates");
108
+ mvCandidates.description(
109
+ "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."
110
+ ).requiredOption(
111
+ "--history-file <path>",
112
+ "Path to a JSON file containing a QueryHistoryEntry[] (e.g. saved from `sdt history --last 1000 --format json`)."
113
+ ).option("--threshold-runs <n>", "Min run count to qualify as a candidate (default 5).", "5").option(
114
+ "--threshold-total-ms <n>",
115
+ "Min sum of durationMs across runs to qualify (default 60000).",
116
+ "60000"
117
+ ).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) => {
118
+ const historyAbs = path.resolve(String(opts.historyFile));
119
+ const historyRaw = await fs.readFile(historyAbs, "utf8");
120
+ const entries = JSON.parse(historyRaw);
121
+ if (!Array.isArray(entries)) {
122
+ throw new Error(
123
+ `--history-file ${historyAbs} did not parse to an array of QueryHistoryEntry.`
124
+ );
125
+ }
126
+ const report = findMaterializedViewCandidates(entries, {
127
+ thresholdRuns: Number(opts.thresholdRuns ?? "5"),
128
+ thresholdTotalMs: Number(opts.thresholdTotalMs ?? "60000"),
129
+ topN: Number(opts.topN ?? "25"),
130
+ mvSchema: opts.mvSchema ? String(opts.mvSchema) : void 0
131
+ });
132
+ const output = String(opts.format).toLowerCase() === "json" ? JSON.stringify(report, null, 2) : renderMvCandidatesMarkdown(report);
133
+ if (opts.out) {
134
+ const out = path.resolve(String(opts.out));
135
+ await fs.mkdir(path.dirname(out), { recursive: true });
136
+ await fs.writeFile(out, output, "utf8");
137
+ logger.info(
138
+ `Wrote ${out} \u2014 ${report.candidates.length} MV candidate(s) (scanned ${report.entryCount} entries).`
139
+ );
140
+ } else {
141
+ process.stdout.write(output + "\n");
142
+ }
143
+ });
144
+ cmd.addCommand(mvCandidates);
145
+ const warehouseSize = new Command("warehouse-size");
146
+ warehouseSize.description(
147
+ "Per-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."
148
+ ).requiredOption(
149
+ "--history-file <path>",
150
+ "Path to a JSON file containing a QueryHistoryEntry[]."
151
+ ).option(
152
+ "--slow-query-ms <n>",
153
+ 'Threshold above which a query counts as "slow" (default 30000).',
154
+ "30000"
155
+ ).option(
156
+ "--size-up-fraction <p>",
157
+ "Fraction of slow queries that triggers size-up (default 0.2).",
158
+ "0.2"
159
+ ).option(
160
+ "--size-down-mean-ms <n>",
161
+ "Mean-duration cap below which size-down may trigger (default 1000).",
162
+ "1000"
163
+ ).option(
164
+ "--low-use-runs <n>",
165
+ "Run-count cap below which size-down may trigger (default 20).",
166
+ "20"
167
+ ).option(
168
+ "--multi-cluster-runs <n>",
169
+ "Run-count floor above which size-up upgrades to multi-cluster (default 500).",
170
+ "500"
171
+ ).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) => {
172
+ const historyAbs = path.resolve(String(opts.historyFile));
173
+ const historyRaw = await fs.readFile(historyAbs, "utf8");
174
+ const entries = JSON.parse(historyRaw);
175
+ if (!Array.isArray(entries)) {
176
+ throw new Error(
177
+ `--history-file ${historyAbs} did not parse to an array of QueryHistoryEntry.`
178
+ );
179
+ }
180
+ const report = recommendWarehouseSize(entries, {
181
+ slowQueryThresholdMs: Number(opts.slowQueryMs ?? "30000"),
182
+ sizeUpSlowFraction: Number(opts.sizeUpFraction ?? "0.2"),
183
+ sizeDownMeanMaxMs: Number(opts.sizeDownMeanMs ?? "1000"),
184
+ lowUseRunThreshold: Number(opts.lowUseRuns ?? "20"),
185
+ multiClusterRunThreshold: Number(opts.multiClusterRuns ?? "500")
186
+ });
187
+ const output = String(opts.format).toLowerCase() === "json" ? JSON.stringify(report, null, 2) : renderWarehouseSizingMarkdown(report);
188
+ if (opts.out) {
189
+ const out = path.resolve(String(opts.out));
190
+ await fs.mkdir(path.dirname(out), { recursive: true });
191
+ await fs.writeFile(out, output, "utf8");
192
+ logger.info(
193
+ `Wrote ${out} \u2014 ${report.recommendations.length} warehouse(s) analysed (scanned ${report.entryCount} entries).`
194
+ );
195
+ } else {
196
+ process.stdout.write(output + "\n");
197
+ }
198
+ });
199
+ cmd.addCommand(warehouseSize);
200
+ return cmd;
201
+ }
202
+ export {
203
+ perfCommand
204
+ };
205
+ //# sourceMappingURL=perf-LL2CPCJF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/perf.ts"],"sourcesContent":["/**\n * `sdt 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 `Databricks/packages/cli/src/commands/perf.ts`.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { loadProject, parseProjectModel } from '@sdt-tools/core/project';\nimport {\n findMaterializedViewCandidates,\n findUnusedObjects,\n recommendWarehouseSize,\n renderMvCandidatesMarkdown,\n renderUnusedObjectsMarkdown,\n renderWarehouseSizingMarkdown,\n} from '@sdt-tools/core/perf';\nimport type { QueryHistoryEntry } from '@sdt-tools/core/queryHistory';\nimport { logger } from '../util/logger.js';\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 { __SDT_START_NS__?: bigint })\n .__SDT_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.__SDT_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(`sdt 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 .sdtproj file.')\n .requiredOption(\n '--history-file <path>',\n 'Path to a JSON file containing a QueryHistoryEntry[] (e.g. saved from `sdt 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 'EXTERNAL_TABLE',\n 'ICEBERG_TABLE',\n 'HYBRID_TABLE',\n 'DYNAMIC_TABLE',\n 'EVENT_TABLE',\n 'VIEW',\n 'MATERIALIZED_VIEW',\n ]);\n const fqns: string[] = [];\n for (const obj of model) {\n if (!testable.has(obj.objectType)) continue;\n const db = obj.fqn.database;\n const sc = obj.fqn.schema;\n if (!db || !sc) continue;\n fqns.push(`${db}.${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 logger.info(\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 `sdt 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 logger.info(\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-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 logger.info(\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,136 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-VM2H4LAO.js";
4
+ import "./chunk-DGUM43GV.js";
5
+
6
+ // src/commands/pii.ts
7
+ import { promises as fs } from "fs";
8
+ import path from "path";
9
+ import { Command } from "commander";
10
+ import { ai, pac, pii, project } from "@sdt-tools/core";
11
+ function piiCommand() {
12
+ const cmd = new Command("pii");
13
+ cmd.description(
14
+ "Detect PII columns + render Snowflake MASKING POLICY DDL. See `sdt pii scan` and `sdt pii mask`."
15
+ );
16
+ cmd.command("scan").description("Scan the project for PII column candidates.").requiredOption("--source <path>", ".sdtproj or .sdtpac to scan.").option("--format <fmt>", "table | json | markdown. Default table.", "table").option(
17
+ "--ai",
18
+ "Route low/medium-confidence candidates through the configured AI provider for verification.",
19
+ false
20
+ ).option(
21
+ "--verify-at-or-below <tier>",
22
+ "When --ai is on, verify candidates at or below this tier (high | medium | low).",
23
+ "medium"
24
+ ).option("-o, --out <path>", "Output file path. Defaults to stdout.").action(
25
+ async (opts) => {
26
+ const sourcePath = String(opts.source);
27
+ const model = await loadModel(sourcePath);
28
+ let candidates = pii.detectPiiCandidates(model);
29
+ if (opts.ai) {
30
+ candidates = await pii.verifyWithAi(candidates, {
31
+ completeFn: async (prompt) => {
32
+ const r = await ai.complete([{ role: "user", content: prompt }], {
33
+ feature: "pii.verify"
34
+ });
35
+ return r.text;
36
+ },
37
+ verifyAtOrBelow: opts.verifyAtOrBelow ?? "medium"
38
+ });
39
+ }
40
+ const fmt = (opts.format ?? "table").toLowerCase();
41
+ let payload;
42
+ if (fmt === "json") {
43
+ payload = JSON.stringify(candidates, null, 2);
44
+ } else if (fmt === "markdown") {
45
+ payload = renderMarkdown(candidates);
46
+ } else {
47
+ payload = renderTable(candidates);
48
+ }
49
+ await emit(payload, opts.out);
50
+ }
51
+ );
52
+ cmd.command("mask").description("Render Snowflake MASKING POLICY DDL for detected PII columns.").requiredOption("--source <path>", ".sdtproj or .sdtpac to scan.").option(
53
+ "--unmasked-role <name>",
54
+ "Role that sees the unmasked value. Default 'PROD_ETL'.",
55
+ "PROD_ETL"
56
+ ).option("--ai", "Route low/medium-confidence candidates through the AI verifier first.", false).option("-o, --out <path>", "Output file path. Default: ./anonymize.sql.").action(async (opts) => {
57
+ const sourcePath = String(opts.source);
58
+ const model = await loadModel(sourcePath);
59
+ let candidates = pii.detectPiiCandidates(model);
60
+ if (opts.ai) {
61
+ candidates = await pii.verifyWithAi(candidates, {
62
+ completeFn: async (prompt) => {
63
+ const r = await ai.complete([{ role: "user", content: prompt }], {
64
+ feature: "pii.mask.verify"
65
+ });
66
+ return r.text;
67
+ }
68
+ });
69
+ }
70
+ if (candidates.length === 0) {
71
+ logger.warn("No PII columns detected.");
72
+ return;
73
+ }
74
+ const sql = pii.renderAnonymizeScript(candidates, {
75
+ unmaskedRole: String(opts.unmaskedRole ?? "PROD_ETL")
76
+ });
77
+ const outPath = opts.out ? path.resolve(String(opts.out)) : path.resolve("anonymize.sql");
78
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
79
+ await fs.writeFile(outPath, sql, "utf8");
80
+ logger.success(`Wrote ${outPath} (${candidates.length} candidate(s), ${sql.length} bytes).`);
81
+ });
82
+ return cmd;
83
+ }
84
+ function renderTable(cands) {
85
+ if (cands.length === 0) return "No PII candidates detected.\n";
86
+ const lines = [];
87
+ const fqnW = Math.max(8, ...cands.map((c) => c.fqn.length));
88
+ const colW = Math.max(8, ...cands.map((c) => c.column.length));
89
+ const catW = Math.max(8, ...cands.map((c) => c.category.length));
90
+ lines.push(
91
+ `${"fqn".padEnd(fqnW)} ${"column".padEnd(colW)} ${"category".padEnd(catW)} confidence reason`
92
+ );
93
+ lines.push(`${"-".repeat(fqnW)} ${"-".repeat(colW)} ${"-".repeat(catW)} ---------- ------`);
94
+ for (const c of cands) {
95
+ lines.push(
96
+ `${c.fqn.padEnd(fqnW)} ${c.column.padEnd(colW)} ${c.category.padEnd(catW)} ${c.confidence.padEnd(10)} ${c.reason}`
97
+ );
98
+ }
99
+ return lines.join("\n");
100
+ }
101
+ function renderMarkdown(cands) {
102
+ if (cands.length === 0) return "_No PII candidates detected._";
103
+ const lines = [];
104
+ lines.push(`# PII candidates (${cands.length})`);
105
+ lines.push("");
106
+ lines.push("| FQN | Column | Category | Confidence | Reason |");
107
+ lines.push("|---|---|---|---|---|");
108
+ for (const c of cands) {
109
+ lines.push(
110
+ `| \`${c.fqn}\` | \`${c.column}\` | ${c.category} | ${c.confidence} | ${c.reason} |`
111
+ );
112
+ }
113
+ return lines.join("\n");
114
+ }
115
+ async function emit(payload, out) {
116
+ if (out) {
117
+ const p = path.resolve(String(out));
118
+ await fs.mkdir(path.dirname(p), { recursive: true });
119
+ await fs.writeFile(p, payload + (payload.endsWith("\n") ? "" : "\n"), "utf8");
120
+ console.error(`Wrote ${p}.`);
121
+ } else {
122
+ process.stdout.write(payload + (payload.endsWith("\n") ? "" : "\n"));
123
+ }
124
+ }
125
+ async function loadModel(sourcePath) {
126
+ if (sourcePath.endsWith(".sdtpac")) {
127
+ const c = await pac.readPac(sourcePath);
128
+ return c.model;
129
+ }
130
+ const loaded = await project.loadProject(sourcePath);
131
+ return await project.parseProjectModel(loaded);
132
+ }
133
+ export {
134
+ piiCommand
135
+ };
136
+ //# sourceMappingURL=pii-FBDRDQ2E.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/pii.ts"],"sourcesContent":["/**\n * `sdt pii` — scan a project for PII columns and optionally generate\n * masking-policy DDL. Wraps `@sdt-tools/core/pii`.\n *\n * Subcommands:\n * sdt pii scan — list candidates (table | json | markdown)\n * sdt pii mask — render the anonymisation script\n *\n * The `--ai` flag on `scan` routes ambiguous candidates through the\n * configured AI provider for a second opinion (uses\n * `@sdt-tools/core/ai.complete()`).\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { ai, pac, pii, project } from '@sdt-tools/core';\nimport { logger } from '../util/logger.js';\n\nexport function piiCommand(): Command {\n const cmd = new Command('pii');\n cmd.description(\n 'Detect PII columns + render Snowflake MASKING POLICY DDL. See `sdt pii scan` and `sdt pii mask`.',\n );\n\n cmd\n .command('scan')\n .description('Scan the project for PII column candidates.')\n .requiredOption('--source <path>', '.sdtproj or .sdtpac 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 Snowflake MASKING POLICY DDL for detected PII columns.')\n .requiredOption('--source <path>', '.sdtproj or .sdtpac to scan.')\n .option(\n '--unmasked-role <name>',\n \"Role that sees the unmasked value. Default 'PROD_ETL'.\",\n 'PROD_ETL',\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(async (opts: { source: string; unmaskedRole?: string; ai?: boolean; out?: string }) => {\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 logger.warn('No PII columns detected.');\n return;\n }\n const sql = pii.renderAnonymizeScript(candidates, {\n unmaskedRole: String(opts.unmaskedRole ?? 'PROD_ETL'),\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 logger.success(`Wrote ${outPath} (${candidates.length} candidate(s), ${sql.length} bytes).`);\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) {\n if (sourcePath.endsWith('.sdtpac')) {\n const c = await pac.readPac(sourcePath);\n return c.model;\n }\n const loaded = await project.loadProject(sourcePath);\n return await project.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 "@sdt-tools/core";
9
+ function pilotCommand() {
10
+ const cmd = new Command("pilot").description(
11
+ "Join or check the SDT pilot program (90-day Pro access for early adopters)."
12
+ );
13
+ cmd.command("register").description("Register for the SDT pilot program.").requiredOption("--email <addr>", "Your email address.").action(async (opts) => {
14
+ logger.info("Registering for the SDT 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 ~/.sdt/license.jwt.");
18
+ logger.info("Run `sdt 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-CCQERKPH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/pilot.ts"],"sourcesContent":["/**\n * `sdt pilot register|status` — manage pilot program registration.\n * Mirrors `ddt pilot` in shape (PIL.2).\n */\nimport { Command } from 'commander';\nimport { license } from '@sdt-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 SDT pilot program (90-day Pro access for early adopters).',\n );\n\n cmd\n .command('register')\n .description('Register for the SDT pilot program.')\n .requiredOption('--email <addr>', 'Your email address.')\n .action(async (opts: { email: string }) => {\n logger.info('Registering for the SDT 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 ~/.sdt/license.jwt.');\n logger.info('Run `sdt 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,79 @@
1
+ import {
2
+ attachExplainFlag,
3
+ runExplain
4
+ } from "./chunk-ZWY4ZRHL.js";
5
+ import {
6
+ attachRelatedOptions
7
+ } from "./chunk-EWXM4KJN.js";
8
+ import "./chunk-VM2H4LAO.js";
9
+ import "./chunk-DGUM43GV.js";
10
+
11
+ // src/commands/pr-comment.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
+ ProjectSource
19
+ } from "@sdt-tools/core/compare";
20
+ import { safety } from "@sdt-tools/core";
21
+ import { pac, project, review } from "@sdt-tools/core";
22
+ function prCommentCommand() {
23
+ const cmd = new Command("pr-comment");
24
+ cmd.description(
25
+ "Generate a Markdown PR comment from a source\u2194target compare (diff + safety + health)."
26
+ ).requiredOption("--source <path>", ".sdtproj or .sdtpac (the desired state).").requiredOption("--target <path>", ".sdtproj or .sdtpac (the current state).").option("-o, --out <path>", "Output file path. Defaults to stdout.").action(async (opts) => {
27
+ const sourcePath = String(opts.source);
28
+ const targetPath = String(opts.target);
29
+ const engine = new CompareEngine();
30
+ const src = sourcePath.endsWith(".sdtpac") ? new PacSource(sourcePath, "source") : new ProjectSource(sourcePath, "source");
31
+ const tgt = targetPath.endsWith(".sdtpac") ? new PacSource(targetPath, "target") : new ProjectSource(targetPath, "target");
32
+ const result = await engine.compare(src, tgt);
33
+ const assessment = safety.assess(result);
34
+ const model = await loadModel(sourcePath);
35
+ const md = review.renderPrComment(model, result, assessment, { source: sourcePath });
36
+ if (opts.out) {
37
+ const p = path.resolve(String(opts.out));
38
+ await fs.mkdir(path.dirname(p), { recursive: true });
39
+ await fs.writeFile(p, md + (md.endsWith("\n") ? "" : "\n"), "utf8");
40
+ console.error(`Wrote ${p} (${md.length} bytes).`);
41
+ } else {
42
+ process.stdout.write(md + (md.endsWith("\n") ? "" : "\n"));
43
+ }
44
+ await runExplain(
45
+ {
46
+ feature: "pr-comment.explain",
47
+ 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."
48
+ },
49
+ opts,
50
+ () => `PR comment payload follows:
51
+
52
+ ${md}
53
+
54
+ Write a tight executive summary a busy reviewer will appreciate.`
55
+ );
56
+ });
57
+ attachExplainFlag(cmd);
58
+ attachRelatedOptions(cmd, [
59
+ "compare.ignoreCase",
60
+ "compare.ignoreComments",
61
+ "compare.ignoreFormattingDifferences",
62
+ "compare.excludeObjectTypes",
63
+ "compare.excludeObjectPatterns",
64
+ "compare.includeObjectPatterns"
65
+ ]);
66
+ return cmd;
67
+ }
68
+ async function loadModel(sourcePath) {
69
+ if (sourcePath.endsWith(".sdtpac")) {
70
+ const c = await pac.readPac(sourcePath);
71
+ return c.model;
72
+ }
73
+ const loaded = await project.loadProject(sourcePath);
74
+ return await project.parseProjectModel(loaded);
75
+ }
76
+ export {
77
+ prCommentCommand
78
+ };
79
+ //# sourceMappingURL=pr-comment-S5FF4QRX.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 type CompareSource,\n} from '@sdt-tools/core/compare';\nimport { safety } from '@sdt-tools/core';\nimport { pac, project, review } from '@sdt-tools/core';\nimport { attachExplainFlag, runExplain } from '../util/ai-explain.js';\nimport { attachRelatedOptions } from '../util/help-catalog.js';\n\n/**\n * `sdt pr-comment` — produce a Markdown PR sticky-comment from a\n * source↔target compare. Bundles the diff summary, reversibility-\n * grouped safety findings, project health diagnostics, and a\n * collapsible per-object changelog into a single deliverable a CI\n * step can POST as a pull-request comment.\n *\n * Distinct from `sdt review`: `review` is project-only, `pr-comment`\n * is change-driven (here's what's *in this PR*).\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>', '.sdtproj or .sdtpac (the desired state).')\n .requiredOption('--target <path>', '.sdtproj or .sdtpac (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('.sdtpac')\n ? new PacSource(sourcePath, 'source')\n : new ProjectSource(sourcePath, 'source');\n const tgt: CompareSource = targetPath.endsWith('.sdtpac')\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) {\n if (sourcePath.endsWith('.sdtpac')) {\n const c = await pac.readPac(sourcePath);\n return c.model;\n }\n const loaded = await project.loadProject(sourcePath);\n return await project.parseProjectModel(loaded);\n}\n"],"mappings":";;;;;;;;;;;AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,cAAc;AACvB,SAAS,KAAK,SAAS,cAAc;AAc9B,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,YAAoB;AAC3C,MAAI,WAAW,SAAS,SAAS,GAAG;AAClC,UAAM,IAAI,MAAM,IAAI,QAAQ,UAAU;AACtC,WAAO,EAAE;AAAA,EACX;AACA,QAAM,SAAS,MAAM,QAAQ,YAAY,UAAU;AACnD,SAAO,MAAM,QAAQ,kBAAkB,MAAM;AAC/C;","names":[]}
@@ -0,0 +1,47 @@
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 } from "@sdt-tools/core";
8
+ import { getProfile, SnowflakeConnection } from "@sdt-tools/core/connection";
9
+ function previewCommand() {
10
+ return new Command("preview").description(
11
+ "Preview rows from a table (SELECT * FROM <fqn> LIMIT <n>). Read-only \u2014 requires a connection profile."
12
+ ).argument("<fqn>", "Fully-qualified object name (DB.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) => {
13
+ const limit = Number(opts.limit ?? "100") || 100;
14
+ const where = opts.where ? String(opts.where) : void 0;
15
+ const sql = preview.buildPreviewSql(fqn, { limit, ...where ? { where } : {} });
16
+ if (opts.sqlOnly === true) {
17
+ process.stdout.write(sql + "\n");
18
+ return;
19
+ }
20
+ const profile = await getProfile(String(opts.connection));
21
+ if (!profile) {
22
+ throw new Error(`Connection profile "${String(opts.connection)}" not found.`);
23
+ }
24
+ const conn = new SnowflakeConnection(profile);
25
+ await conn.connect();
26
+ try {
27
+ const result = await preview.previewRows(conn, fqn, {
28
+ limit,
29
+ ...where ? { where } : {}
30
+ });
31
+ const text = String(opts.format ?? "table").toLowerCase() === "json" ? JSON.stringify(result, null, 2) : preview.renderPreviewTable(result);
32
+ if (opts.output) {
33
+ const outPath = path.resolve(String(opts.output));
34
+ await fs.writeFile(outPath, text + "\n", "utf8");
35
+ console.error(`preview: wrote ${result.rows.length} row(s) to ${outPath}`);
36
+ } else {
37
+ process.stdout.write(text + "\n");
38
+ }
39
+ } finally {
40
+ await conn.disconnect();
41
+ }
42
+ });
43
+ }
44
+ export {
45
+ previewCommand
46
+ };
47
+ //# sourceMappingURL=preview-5U4YVCRM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/preview.ts"],"sourcesContent":["/**\n * `sdt preview <fqn>` — DCM compatibility item 3.\n *\n * Sample-data inspector. SELECT * FROM <fqn> LIMIT <n> against a\n * connection profile, rendered as table or JSON.\n *\n * Mirrors DCM's `EXECUTE DCM PROJECT <n> PREVIEW`.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { preview } from '@sdt-tools/core';\nimport { getProfile, SnowflakeConnection } from '@sdt-tools/core/connection';\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 (DB.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 = new SnowflakeConnection(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":";;;AAQA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB,SAAS,eAAe;AACxB,SAAS,YAAY,2BAA2B;AAEzC,SAAS,iBAA0B;AACxC,SAAO,IAAI,QAAQ,SAAS,EACzB;AAAA,IACC;AAAA,EAEF,EACC,SAAS,SAAS,gDAAgD,EAClE,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,IAAI,oBAAoB,OAAO;AAC5C,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":[]}