@sdt-tools/cli 0.2.0 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advise-tests-6DRSZMBL.js +87 -0
- package/dist/advise-tests-6DRSZMBL.js.map +1 -0
- package/dist/ai-G4MJWHTM.js +89 -0
- package/dist/ai-G4MJWHTM.js.map +1 -0
- package/dist/anonymize-QR6JGXA7.js +123 -0
- package/dist/anonymize-QR6JGXA7.js.map +1 -0
- package/dist/approval-YVHYTV53.js +73 -0
- package/dist/approval-YVHYTV53.js.map +1 -0
- package/dist/approval-chain-54KKJZS3.js +120 -0
- package/dist/approval-chain-54KKJZS3.js.map +1 -0
- package/dist/audit-log-QZFH7LUX.js +159 -0
- package/dist/audit-log-QZFH7LUX.js.map +1 -0
- package/dist/backlog-V2YUIQDL.js +76 -0
- package/dist/backlog-V2YUIQDL.js.map +1 -0
- package/dist/bisect-GEVYAVL5.js +111 -0
- package/dist/bisect-GEVYAVL5.js.map +1 -0
- package/dist/bookmarks-57LKS7P6.js +107 -0
- package/dist/bookmarks-57LKS7P6.js.map +1 -0
- package/dist/branch-W2MGMPSH.js +88 -0
- package/dist/branch-W2MGMPSH.js.map +1 -0
- package/dist/build-VNIQFKSP.js +23 -0
- package/dist/build-VNIQFKSP.js.map +1 -0
- package/dist/catalog-JLB5VCEV.js +137 -0
- package/dist/catalog-JLB5VCEV.js.map +1 -0
- package/dist/changelog-M7XGDYSY.js +220 -0
- package/dist/changelog-M7XGDYSY.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-EWXM4KJN.js +25 -0
- package/dist/chunk-EWXM4KJN.js.map +1 -0
- package/dist/chunk-JP2EZLR5.js +50 -0
- package/dist/chunk-JP2EZLR5.js.map +1 -0
- package/dist/chunk-VM2H4LAO.js +15 -0
- package/dist/chunk-VM2H4LAO.js.map +1 -0
- package/dist/chunk-ZWY4ZRHL.js +44 -0
- package/dist/chunk-ZWY4ZRHL.js.map +1 -0
- package/dist/cli.js +506 -19014
- package/dist/cli.js.map +1 -1
- package/dist/compare-5O6UTWPJ.js +405 -0
- package/dist/compare-5O6UTWPJ.js.map +1 -0
- package/dist/compare-profiles-7ZSNIW7B.js +218 -0
- package/dist/compare-profiles-7ZSNIW7B.js.map +1 -0
- package/dist/completion-I5U5VVAX.js +82 -0
- package/dist/completion-I5U5VVAX.js.map +1 -0
- package/dist/connection-SYTH4V53.js +110 -0
- package/dist/connection-SYTH4V53.js.map +1 -0
- package/dist/cost-estimate-TJDDH6TO.js +328 -0
- package/dist/cost-estimate-TJDDH6TO.js.map +1 -0
- package/dist/data-compare-UK2UXAS3.js +134 -0
- package/dist/data-compare-UK2UXAS3.js.map +1 -0
- package/dist/data-fit-Q45ENBRL.js +125 -0
- package/dist/data-fit-Q45ENBRL.js.map +1 -0
- package/dist/deploy-status-UUHKVDTI.js +58 -0
- package/dist/deploy-status-UUHKVDTI.js.map +1 -0
- package/dist/design-PO6UPBL7.js +138 -0
- package/dist/design-PO6UPBL7.js.map +1 -0
- package/dist/diagnose-6IFMELFR.js +145 -0
- package/dist/diagnose-6IFMELFR.js.map +1 -0
- package/dist/discover-A7OSZAHK.js +78 -0
- package/dist/discover-A7OSZAHK.js.map +1 -0
- package/dist/docs-CVRKGUSW.js +177 -0
- package/dist/docs-CVRKGUSW.js.map +1 -0
- package/dist/drift-XDA3BDYN.js +226 -0
- package/dist/drift-XDA3BDYN.js.map +1 -0
- package/dist/drift-gate-V7QSIOGZ.js +94 -0
- package/dist/drift-gate-V7QSIOGZ.js.map +1 -0
- package/dist/error-lookup-7ZWCZJ44.js +56 -0
- package/dist/error-lookup-7ZWCZJ44.js.map +1 -0
- package/dist/errorReporting-ZRNJ3VW7.js +109 -0
- package/dist/errorReporting-ZRNJ3VW7.js.map +1 -0
- package/dist/exec-PKBHLI7T.js +121 -0
- package/dist/exec-PKBHLI7T.js.map +1 -0
- package/dist/explain-LWKJOTL7.js +192 -0
- package/dist/explain-LWKJOTL7.js.map +1 -0
- package/dist/explorer-QOVM6VBD.js +61 -0
- package/dist/explorer-QOVM6VBD.js.map +1 -0
- package/dist/export-IYYBZ5HE.js +42 -0
- package/dist/export-IYYBZ5HE.js.map +1 -0
- package/dist/extract-VMMVRQVT.js +102 -0
- package/dist/extract-VMMVRQVT.js.map +1 -0
- package/dist/features-LE6BDZ2S.js +59 -0
- package/dist/features-LE6BDZ2S.js.map +1 -0
- package/dist/feedback-M7DM2EQC.js +161 -0
- package/dist/feedback-M7DM2EQC.js.map +1 -0
- package/dist/find-EME2JG2I.js +176 -0
- package/dist/find-EME2JG2I.js.map +1 -0
- package/dist/format-TRLWLMGS.js +141 -0
- package/dist/format-TRLWLMGS.js.map +1 -0
- package/dist/generate-6NAZGZDV.js +152 -0
- package/dist/generate-6NAZGZDV.js.map +1 -0
- package/dist/graph-QNQDAUO7.js +161 -0
- package/dist/graph-QNQDAUO7.js.map +1 -0
- package/dist/history-RONA7ZTI.js +199 -0
- package/dist/history-RONA7ZTI.js.map +1 -0
- package/dist/hosts-YBXY2ZG5.js +49 -0
- package/dist/hosts-YBXY2ZG5.js.map +1 -0
- package/dist/impact-T2JSANHS.js +59 -0
- package/dist/impact-T2JSANHS.js.map +1 -0
- package/dist/import-AELYLY6A.js +32 -0
- package/dist/import-AELYLY6A.js.map +1 -0
- package/dist/index.cjs +36 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +60 -25
- package/dist/index.js.map +1 -1
- package/dist/init-SWRRJMGI.js +57 -0
- package/dist/init-SWRRJMGI.js.map +1 -0
- package/dist/install-hooks-6SIAGTAF.js +109 -0
- package/dist/install-hooks-6SIAGTAF.js.map +1 -0
- package/dist/license-OAF22PLZ.js +46 -0
- package/dist/license-OAF22PLZ.js.map +1 -0
- package/dist/lineage-EW66XJ6O.js +552 -0
- package/dist/lineage-EW66XJ6O.js.map +1 -0
- package/dist/lint-FQ2OTYTQ.js +143 -0
- package/dist/lint-FQ2OTYTQ.js.map +1 -0
- package/dist/mcp-3QI4TH4N.js +344 -0
- package/dist/mcp-3QI4TH4N.js.map +1 -0
- package/dist/migrate-from-dbt-JVTXPWKQ.js +156 -0
- package/dist/migrate-from-dbt-JVTXPWKQ.js.map +1 -0
- package/dist/migrate-platform-NTRTOGNR.js +91 -0
- package/dist/migrate-platform-NTRTOGNR.js.map +1 -0
- package/dist/optimize-CJYWMAWA.js +105 -0
- package/dist/optimize-CJYWMAWA.js.map +1 -0
- package/dist/perf-LL2CPCJF.js +205 -0
- package/dist/perf-LL2CPCJF.js.map +1 -0
- package/dist/pii-FBDRDQ2E.js +136 -0
- package/dist/pii-FBDRDQ2E.js.map +1 -0
- package/dist/pilot-CCQERKPH.js +29 -0
- package/dist/pilot-CCQERKPH.js.map +1 -0
- package/dist/pr-comment-S5FF4QRX.js +79 -0
- package/dist/pr-comment-S5FF4QRX.js.map +1 -0
- package/dist/preview-5U4YVCRM.js +47 -0
- package/dist/preview-5U4YVCRM.js.map +1 -0
- package/dist/profile-7VC57KD2.js +101 -0
- package/dist/profile-7VC57KD2.js.map +1 -0
- package/dist/promote-AASEFTIA.js +408 -0
- package/dist/promote-AASEFTIA.js.map +1 -0
- package/dist/publish-Y2J56K4Y.js +715 -0
- package/dist/publish-Y2J56K4Y.js.map +1 -0
- package/dist/purge-QMXZKCMD.js +57 -0
- package/dist/purge-QMXZKCMD.js.map +1 -0
- package/dist/query-log-6OM4GI7W.js +112 -0
- package/dist/query-log-6OM4GI7W.js.map +1 -0
- package/dist/refactor-LTZQLJ35.js +5799 -0
- package/dist/refactor-LTZQLJ35.js.map +1 -0
- package/dist/refresh-4TY2AGOU.js +38 -0
- package/dist/refresh-4TY2AGOU.js.map +1 -0
- package/dist/replay-OOC25FZN.js +117 -0
- package/dist/replay-OOC25FZN.js.map +1 -0
- package/dist/revert-ODMUVJW6.js +110 -0
- package/dist/revert-ODMUVJW6.js.map +1 -0
- package/dist/review-XXPWOBFP.js +158 -0
- package/dist/review-XXPWOBFP.js.map +1 -0
- package/dist/rollback-suggest-6G2HEKFR.js +79 -0
- package/dist/rollback-suggest-6G2HEKFR.js.map +1 -0
- package/dist/safer-alternative-QFVNLG3L.js +89 -0
- package/dist/safer-alternative-QFVNLG3L.js.map +1 -0
- package/dist/safety-7QWRSUEZ.js +168 -0
- package/dist/safety-7QWRSUEZ.js.map +1 -0
- package/dist/savings-RHIXP6IT.js +95 -0
- package/dist/savings-RHIXP6IT.js.map +1 -0
- package/dist/scan-secrets-5YCQ4UCU.js +54 -0
- package/dist/scan-secrets-5YCQ4UCU.js.map +1 -0
- package/dist/schema-CIZXCQD2.js +429 -0
- package/dist/schema-CIZXCQD2.js.map +1 -0
- package/dist/script-K7CIN2P6.js +153 -0
- package/dist/script-K7CIN2P6.js.map +1 -0
- package/dist/search-BUZ5NXZZ.js +151 -0
- package/dist/search-BUZ5NXZZ.js.map +1 -0
- package/dist/seed-76QAK276.js +96 -0
- package/dist/seed-76QAK276.js.map +1 -0
- package/dist/sketch-PTLKDIK3.js +88 -0
- package/dist/sketch-PTLKDIK3.js.map +1 -0
- package/dist/snapshot-XLPR2OZ5.js +177 -0
- package/dist/snapshot-XLPR2OZ5.js.map +1 -0
- package/dist/snippets-EK4DK5CN.js +74 -0
- package/dist/snippets-EK4DK5CN.js.map +1 -0
- package/dist/standards-7T2UY6DD.js +241 -0
- package/dist/standards-7T2UY6DD.js.map +1 -0
- package/dist/suggest-VGRYSAR6.js +39 -0
- package/dist/suggest-VGRYSAR6.js.map +1 -0
- package/dist/suggest-constraints-MY5WKUHA.js +160 -0
- package/dist/suggest-constraints-MY5WKUHA.js.map +1 -0
- package/dist/suite-TRNGZWQM.js +88 -0
- package/dist/suite-TRNGZWQM.js.map +1 -0
- package/dist/telemetry-3U2QLA2S.js +75 -0
- package/dist/telemetry-3U2QLA2S.js.map +1 -0
- package/dist/template-ZERIXVXF.js +403 -0
- package/dist/template-ZERIXVXF.js.map +1 -0
- package/dist/test-5M2ED3WT.js +169 -0
- package/dist/test-5M2ED3WT.js.map +1 -0
- package/dist/trial-U732FONV.js +31 -0
- package/dist/trial-U732FONV.js.map +1 -0
- package/dist/validate-T6D2WCOK.js +106 -0
- package/dist/validate-T6D2WCOK.js.map +1 -0
- package/dist/verify-KXVASEEG.js +76 -0
- package/dist/verify-KXVASEEG.js.map +1 -0
- package/dist/watch-I6K4BNMA.js +80 -0
- package/dist/watch-I6K4BNMA.js.map +1 -0
- package/dist/xcompare-TPFLQO6W.js +87 -0
- package/dist/xcompare-TPFLQO6W.js.map +1 -0
- package/package.json +2 -2
- package/dist/cli.cjs +0 -19040
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.cts +0 -1
- package/dist/cli.d.ts +0 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logger
|
|
3
|
+
} from "./chunk-VM2H4LAO.js";
|
|
4
|
+
import "./chunk-DGUM43GV.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/format.ts
|
|
7
|
+
import { promises as fs } from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import {
|
|
11
|
+
FORMAT_STYLE_PRESET_NAMES,
|
|
12
|
+
format,
|
|
13
|
+
getFormatPreset,
|
|
14
|
+
isFormatStylePreset,
|
|
15
|
+
mergeFormatStyle
|
|
16
|
+
} from "@sdt-tools/core/format";
|
|
17
|
+
function formatCommand() {
|
|
18
|
+
return new Command("format").description(
|
|
19
|
+
"Format SQL files using the tokenizer-based v2 engine (refuse-on-invalid, idempotent, token-faithful)."
|
|
20
|
+
).option("--check", "Exit with code 1 if any file is not formatted.").option(
|
|
21
|
+
"--engine <v>",
|
|
22
|
+
'Formatter engine: "v2" (default; tokenizer-based) | "legacy" (deprecated alias of v2).',
|
|
23
|
+
"v2"
|
|
24
|
+
).option(
|
|
25
|
+
"-p, --project <path>",
|
|
26
|
+
"Path to the .sdtproj (used to find the file set + load formatStyle)."
|
|
27
|
+
).option("--in-place", "Write formatted output back to the file. Default: stdout.", false).option(
|
|
28
|
+
"--dbt",
|
|
29
|
+
"Force dbt-mode on: pre-mask {{ ... }} / {% ... %} Jinja blocks so they survive byte-for-byte across the format pass. Default: auto-detect."
|
|
30
|
+
).option(
|
|
31
|
+
"--no-dbt",
|
|
32
|
+
"Force dbt-mode off: feed Jinja text directly to the formatter (almost always wrong on dbt models; opt-out for power users with pre-compiled SQL)."
|
|
33
|
+
).option(
|
|
34
|
+
"--style <preset>",
|
|
35
|
+
`Apply a named layout preset before any project-level overrides: ${FORMAT_STYLE_PRESET_NAMES.join(" | ")}. compact = single-line where possible (120-col, 0 blank lines, sameLine closers); expanded = each clause on its own line (100-col, 1 blank line, aligned closers); wide = expanded with maxLineWidth 140 for ultrawide terminals.`
|
|
36
|
+
).argument("[files...]", "Specific files to format. Required if --project is not given.").action(
|
|
37
|
+
async (files, opts) => {
|
|
38
|
+
const engine = String(opts.engine ?? "v2").toLowerCase();
|
|
39
|
+
if (engine !== "v2" && engine !== "legacy") {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Unknown --engine: ${opts.engine}. Use "v2" or "legacy" (deprecated alias).`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (engine === "legacy") {
|
|
45
|
+
logger.warn(
|
|
46
|
+
"--engine legacy is a deprecated alias of --engine v2 and will be removed in a future major. Running v2."
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const targets = await collectTargets(files, opts.project);
|
|
50
|
+
if (targets.length === 0) {
|
|
51
|
+
logger.error("No files to format. Pass file paths as arguments or --project <path>.");
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const dbtMode = opts.dbt === true ? "on" : opts.dbt === false ? "off" : void 0;
|
|
56
|
+
let preset;
|
|
57
|
+
if (opts.style) {
|
|
58
|
+
const styleName = String(opts.style).toLowerCase();
|
|
59
|
+
if (!isFormatStylePreset(styleName)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`--style must be one of: ${FORMAT_STYLE_PRESET_NAMES.join(", ")}. Got '${opts.style}'.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
preset = getFormatPreset(styleName);
|
|
65
|
+
}
|
|
66
|
+
const overrides = dbtMode ? { experimental: { dbtMode } } : {};
|
|
67
|
+
const style = mergeFormatStyle(preset, overrides);
|
|
68
|
+
let changedCount = 0;
|
|
69
|
+
let refusedCount = 0;
|
|
70
|
+
for (const file of targets) {
|
|
71
|
+
const input = await fs.readFile(file, "utf8");
|
|
72
|
+
const result = format(input, style);
|
|
73
|
+
if (result.refused) {
|
|
74
|
+
logger.error(
|
|
75
|
+
`\u2717 ${file} \u2014 refused (${result.diagnostics.filter((d) => d.severity === "ERROR").length} error(s)):`
|
|
76
|
+
);
|
|
77
|
+
for (const d of result.diagnostics.filter((d2) => d2.severity === "ERROR")) {
|
|
78
|
+
logger.error(` L${d.line}:${d.column} [${d.code}] ${d.message}`);
|
|
79
|
+
}
|
|
80
|
+
refusedCount++;
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (opts.check) {
|
|
85
|
+
if (result.changed) {
|
|
86
|
+
logger.warn(`! ${file} \u2014 would reformat`);
|
|
87
|
+
changedCount++;
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
} else {
|
|
90
|
+
logger.info(` ${file} \u2014 ok`);
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (opts.inPlace) {
|
|
95
|
+
if (result.changed) {
|
|
96
|
+
await fs.writeFile(file, result.output, "utf8");
|
|
97
|
+
logger.info(`\u2713 ${file} \u2014 reformatted`);
|
|
98
|
+
changedCount++;
|
|
99
|
+
} else {
|
|
100
|
+
logger.info(` ${file} \u2014 already formatted`);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
process.stdout.write(result.output);
|
|
104
|
+
if (!result.output.endsWith("\n")) process.stdout.write("\n");
|
|
105
|
+
}
|
|
106
|
+
for (const d of result.diagnostics.filter((d2) => d2.severity === "WARNING")) {
|
|
107
|
+
logger.warn(` L${d.line}:${d.column} [${d.code}] ${d.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (opts.check && changedCount === 0 && refusedCount === 0) {
|
|
111
|
+
logger.info(`All ${targets.length} file(s) already formatted.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
async function collectTargets(files, project) {
|
|
117
|
+
if (files.length > 0) {
|
|
118
|
+
return files.map((f) => path.resolve(f));
|
|
119
|
+
}
|
|
120
|
+
if (!project) return [];
|
|
121
|
+
const root = path.dirname(path.resolve(project));
|
|
122
|
+
const out = [];
|
|
123
|
+
await walk(root, out);
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
async function walk(dir, out) {
|
|
127
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
128
|
+
for (const e of entries) {
|
|
129
|
+
if (e.name === "node_modules" || e.name === "dist" || e.name.startsWith(".")) continue;
|
|
130
|
+
const full = path.join(dir, e.name);
|
|
131
|
+
if (e.isDirectory()) {
|
|
132
|
+
await walk(full, out);
|
|
133
|
+
} else if (e.isFile() && e.name.endsWith(".sql")) {
|
|
134
|
+
out.push(full);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
formatCommand
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=format-TRLWLMGS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/format.ts"],"sourcesContent":["/**\n * `sdt format` — normalize SQL formatting in project source files.\n *\n * Safety properties (see @sdt-tools/core/format):\n * 1. Pre-format validation: refuses to touch files whose SQL doesn't\n * tokenize (unclosed string / comment / dollar-quote / unbalanced\n * parens). Diagnostics are reported with line/column.\n * 2. Functional identity: every non-whitespace token in the input\n * appears in the output, in the same order, with identical text —\n * except KEYWORD/BUILTIN tokens whose case may be normalized per\n * the configured style.\n * 3. Idempotence: format(format(x)) === format(x).\n *\n * `--engine legacy` is retained as a deprecated alias of `v2` so existing\n * scripts keep working; it emits a one-line deprecation notice and then\n * runs v2. Will be removed in a future major.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport {\n FORMAT_STYLE_PRESET_NAMES,\n format,\n getFormatPreset,\n isFormatStylePreset,\n mergeFormatStyle,\n} from '@sdt-tools/core/format';\nimport type { FormatStyle } from '@sdt-tools/core/format';\nimport { logger } from '../util/logger.js';\n\nexport function formatCommand(): Command {\n return new Command('format')\n .description(\n 'Format SQL files using the tokenizer-based v2 engine (refuse-on-invalid, idempotent, token-faithful).',\n )\n .option('--check', 'Exit with code 1 if any file is not formatted.')\n .option(\n '--engine <v>',\n 'Formatter engine: \"v2\" (default; tokenizer-based) | \"legacy\" (deprecated alias of v2).',\n 'v2',\n )\n .option(\n '-p, --project <path>',\n 'Path to the .sdtproj (used to find the file set + load formatStyle).',\n )\n .option('--in-place', 'Write formatted output back to the file. Default: stdout.', false)\n .option(\n '--dbt',\n 'Force dbt-mode on: pre-mask {{ ... }} / {% ... %} Jinja blocks so they survive byte-for-byte across the format pass. Default: auto-detect.',\n )\n .option(\n '--no-dbt',\n 'Force dbt-mode off: feed Jinja text directly to the formatter (almost always wrong on dbt models; opt-out for power users with pre-compiled SQL).',\n )\n .option(\n '--style <preset>',\n `Apply a named layout preset before any project-level overrides: ${FORMAT_STYLE_PRESET_NAMES.join(' | ')}. compact = single-line where possible (120-col, 0 blank lines, sameLine closers); expanded = each clause on its own line (100-col, 1 blank line, aligned closers); wide = expanded with maxLineWidth 140 for ultrawide terminals.`,\n )\n .argument('[files...]', 'Specific files to format. Required if --project is not given.')\n .action(\n async (\n files: string[],\n opts: {\n check?: boolean;\n engine?: string;\n project?: string;\n inPlace?: boolean;\n dbt?: boolean;\n style?: string;\n },\n ) => {\n const engine = String(opts.engine ?? 'v2').toLowerCase();\n if (engine !== 'v2' && engine !== 'legacy') {\n throw new Error(\n `Unknown --engine: ${opts.engine}. Use \"v2\" or \"legacy\" (deprecated alias).`,\n );\n }\n if (engine === 'legacy') {\n logger.warn(\n '--engine legacy is a deprecated alias of --engine v2 and will be removed in a future major. Running v2.',\n );\n }\n\n // v2 engine path.\n const targets = await collectTargets(files, opts.project);\n if (targets.length === 0) {\n logger.error('No files to format. Pass file paths as arguments or --project <path>.');\n process.exitCode = 1;\n return;\n }\n\n // Project-level style override comes from .sdtproj (formatStyle field).\n // For now, just use defaults — the v0 engine only honors casing.\n // DBTC.4 — --dbt / --no-dbt resolves to FormatStyle.experimental.dbtMode.\n // Commander's --no-dbt yields opts.dbt === false; absent flag yields\n // undefined (default auto-detect).\n const dbtMode: 'auto' | 'on' | 'off' | undefined =\n opts.dbt === true ? 'on' : opts.dbt === false ? 'off' : undefined;\n let preset: FormatStyle | undefined;\n if (opts.style) {\n const styleName = String(opts.style).toLowerCase();\n if (!isFormatStylePreset(styleName)) {\n throw new Error(\n `--style must be one of: ${FORMAT_STYLE_PRESET_NAMES.join(', ')}. Got '${opts.style}'.`,\n );\n }\n preset = getFormatPreset(styleName);\n }\n const overrides: FormatStyle = dbtMode ? { experimental: { dbtMode } } : {};\n const style: FormatStyle = mergeFormatStyle(preset, overrides);\n\n let changedCount = 0;\n let refusedCount = 0;\n for (const file of targets) {\n const input = await fs.readFile(file, 'utf8');\n const result = format(input, style);\n if (result.refused) {\n logger.error(\n `✗ ${file} — refused (${result.diagnostics.filter((d) => d.severity === 'ERROR').length} error(s)):`,\n );\n for (const d of result.diagnostics.filter((d) => d.severity === 'ERROR')) {\n logger.error(` L${d.line}:${d.column} [${d.code}] ${d.message}`);\n }\n refusedCount++;\n process.exitCode = 1;\n continue;\n }\n if (opts.check) {\n if (result.changed) {\n logger.warn(`! ${file} — would reformat`);\n changedCount++;\n process.exitCode = 1;\n } else {\n logger.info(` ${file} — ok`);\n }\n continue;\n }\n if (opts.inPlace) {\n if (result.changed) {\n await fs.writeFile(file, result.output, 'utf8');\n logger.info(`✓ ${file} — reformatted`);\n changedCount++;\n } else {\n logger.info(` ${file} — already formatted`);\n }\n } else {\n // Default: print to stdout. Useful for pipelines.\n process.stdout.write(result.output);\n if (!result.output.endsWith('\\n')) process.stdout.write('\\n');\n }\n for (const d of result.diagnostics.filter((d) => d.severity === 'WARNING')) {\n logger.warn(` L${d.line}:${d.column} [${d.code}] ${d.message}`);\n }\n }\n\n if (opts.check && changedCount === 0 && refusedCount === 0) {\n logger.info(`All ${targets.length} file(s) already formatted.`);\n }\n },\n );\n}\n\nasync function collectTargets(files: string[], project?: string): Promise<string[]> {\n if (files.length > 0) {\n return files.map((f) => path.resolve(f));\n }\n if (!project) return [];\n // Minimal project-aware mode: walk the project root for *.sql files.\n // Full project-aware glob handling is deferred to a follow-up — for\n // now, scan the directory containing the .sdtproj.\n const root = path.dirname(path.resolve(project));\n const out: string[] = [];\n await walk(root, out);\n return out;\n}\n\nasync function walk(dir: string, out: string[]): Promise<void> {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n for (const e of entries) {\n if (e.name === 'node_modules' || e.name === 'dist' || e.name.startsWith('.')) continue;\n const full = path.join(dir, e.name);\n if (e.isDirectory()) {\n await walk(full, out);\n } else if (e.isFile() && e.name.endsWith('.sql')) {\n out.push(full);\n }\n }\n}\n"],"mappings":";;;;;;AAiBA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAIA,SAAS,gBAAyB;AACvC,SAAO,IAAI,QAAQ,QAAQ,EACxB;AAAA,IACC;AAAA,EACF,EACC,OAAO,WAAW,gDAAgD,EAClE;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC,OAAO,cAAc,6DAA6D,KAAK,EACvF;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA,mEAAmE,0BAA0B,KAAK,KAAK,CAAC;AAAA,EAC1G,EACC,SAAS,cAAc,+DAA+D,EACtF;AAAA,IACC,OACE,OACA,SAQG;AACH,YAAM,SAAS,OAAO,KAAK,UAAU,IAAI,EAAE,YAAY;AACvD,UAAI,WAAW,QAAQ,WAAW,UAAU;AAC1C,cAAM,IAAI;AAAA,UACR,qBAAqB,KAAK,MAAM;AAAA,QAClC;AAAA,MACF;AACA,UAAI,WAAW,UAAU;AACvB,eAAO;AAAA,UACL;AAAA,QACF;AAAA,MACF;AAGA,YAAM,UAAU,MAAM,eAAe,OAAO,KAAK,OAAO;AACxD,UAAI,QAAQ,WAAW,GAAG;AACxB,eAAO,MAAM,uEAAuE;AACpF,gBAAQ,WAAW;AACnB;AAAA,MACF;AAOA,YAAM,UACJ,KAAK,QAAQ,OAAO,OAAO,KAAK,QAAQ,QAAQ,QAAQ;AAC1D,UAAI;AACJ,UAAI,KAAK,OAAO;AACd,cAAM,YAAY,OAAO,KAAK,KAAK,EAAE,YAAY;AACjD,YAAI,CAAC,oBAAoB,SAAS,GAAG;AACnC,gBAAM,IAAI;AAAA,YACR,2BAA2B,0BAA0B,KAAK,IAAI,CAAC,UAAU,KAAK,KAAK;AAAA,UACrF;AAAA,QACF;AACA,iBAAS,gBAAgB,SAAS;AAAA,MACpC;AACA,YAAM,YAAyB,UAAU,EAAE,cAAc,EAAE,QAAQ,EAAE,IAAI,CAAC;AAC1E,YAAM,QAAqB,iBAAiB,QAAQ,SAAS;AAE7D,UAAI,eAAe;AACnB,UAAI,eAAe;AACnB,iBAAW,QAAQ,SAAS;AAC1B,cAAM,QAAQ,MAAM,GAAG,SAAS,MAAM,MAAM;AAC5C,cAAM,SAAS,OAAO,OAAO,KAAK;AAClC,YAAI,OAAO,SAAS;AAClB,iBAAO;AAAA,YACL,UAAK,IAAI,oBAAe,OAAO,YAAY,OAAO,CAAC,MAAM,EAAE,aAAa,OAAO,EAAE,MAAM;AAAA,UACzF;AACA,qBAAW,KAAK,OAAO,YAAY,OAAO,CAACA,OAAMA,GAAE,aAAa,OAAO,GAAG;AACxE,mBAAO,MAAM,QAAQ,EAAE,IAAI,IAAI,EAAE,MAAM,KAAK,EAAE,IAAI,KAAK,EAAE,OAAO,EAAE;AAAA,UACpE;AACA;AACA,kBAAQ,WAAW;AACnB;AAAA,QACF;AACA,YAAI,KAAK,OAAO;AACd,cAAI,OAAO,SAAS;AAClB,mBAAO,KAAK,KAAK,IAAI,wBAAmB;AACxC;AACA,oBAAQ,WAAW;AAAA,UACrB,OAAO;AACL,mBAAO,KAAK,KAAK,IAAI,YAAO;AAAA,UAC9B;AACA;AAAA,QACF;AACA,YAAI,KAAK,SAAS;AAChB,cAAI,OAAO,SAAS;AAClB,kBAAM,GAAG,UAAU,MAAM,OAAO,QAAQ,MAAM;AAC9C,mBAAO,KAAK,UAAK,IAAI,qBAAgB;AACrC;AAAA,UACF,OAAO;AACL,mBAAO,KAAK,KAAK,IAAI,2BAAsB;AAAA,UAC7C;AAAA,QACF,OAAO;AAEL,kBAAQ,OAAO,MAAM,OAAO,MAAM;AAClC,cAAI,CAAC,OAAO,OAAO,SAAS,IAAI,EAAG,SAAQ,OAAO,MAAM,IAAI;AAAA,QAC9D;AACA,mBAAW,KAAK,OAAO,YAAY,OAAO,CAACA,OAAMA,GAAE,aAAa,SAAS,GAAG;AAC1E,iBAAO,KAAK,QAAQ,EAAE,IAAI,IAAI,EAAE,MAAM,KAAK,EAAE,IAAI,KAAK,EAAE,OAAO,EAAE;AAAA,QACnE;AAAA,MACF;AAEA,UAAI,KAAK,SAAS,iBAAiB,KAAK,iBAAiB,GAAG;AAC1D,eAAO,KAAK,OAAO,QAAQ,MAAM,6BAA6B;AAAA,MAChE;AAAA,IACF;AAAA,EACF;AACJ;AAEA,eAAe,eAAe,OAAiB,SAAqC;AAClF,MAAI,MAAM,SAAS,GAAG;AACpB,WAAO,MAAM,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC;AAAA,EACzC;AACA,MAAI,CAAC,QAAS,QAAO,CAAC;AAItB,QAAM,OAAO,KAAK,QAAQ,KAAK,QAAQ,OAAO,CAAC;AAC/C,QAAM,MAAgB,CAAC;AACvB,QAAM,KAAK,MAAM,GAAG;AACpB,SAAO;AACT;AAEA,eAAe,KAAK,KAAa,KAA8B;AAC7D,QAAM,UAAU,MAAM,GAAG,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;AAC7D,aAAW,KAAK,SAAS;AACvB,QAAI,EAAE,SAAS,kBAAkB,EAAE,SAAS,UAAU,EAAE,KAAK,WAAW,GAAG,EAAG;AAC9E,UAAM,OAAO,KAAK,KAAK,KAAK,EAAE,IAAI;AAClC,QAAI,EAAE,YAAY,GAAG;AACnB,YAAM,KAAK,MAAM,GAAG;AAAA,IACtB,WAAW,EAAE,OAAO,KAAK,EAAE,KAAK,SAAS,MAAM,GAAG;AAChD,UAAI,KAAK,IAAI;AAAA,IACf;AAAA,EACF;AACF;","names":["d"]}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/generate.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { generate, deploy } from "@sdt-tools/core";
|
|
8
|
+
function notebookSubcommand() {
|
|
9
|
+
return new Command("notebook").description("Emit a Snowpark Python ingest notebook from a JSON sample file.").requiredOption("--from-json <path>", "Path to a JSON data file (array of records or NDJSON).").requiredOption("--target <fqn>", "Target table FQN (DB.SCHEMA.TABLE).").requiredOption("--source <path>", "Source path for the read (stage path or S3 URI).").option(
|
|
10
|
+
"--runtime <ver>",
|
|
11
|
+
"Snowpark runtime version tag (e.g. snowpark-python-1.22).",
|
|
12
|
+
"snowpark-python-1.x"
|
|
13
|
+
).option("--merge-schema", "Emit ENABLE_SCHEMA_EVOLUTION comment.", false).option("--write-mode <mode>", "append | overwrite (default append).", "append").option("--out <dir>", "Write .ipynb + .py to this directory instead of stdout.").action(async (opts) => {
|
|
14
|
+
const rows = await readJsonRows(opts.fromJson);
|
|
15
|
+
const schema = await generate.inferTypes(rows);
|
|
16
|
+
const result = generate.emitIngestNotebook(schema, {
|
|
17
|
+
targetFqn: opts.target,
|
|
18
|
+
sourcePath: opts.source,
|
|
19
|
+
runtime: opts.runtime,
|
|
20
|
+
mergeSchema: Boolean(opts.mergeSchema),
|
|
21
|
+
writeMode: opts.writeMode === "overwrite" ? "overwrite" : "append"
|
|
22
|
+
});
|
|
23
|
+
if (opts.out) {
|
|
24
|
+
await fs.mkdir(path.resolve(opts.out), { recursive: true });
|
|
25
|
+
const stem = sanitizeFileStem(opts.target);
|
|
26
|
+
await fs.writeFile(
|
|
27
|
+
path.join(path.resolve(opts.out), `ingest_${stem}.ipynb`),
|
|
28
|
+
JSON.stringify(result.ipynb, null, 2) + "\n",
|
|
29
|
+
"utf8"
|
|
30
|
+
);
|
|
31
|
+
await fs.writeFile(
|
|
32
|
+
path.join(path.resolve(opts.out), `ingest_${stem}.py`),
|
|
33
|
+
result.py,
|
|
34
|
+
"utf8"
|
|
35
|
+
);
|
|
36
|
+
} else {
|
|
37
|
+
process.stdout.write(result.py);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function scdSubcommand() {
|
|
42
|
+
return new Command("scd").description("Emit Snowflake MERGE SQL for a given table and SCD strategy.").requiredOption("--table <fqn>", "Target table FQN (DB.SCHEMA.TABLE).").requiredOption("--natural-key <cols>", "Comma-separated natural key columns.").option("--type <n>", "SCD type (1 | 2, default 2).", "2").option("--effective-from <col>", "Effective-from timestamp column.", "EFFECTIVE_FROM").option("--effective-to <col>", "Effective-to timestamp column.", "EFFECTIVE_TO").option("--is-current <col>", "Is-current boolean column (optional).").option("--hash-cols <cols>", "Comma-separated columns for hash-based change detection.").option("--surrogate-key <col>", "Surrogate key column (SCD-2 only).", "ID").option(
|
|
43
|
+
"--on-schema-change <action>",
|
|
44
|
+
"add-column | fail | rebuild (default add-column).",
|
|
45
|
+
"add-column"
|
|
46
|
+
).option("--out <dir>", "Write .sql file to this directory instead of stdout.").action(async (opts) => {
|
|
47
|
+
const naturalKey = String(opts.naturalKey).split(",").map((s) => s.trim()).filter(Boolean);
|
|
48
|
+
if (naturalKey.length === 0) {
|
|
49
|
+
throw new Error("--natural-key must list at least one column name.");
|
|
50
|
+
}
|
|
51
|
+
const scdType = parseInt(opts.type, 10);
|
|
52
|
+
if (scdType !== 1 && scdType !== 2) {
|
|
53
|
+
throw new Error(`--type must be 1 or 2 (got '${opts.type}').`);
|
|
54
|
+
}
|
|
55
|
+
const hashCols = opts.hashCols ? String(opts.hashCols).split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
56
|
+
const strategy = {
|
|
57
|
+
type: scdType,
|
|
58
|
+
naturalKey,
|
|
59
|
+
surrogateKeyColumn: scdType === 2 ? opts.surrogateKey : void 0,
|
|
60
|
+
effectiveFromColumn: opts.effectiveFrom,
|
|
61
|
+
effectiveToColumn: opts.effectiveTo,
|
|
62
|
+
isCurrentColumn: opts.isCurrent,
|
|
63
|
+
hashColumns: hashCols,
|
|
64
|
+
onSchemaChange: opts.onSchemaChange
|
|
65
|
+
};
|
|
66
|
+
const sql = deploy.emitScdMerge(strategy, {
|
|
67
|
+
targetFqn: opts.table,
|
|
68
|
+
sourceFqn: `STG.STG_${sanitizeFileStem(opts.table).toUpperCase()}`,
|
|
69
|
+
projectionColumns: naturalKey
|
|
70
|
+
});
|
|
71
|
+
if (opts.out) {
|
|
72
|
+
const stem = sanitizeFileStem(opts.table);
|
|
73
|
+
await fs.mkdir(path.resolve(opts.out), { recursive: true });
|
|
74
|
+
await fs.writeFile(path.join(path.resolve(opts.out), `scd_${stem}.sql`), sql, "utf8");
|
|
75
|
+
} else {
|
|
76
|
+
process.stdout.write(sql + "\n");
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function starPipelineSubcommand() {
|
|
81
|
+
return new Command("star-pipeline").description("Emit SCD-2 dim + FK-enrichment fact scaffolds from a designStar result.").requiredOption("--from-design <path>", "Path to a DesignStarResult JSON file.").option("--format <fmt>", "pyspark | sparksql | snowpark (default snowpark).", "snowpark").option("--schema <catalog.schema>", "Target catalog.schema prefix (default DW.PUBLIC).").option("--out <dir>", "Write pipeline files to this directory instead of stdout.").action(async (opts) => {
|
|
82
|
+
const raw = await fs.readFile(path.resolve(opts.fromDesign), "utf8");
|
|
83
|
+
const parsed = JSON.parse(raw);
|
|
84
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || !("factName" in parsed) || !Array.isArray(parsed.dimensions)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`--from-design must point to a DesignStarResult JSON (fields: factName, dimensions, ...).`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const design = {
|
|
90
|
+
measureColumns: [],
|
|
91
|
+
assumptions: [],
|
|
92
|
+
proposedInvocation: "",
|
|
93
|
+
needsReview: true,
|
|
94
|
+
rawModelText: "",
|
|
95
|
+
parseFailed: false,
|
|
96
|
+
...parsed
|
|
97
|
+
};
|
|
98
|
+
const fmt = opts.format ?? "snowpark";
|
|
99
|
+
const validFormats = ["pyspark", "sparksql", "snowpark"];
|
|
100
|
+
if (!validFormats.includes(fmt)) {
|
|
101
|
+
throw new Error(`--format must be pyspark | sparksql | snowpark (got '${opts.format}').`);
|
|
102
|
+
}
|
|
103
|
+
const result = generate.emitStarPipeline(design, {
|
|
104
|
+
format: fmt,
|
|
105
|
+
targetSchema: opts.schema ?? "DW.PUBLIC"
|
|
106
|
+
});
|
|
107
|
+
if (opts.out) {
|
|
108
|
+
const outDir = path.resolve(opts.out);
|
|
109
|
+
for (const [relPath, content] of result.files) {
|
|
110
|
+
const dest = path.join(outDir, relPath);
|
|
111
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
112
|
+
await fs.writeFile(dest, content, "utf8");
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
for (const [relPath, content] of result.files) {
|
|
116
|
+
process.stdout.write(`-- ${relPath} --
|
|
117
|
+
`);
|
|
118
|
+
process.stdout.write(content + "\n\n");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function generateCommand() {
|
|
124
|
+
const cmd = new Command("generate");
|
|
125
|
+
cmd.description("Code generation: ingest notebooks, SCD merge scripts, star-schema pipelines.");
|
|
126
|
+
cmd.addCommand(notebookSubcommand());
|
|
127
|
+
cmd.addCommand(scdSubcommand());
|
|
128
|
+
cmd.addCommand(starPipelineSubcommand());
|
|
129
|
+
return cmd;
|
|
130
|
+
}
|
|
131
|
+
async function readJsonRows(filePath) {
|
|
132
|
+
const raw = await fs.readFile(path.resolve(filePath), "utf8");
|
|
133
|
+
const trimmed = raw.trim();
|
|
134
|
+
if (trimmed.startsWith("[")) {
|
|
135
|
+
const parsed = JSON.parse(raw);
|
|
136
|
+
if (!Array.isArray(parsed)) throw new Error(`${filePath} must be a JSON array of objects.`);
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
return trimmed.split("\n").filter((l) => l.trim().length > 0).map((l, i) => {
|
|
140
|
+
const obj = JSON.parse(l);
|
|
141
|
+
if (obj === null || typeof obj !== "object" || Array.isArray(obj))
|
|
142
|
+
throw new Error(`Line ${i + 1} of ${filePath} is not a JSON object.`);
|
|
143
|
+
return obj;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function sanitizeFileStem(fqn) {
|
|
147
|
+
return fqn.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
148
|
+
}
|
|
149
|
+
export {
|
|
150
|
+
generateCommand
|
|
151
|
+
};
|
|
152
|
+
//# sourceMappingURL=generate-6NAZGZDV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/generate.ts"],"sourcesContent":["/**\n * `sdt generate` — code generation CLI (GENPY.5).\n *\n * Three subcommands wiring GENPY.1/3/4 substrates onto the operator surface:\n *\n * sdt generate notebook --from-json <path> --target <fqn> --source <path>\n * [--runtime <ver>] [--merge-schema] [--write-mode append|overwrite]\n * [--out <dir>]\n *\n * sdt generate scd --table <fqn> --natural-key <cols> [--type 1|2]\n * [--effective-from <col>] [--effective-to <col>]\n * [--is-current <col>] [--hash-cols <cols>]\n * [--surrogate-key <col>] [--out <dir>]\n *\n * sdt generate star-pipeline --from-design <path>\n * [--format pyspark|sparksql|snowpark]\n * [--schema <catalog.schema>] [--out <dir>]\n *\n * Mirrors `Databricks/packages/cli/src/commands/generate.ts`.\n */\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { generate, deploy } from '@sdt-tools/core';\n\n// ── notebook ──────────────────────────────────────────────────────────────────\n\nfunction notebookSubcommand(): Command {\n return new Command('notebook')\n .description('Emit a Snowpark Python ingest notebook from a JSON sample file.')\n .requiredOption('--from-json <path>', 'Path to a JSON data file (array of records or NDJSON).')\n .requiredOption('--target <fqn>', 'Target table FQN (DB.SCHEMA.TABLE).')\n .requiredOption('--source <path>', 'Source path for the read (stage path or S3 URI).')\n .option(\n '--runtime <ver>',\n 'Snowpark runtime version tag (e.g. snowpark-python-1.22).',\n 'snowpark-python-1.x',\n )\n .option('--merge-schema', 'Emit ENABLE_SCHEMA_EVOLUTION comment.', false)\n .option('--write-mode <mode>', 'append | overwrite (default append).', 'append')\n .option('--out <dir>', 'Write .ipynb + .py to this directory instead of stdout.')\n .action(async (opts) => {\n const rows = await readJsonRows(opts.fromJson);\n const schema = await generate.inferTypes(rows);\n const result = generate.emitIngestNotebook(schema, {\n targetFqn: opts.target,\n sourcePath: opts.source,\n runtime: opts.runtime,\n mergeSchema: Boolean(opts.mergeSchema),\n writeMode: opts.writeMode === 'overwrite' ? 'overwrite' : 'append',\n });\n\n if (opts.out) {\n await fs.mkdir(path.resolve(opts.out), { recursive: true });\n const stem = sanitizeFileStem(opts.target);\n await fs.writeFile(\n path.join(path.resolve(opts.out), `ingest_${stem}.ipynb`),\n JSON.stringify(result.ipynb, null, 2) + '\\n',\n 'utf8',\n );\n await fs.writeFile(\n path.join(path.resolve(opts.out), `ingest_${stem}.py`),\n result.py,\n 'utf8',\n );\n } else {\n process.stdout.write(result.py);\n }\n });\n}\n\n// ── scd ───────────────────────────────────────────────────────────────────────\n\nfunction scdSubcommand(): Command {\n return new Command('scd')\n .description('Emit Snowflake MERGE SQL for a given table and SCD strategy.')\n .requiredOption('--table <fqn>', 'Target table FQN (DB.SCHEMA.TABLE).')\n .requiredOption('--natural-key <cols>', 'Comma-separated natural key columns.')\n .option('--type <n>', 'SCD type (1 | 2, default 2).', '2')\n .option('--effective-from <col>', 'Effective-from timestamp column.', 'EFFECTIVE_FROM')\n .option('--effective-to <col>', 'Effective-to timestamp column.', 'EFFECTIVE_TO')\n .option('--is-current <col>', 'Is-current boolean column (optional).')\n .option('--hash-cols <cols>', 'Comma-separated columns for hash-based change detection.')\n .option('--surrogate-key <col>', 'Surrogate key column (SCD-2 only).', 'ID')\n .option(\n '--on-schema-change <action>',\n 'add-column | fail | rebuild (default add-column).',\n 'add-column',\n )\n .option('--out <dir>', 'Write .sql file to this directory instead of stdout.')\n .action(async (opts) => {\n const naturalKey = String(opts.naturalKey)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (naturalKey.length === 0) {\n throw new Error('--natural-key must list at least one column name.');\n }\n\n const scdType = parseInt(opts.type, 10);\n if (scdType !== 1 && scdType !== 2) {\n throw new Error(`--type must be 1 or 2 (got '${opts.type}').`);\n }\n\n const hashCols = opts.hashCols\n ? String(opts.hashCols)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n : undefined;\n\n const strategy: deploy.SCDStrategy = {\n type: scdType as 1 | 2,\n naturalKey,\n surrogateKeyColumn: scdType === 2 ? opts.surrogateKey : undefined,\n effectiveFromColumn: opts.effectiveFrom,\n effectiveToColumn: opts.effectiveTo,\n isCurrentColumn: opts.isCurrent,\n hashColumns: hashCols,\n onSchemaChange: opts.onSchemaChange as 'add-column' | 'fail' | 'rebuild',\n };\n\n const sql = deploy.emitScdMerge(strategy, {\n targetFqn: opts.table,\n sourceFqn: `STG.STG_${sanitizeFileStem(opts.table).toUpperCase()}`,\n projectionColumns: naturalKey,\n });\n\n if (opts.out) {\n const stem = sanitizeFileStem(opts.table);\n await fs.mkdir(path.resolve(opts.out), { recursive: true });\n await fs.writeFile(path.join(path.resolve(opts.out), `scd_${stem}.sql`), sql, 'utf8');\n } else {\n process.stdout.write(sql + '\\n');\n }\n });\n}\n\n// ── star-pipeline ─────────────────────────────────────────────────────────────\n\nfunction starPipelineSubcommand(): Command {\n return new Command('star-pipeline')\n .description('Emit SCD-2 dim + FK-enrichment fact scaffolds from a designStar result.')\n .requiredOption('--from-design <path>', 'Path to a DesignStarResult JSON file.')\n .option('--format <fmt>', 'pyspark | sparksql | snowpark (default snowpark).', 'snowpark')\n .option('--schema <catalog.schema>', 'Target catalog.schema prefix (default DW.PUBLIC).')\n .option('--out <dir>', 'Write pipeline files to this directory instead of stdout.')\n .action(async (opts) => {\n const raw = await fs.readFile(path.resolve(opts.fromDesign), 'utf8');\n const parsed: unknown = JSON.parse(raw);\n if (\n parsed === null ||\n typeof parsed !== 'object' ||\n Array.isArray(parsed) ||\n !('factName' in parsed) ||\n !Array.isArray((parsed as Record<string, unknown>).dimensions)\n ) {\n throw new Error(\n `--from-design must point to a DesignStarResult JSON (fields: factName, dimensions, ...).`,\n );\n }\n const design = {\n measureColumns: [],\n assumptions: [],\n proposedInvocation: '',\n needsReview: true as const,\n rawModelText: '',\n parseFailed: false,\n ...(parsed as Record<string, unknown>),\n } as unknown as Parameters<typeof generate.emitStarPipeline>[0];\n\n const fmt = (opts.format ?? 'snowpark') as generate.StarPipelineFormat;\n const validFormats: generate.StarPipelineFormat[] = ['pyspark', 'sparksql', 'snowpark'];\n if (!validFormats.includes(fmt)) {\n throw new Error(`--format must be pyspark | sparksql | snowpark (got '${opts.format}').`);\n }\n\n const result = generate.emitStarPipeline(design, {\n format: fmt,\n targetSchema: opts.schema ?? 'DW.PUBLIC',\n });\n\n if (opts.out) {\n const outDir = path.resolve(opts.out);\n for (const [relPath, content] of result.files) {\n const dest = path.join(outDir, relPath);\n await fs.mkdir(path.dirname(dest), { recursive: true });\n await fs.writeFile(dest, content, 'utf8');\n }\n } else {\n for (const [relPath, content] of result.files) {\n process.stdout.write(`-- ${relPath} --\\n`);\n process.stdout.write(content + '\\n\\n');\n }\n }\n });\n}\n\n// ── public factory ────────────────────────────────────────────────────────────\n\nexport function generateCommand(): Command {\n const cmd = new Command('generate');\n cmd.description('Code generation: ingest notebooks, SCD merge scripts, star-schema pipelines.');\n cmd.addCommand(notebookSubcommand());\n cmd.addCommand(scdSubcommand());\n cmd.addCommand(starPipelineSubcommand());\n return cmd;\n}\n\n// ── helpers ───────────────────────────────────────────────────────────────────\n\nasync function readJsonRows(filePath: string): Promise<Record<string, unknown>[]> {\n const raw = await fs.readFile(path.resolve(filePath), 'utf8');\n const trimmed = raw.trim();\n if (trimmed.startsWith('[')) {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) throw new Error(`${filePath} must be a JSON array of objects.`);\n return parsed as Record<string, unknown>[];\n }\n // NDJSON\n return trimmed\n .split('\\n')\n .filter((l) => l.trim().length > 0)\n .map((l, i) => {\n const obj: unknown = JSON.parse(l);\n if (obj === null || typeof obj !== 'object' || Array.isArray(obj))\n throw new Error(`Line ${i + 1} of ${filePath} is not a JSON object.`);\n return obj as Record<string, unknown>;\n });\n}\n\n/** `DW.PUBLIC.FACT_SALES` → `dw_public_fact_sales` */\nfunction sanitizeFileStem(fqn: string): string {\n return fqn\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '_')\n .replace(/^_|_$/g, '');\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attachExplainFlag,
|
|
3
|
+
runExplain
|
|
4
|
+
} from "./chunk-ZWY4ZRHL.js";
|
|
5
|
+
import "./chunk-VM2H4LAO.js";
|
|
6
|
+
import "./chunk-DGUM43GV.js";
|
|
7
|
+
|
|
8
|
+
// src/commands/graph.ts
|
|
9
|
+
import { promises as fs } from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { Command } from "commander";
|
|
12
|
+
import { compare, pac, project } from "@sdt-tools/core";
|
|
13
|
+
function graphCommand() {
|
|
14
|
+
const cmd = new Command("graph");
|
|
15
|
+
cmd.description("Build an object-dependency DAG and emit it as Mermaid or DOT.").requiredOption("--source <path>", ".sdtproj or .sdtpac to analyze.").option("-o, --out <path>", "Output file path. Defaults to stdout.").option(
|
|
16
|
+
"--format <fmt>",
|
|
17
|
+
"mermaid | dot | md (Mermaid wrapped in a markdown fenced block)",
|
|
18
|
+
"mermaid"
|
|
19
|
+
).option("--json", "Emit the raw { nodes, edges } JSON instead of a renderer.", false).option(
|
|
20
|
+
"--compare-to <path>",
|
|
21
|
+
"A second .sdtproj or .sdtpac. When set, emit added / removed / changed nodes + edges (the DAG delta) instead of the static graph."
|
|
22
|
+
).action(async (opts) => {
|
|
23
|
+
const model = await loadModel(String(opts.source));
|
|
24
|
+
const graph = compare.buildDependencyGraph(model);
|
|
25
|
+
let diff;
|
|
26
|
+
if (opts.compareTo) {
|
|
27
|
+
const otherModel = await loadModel(String(opts.compareTo));
|
|
28
|
+
const otherGraph = compare.buildDependencyGraph(otherModel);
|
|
29
|
+
diff = compare.diffDependencyGraphs(otherGraph, graph);
|
|
30
|
+
}
|
|
31
|
+
const fmt = String(opts.format ?? "mermaid").toLowerCase();
|
|
32
|
+
let payload;
|
|
33
|
+
if (opts.json) {
|
|
34
|
+
payload = JSON.stringify(diff ?? graph, null, 2);
|
|
35
|
+
} else if (diff) {
|
|
36
|
+
payload = renderDiffMarkdown(diff);
|
|
37
|
+
} else if (fmt === "mermaid") {
|
|
38
|
+
payload = compare.renderGraphMermaid(graph);
|
|
39
|
+
} else if (fmt === "dot") {
|
|
40
|
+
payload = compare.renderGraphDot(graph);
|
|
41
|
+
} else if (fmt === "md") {
|
|
42
|
+
payload = `# Schema dependency graph
|
|
43
|
+
|
|
44
|
+
${graph.nodes.length} node(s), ${graph.edges.length} edge(s).
|
|
45
|
+
|
|
46
|
+
\`\`\`mermaid
|
|
47
|
+
${compare.renderGraphMermaid(graph)}
|
|
48
|
+
\`\`\`
|
|
49
|
+
`;
|
|
50
|
+
} else {
|
|
51
|
+
throw new Error(`Unknown --format: ${fmt}. Use mermaid | dot | md.`);
|
|
52
|
+
}
|
|
53
|
+
if (opts.out) {
|
|
54
|
+
const out = path.resolve(String(opts.out));
|
|
55
|
+
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
56
|
+
await fs.writeFile(out, payload + (payload.endsWith("\n") ? "" : "\n"), "utf8");
|
|
57
|
+
console.error(
|
|
58
|
+
`Wrote ${out} (${payload.length} bytes, ${graph.nodes.length} nodes, ${graph.edges.length} edges).`
|
|
59
|
+
);
|
|
60
|
+
} else {
|
|
61
|
+
process.stdout.write(payload + (payload.endsWith("\n") ? "" : "\n"));
|
|
62
|
+
}
|
|
63
|
+
await runExplain(
|
|
64
|
+
{
|
|
65
|
+
feature: "graph.explain",
|
|
66
|
+
systemPrompt: diff ? "You are a data-platform architect narrating a dependency-graph DELTA between two snapshots. Walk through added / removed / changed nodes and edges in plain English. Call out structural shifts (new sources, dropped consumers, type changes) and likely-but-not-yet-broken downstream consequences." : "You are a data-platform architect explaining a dependency graph. Describe the dominant data-flow shape, point out hot or fragile nodes, and recommend any decomposition the graph suggests."
|
|
67
|
+
},
|
|
68
|
+
opts,
|
|
69
|
+
() => diff ? buildDiffPrompt(diff) : buildGraphPrompt(graph)
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
attachExplainFlag(cmd);
|
|
73
|
+
return cmd;
|
|
74
|
+
}
|
|
75
|
+
function renderDiffMarkdown(d) {
|
|
76
|
+
const lines = ["# Dependency-graph delta", ""];
|
|
77
|
+
lines.push(
|
|
78
|
+
`**Summary**: ${d.summary.nodeDelta >= 0 ? "+" : ""}${d.summary.nodeDelta} nodes, ${d.summary.edgeDelta >= 0 ? "+" : ""}${d.summary.edgeDelta} edges. ${d.addedNodes.length} added node(s), ${d.removedNodes.length} removed node(s), ${d.changedNodes.length} changed node(s).`
|
|
79
|
+
);
|
|
80
|
+
lines.push("");
|
|
81
|
+
if (d.addedNodes.length > 0) {
|
|
82
|
+
lines.push("## Added nodes");
|
|
83
|
+
for (const n of d.addedNodes) lines.push(`- \`${n.fqn}\` (${n.objectType})`);
|
|
84
|
+
lines.push("");
|
|
85
|
+
}
|
|
86
|
+
if (d.removedNodes.length > 0) {
|
|
87
|
+
lines.push("## Removed nodes");
|
|
88
|
+
for (const n of d.removedNodes) lines.push(`- \`${n.fqn}\` (${n.objectType})`);
|
|
89
|
+
lines.push("");
|
|
90
|
+
}
|
|
91
|
+
if (d.changedNodes.length > 0) {
|
|
92
|
+
lines.push("## Changed nodes (objectType)");
|
|
93
|
+
for (const c of d.changedNodes) lines.push(`- \`${c.fqn}\`: ${c.before} \u2192 ${c.after}`);
|
|
94
|
+
lines.push("");
|
|
95
|
+
}
|
|
96
|
+
if (d.addedEdges.length > 0) {
|
|
97
|
+
lines.push("## Added edges");
|
|
98
|
+
for (const e of d.addedEdges) lines.push(`- \`${e.from}\` \u2192 \`${e.to}\` (${e.kind})`);
|
|
99
|
+
lines.push("");
|
|
100
|
+
}
|
|
101
|
+
if (d.removedEdges.length > 0) {
|
|
102
|
+
lines.push("## Removed edges");
|
|
103
|
+
for (const e of d.removedEdges) lines.push(`- \`${e.from}\` \u2192 \`${e.to}\` (${e.kind})`);
|
|
104
|
+
}
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
function buildGraphPrompt(g) {
|
|
108
|
+
const sample = g.edges.slice(0, 40).map((e) => ` - ${e.from} -> ${e.to}`);
|
|
109
|
+
return [
|
|
110
|
+
`Dependency graph: ${g.nodes.length} nodes, ${g.edges.length} edges.`,
|
|
111
|
+
"",
|
|
112
|
+
"Edges (up to 40):",
|
|
113
|
+
...sample,
|
|
114
|
+
"",
|
|
115
|
+
"Narrate this graph in plain English."
|
|
116
|
+
].join("\n");
|
|
117
|
+
}
|
|
118
|
+
function buildDiffPrompt(d) {
|
|
119
|
+
const lines = [];
|
|
120
|
+
lines.push(
|
|
121
|
+
`Graph delta: ${d.summary.nodeDelta >= 0 ? "+" : ""}${d.summary.nodeDelta} nodes, ${d.summary.edgeDelta >= 0 ? "+" : ""}${d.summary.edgeDelta} edges.`
|
|
122
|
+
);
|
|
123
|
+
if (d.addedNodes.length > 0) {
|
|
124
|
+
lines.push("", `Added nodes (${d.addedNodes.length}, up to 20):`);
|
|
125
|
+
for (const n of d.addedNodes.slice(0, 20)) lines.push(` + ${n.fqn} (${n.objectType})`);
|
|
126
|
+
}
|
|
127
|
+
if (d.removedNodes.length > 0) {
|
|
128
|
+
lines.push("", `Removed nodes (${d.removedNodes.length}, up to 20):`);
|
|
129
|
+
for (const n of d.removedNodes.slice(0, 20)) lines.push(` - ${n.fqn} (${n.objectType})`);
|
|
130
|
+
}
|
|
131
|
+
if (d.changedNodes.length > 0) {
|
|
132
|
+
lines.push("", `Changed nodes (${d.changedNodes.length}):`);
|
|
133
|
+
for (const c of d.changedNodes.slice(0, 20))
|
|
134
|
+
lines.push(` ~ ${c.fqn}: ${c.before} -> ${c.after}`);
|
|
135
|
+
}
|
|
136
|
+
if (d.addedEdges.length > 0) {
|
|
137
|
+
lines.push("", `Added edges (${d.addedEdges.length}, up to 20):`);
|
|
138
|
+
for (const e of d.addedEdges.slice(0, 20)) lines.push(` + ${e.from} -> ${e.to}`);
|
|
139
|
+
}
|
|
140
|
+
if (d.removedEdges.length > 0) {
|
|
141
|
+
lines.push("", `Removed edges (${d.removedEdges.length}, up to 20):`);
|
|
142
|
+
for (const e of d.removedEdges.slice(0, 20)) lines.push(` - ${e.from} -> ${e.to}`);
|
|
143
|
+
}
|
|
144
|
+
lines.push(
|
|
145
|
+
"",
|
|
146
|
+
"Narrate this delta in plain English. Highlight structural shifts and likely downstream consequences."
|
|
147
|
+
);
|
|
148
|
+
return lines.join("\n");
|
|
149
|
+
}
|
|
150
|
+
async function loadModel(sourcePath) {
|
|
151
|
+
if (sourcePath.endsWith(".sdtpac")) {
|
|
152
|
+
const c = await pac.readPac(sourcePath);
|
|
153
|
+
return c.model;
|
|
154
|
+
}
|
|
155
|
+
const loaded = await project.loadProject(sourcePath);
|
|
156
|
+
return await project.parseProjectModel(loaded);
|
|
157
|
+
}
|
|
158
|
+
export {
|
|
159
|
+
graphCommand
|
|
160
|
+
};
|
|
161
|
+
//# sourceMappingURL=graph-QNQDAUO7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/graph.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { compare, pac, project } from '@sdt-tools/core';\nimport { attachExplainFlag, runExplain } from '../util/ai-explain.js';\n\n/**\n * `sdt graph` — emit an object-dependency DAG (Mermaid or DOT) from a\n * `.sdtproj` or `.sdtpac`. Lets reviewers see blast radius before a\n * drop. Inspired by DBeaver's \"Object References\" panel — but at the\n * cross-object level rather than column level.\n *\n * With `--compare-to <other-source>`, diffs the two graphs and (when\n * `--explain` is on) narrates the DAG changes in plain English — the\n * AI Phase 6 \"lineage explanation\" surface.\n *\n * Mirrors `ddt graph`.\n */\nexport function graphCommand(): Command {\n const cmd = new Command('graph');\n cmd\n .description('Build an object-dependency DAG and emit it as Mermaid or DOT.')\n .requiredOption('--source <path>', '.sdtproj or .sdtpac to analyze.')\n .option('-o, --out <path>', 'Output file path. Defaults to stdout.')\n .option(\n '--format <fmt>',\n 'mermaid | dot | md (Mermaid wrapped in a markdown fenced block)',\n 'mermaid',\n )\n .option('--json', 'Emit the raw { nodes, edges } JSON instead of a renderer.', false)\n .option(\n '--compare-to <path>',\n 'A second .sdtproj or .sdtpac. When set, emit added / removed / changed nodes + edges (the DAG delta) instead of the static graph.',\n )\n .action(async (opts) => {\n const model = await loadModel(String(opts.source));\n const graph = compare.buildDependencyGraph(model);\n\n let diff: compare.DependencyGraphDiff | undefined;\n if (opts.compareTo) {\n const otherModel = await loadModel(String(opts.compareTo));\n const otherGraph = compare.buildDependencyGraph(otherModel);\n // Source = \"after\", compareTo = \"before\" → the user is asking\n // \"what did the source side add vs the reference snapshot?\"\n diff = compare.diffDependencyGraphs(otherGraph, graph);\n }\n\n const fmt = String(opts.format ?? 'mermaid').toLowerCase();\n let payload: string;\n if (opts.json) {\n payload = JSON.stringify(diff ?? graph, null, 2);\n } else if (diff) {\n payload = renderDiffMarkdown(diff);\n } else if (fmt === 'mermaid') {\n payload = compare.renderGraphMermaid(graph);\n } else if (fmt === 'dot') {\n payload = compare.renderGraphDot(graph);\n } else if (fmt === 'md') {\n payload = `# Schema dependency graph\\n\\n${graph.nodes.length} node(s), ${graph.edges.length} edge(s).\\n\\n\\`\\`\\`mermaid\\n${compare.renderGraphMermaid(graph)}\\n\\`\\`\\`\\n`;\n } else {\n throw new Error(`Unknown --format: ${fmt}. Use mermaid | dot | md.`);\n }\n\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, payload + (payload.endsWith('\\n') ? '' : '\\n'), 'utf8');\n console.error(\n `Wrote ${out} (${payload.length} bytes, ${graph.nodes.length} nodes, ${graph.edges.length} edges).`,\n );\n } else {\n process.stdout.write(payload + (payload.endsWith('\\n') ? '' : '\\n'));\n }\n\n await runExplain(\n {\n feature: 'graph.explain',\n systemPrompt: diff\n ? 'You are a data-platform architect narrating a dependency-graph DELTA between two snapshots. Walk through added / removed / changed nodes and edges in plain English. Call out structural shifts (new sources, dropped consumers, type changes) and likely-but-not-yet-broken downstream consequences.'\n : 'You are a data-platform architect explaining a dependency graph. Describe the dominant data-flow shape, point out hot or fragile nodes, and recommend any decomposition the graph suggests.',\n },\n opts as { explain?: boolean },\n () => (diff ? buildDiffPrompt(diff) : buildGraphPrompt(graph)),\n );\n });\n attachExplainFlag(cmd);\n return cmd;\n}\n\nfunction renderDiffMarkdown(d: compare.DependencyGraphDiff): string {\n const lines: string[] = ['# Dependency-graph delta', ''];\n lines.push(\n `**Summary**: ${d.summary.nodeDelta >= 0 ? '+' : ''}${d.summary.nodeDelta} nodes, ` +\n `${d.summary.edgeDelta >= 0 ? '+' : ''}${d.summary.edgeDelta} edges. ` +\n `${d.addedNodes.length} added node(s), ${d.removedNodes.length} removed node(s), ` +\n `${d.changedNodes.length} changed node(s).`,\n );\n lines.push('');\n if (d.addedNodes.length > 0) {\n lines.push('## Added nodes');\n for (const n of d.addedNodes) lines.push(`- \\`${n.fqn}\\` (${n.objectType})`);\n lines.push('');\n }\n if (d.removedNodes.length > 0) {\n lines.push('## Removed nodes');\n for (const n of d.removedNodes) lines.push(`- \\`${n.fqn}\\` (${n.objectType})`);\n lines.push('');\n }\n if (d.changedNodes.length > 0) {\n lines.push('## Changed nodes (objectType)');\n for (const c of d.changedNodes) lines.push(`- \\`${c.fqn}\\`: ${c.before} → ${c.after}`);\n lines.push('');\n }\n if (d.addedEdges.length > 0) {\n lines.push('## Added edges');\n for (const e of d.addedEdges) lines.push(`- \\`${e.from}\\` → \\`${e.to}\\` (${e.kind})`);\n lines.push('');\n }\n if (d.removedEdges.length > 0) {\n lines.push('## Removed edges');\n for (const e of d.removedEdges) lines.push(`- \\`${e.from}\\` → \\`${e.to}\\` (${e.kind})`);\n }\n return lines.join('\\n');\n}\n\nfunction buildGraphPrompt(g: compare.DependencyGraph): string {\n const sample = g.edges.slice(0, 40).map((e) => ` - ${e.from} -> ${e.to}`);\n return [\n `Dependency graph: ${g.nodes.length} nodes, ${g.edges.length} edges.`,\n '',\n 'Edges (up to 40):',\n ...sample,\n '',\n 'Narrate this graph in plain English.',\n ].join('\\n');\n}\n\nfunction buildDiffPrompt(d: compare.DependencyGraphDiff): string {\n const lines: string[] = [];\n lines.push(\n `Graph delta: ${d.summary.nodeDelta >= 0 ? '+' : ''}${d.summary.nodeDelta} nodes, ` +\n `${d.summary.edgeDelta >= 0 ? '+' : ''}${d.summary.edgeDelta} edges.`,\n );\n if (d.addedNodes.length > 0) {\n lines.push('', `Added nodes (${d.addedNodes.length}, up to 20):`);\n for (const n of d.addedNodes.slice(0, 20)) lines.push(` + ${n.fqn} (${n.objectType})`);\n }\n if (d.removedNodes.length > 0) {\n lines.push('', `Removed nodes (${d.removedNodes.length}, up to 20):`);\n for (const n of d.removedNodes.slice(0, 20)) lines.push(` - ${n.fqn} (${n.objectType})`);\n }\n if (d.changedNodes.length > 0) {\n lines.push('', `Changed nodes (${d.changedNodes.length}):`);\n for (const c of d.changedNodes.slice(0, 20))\n lines.push(` ~ ${c.fqn}: ${c.before} -> ${c.after}`);\n }\n if (d.addedEdges.length > 0) {\n lines.push('', `Added edges (${d.addedEdges.length}, up to 20):`);\n for (const e of d.addedEdges.slice(0, 20)) lines.push(` + ${e.from} -> ${e.to}`);\n }\n if (d.removedEdges.length > 0) {\n lines.push('', `Removed edges (${d.removedEdges.length}, up to 20):`);\n for (const e of d.removedEdges.slice(0, 20)) lines.push(` - ${e.from} -> ${e.to}`);\n }\n lines.push(\n '',\n 'Narrate this delta in plain English. Highlight structural shifts and likely downstream consequences.',\n );\n return lines.join('\\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":[]}
|