@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 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/lineage.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { lineage, pac, project, columnLineage } from '@sdt-tools/core';\nimport { attachExplainFlag, runExplain } from '../util/ai-explain.js';\n\n/**\n * CLL.7-followup runtime: thread CLI flags + `.sdt/lineage.json` config\n * through the OpenLineage substrate. Exported so vitest can drive it\n * directly with a stub fetchImpl + readFile (no real network / disk).\n */\nexport interface EmitOpenLineageCliInputs {\n dag: columnLineage.ColumnLineageDag;\n endpointFlag?: string;\n configPath?: string;\n namespace?: string;\n jobNamespace?: string;\n job?: string;\n /**\n * OpenLineage emit mode. `auto` resolves to `per-event` when the\n * built events array has ≤ AUTO_MODE_THRESHOLD entries, `batch`\n * otherwise. Resolution happens at the CLI layer; the substrate\n * only ever sees the concrete `per-event` or `batch`.\n */\n mode?: 'per-event' | 'batch' | 'auto';\n /** OpenLineage event type (subset of the spec enum). Default `COMPLETE`. */\n eventType?: 'START' | 'RUNNING' | 'COMPLETE' | 'ABORT' | 'FAIL' | 'OTHER';\n /**\n * Override the event timestamp (ISO 8601). Default: now() at emit time.\n * Useful for backfill — historical pipeline runs replayed into Marquez/\n * OpenMetadata keep their original eventTime instead of being collapsed\n * onto \"now\".\n */\n eventTime?: string;\n /** Per-request timeout in ms (positive integer; 0/negative/NaN are ignored). */\n timeoutMs?: number;\n token?: string;\n /** Override OpenLineage producer URL identifying the emitting tool. */\n producer?: string;\n /** Repeatable `name=value` strings from `--emit-openlineage-header`. */\n headerArgs?: readonly string[];\n runId?: string;\n dryRun?: boolean;\n /** Persist built events JSON to this path on disk (audit / replay). */\n eventsOutPath?: string;\n /**\n * Output format for the events-out file. `json` (default) writes the\n * familiar `{events: [...]}` wrapper with pretty-printed indentation;\n * `jsonl` writes one event per line (newline-delimited JSON) for\n * streaming-ingest pipelines (Vector / Fluent Bit / OpenTelemetry\n * collectors / Snowflake VARIANT loaders).\n */\n eventsOutFormat?: 'json' | 'jsonl';\n /** Skip the POST when the events array is empty (no DAG produced 0 events). */\n skipEmpty?: boolean;\n /**\n * Refuse to emit when the built events count is below this threshold.\n * Returns `ok: false` with `MIN_EVENTS_NOT_MET`. CI-friendly guard\n * against silent lineage gaps — a refactor that accidentally bypasses\n * column-lineage extraction would otherwise quietly emit zero events\n * and pass the build.\n */\n minEvents?: number;\n sourceHint?: string;\n}\n\nconst VALID_OL_EVENT_TYPES = new Set<EmitOpenLineageCliInputs['eventType']>([\n 'START',\n 'RUNNING',\n 'COMPLETE',\n 'ABORT',\n 'FAIL',\n 'OTHER',\n]);\n\n/**\n * Validate `--emit-openlineage-event-type <value>` against the OL enum.\n * Returns the upper-cased value when valid. Throws with operator-facing\n * message when unknown — the action handler surfaces the throw as exit\n * code 1 (uncaught throw from commander action) which is the same\n * convention used by other input-validation throws in this CLI.\n */\nexport function validateEventType(raw: string): EmitOpenLineageCliInputs['eventType'] {\n const upper = raw.toUpperCase() as EmitOpenLineageCliInputs['eventType'];\n if (!VALID_OL_EVENT_TYPES.has(upper)) {\n throw new Error(\n `--emit-openlineage-event-type ${JSON.stringify(raw)} is not a valid OpenLineage event type. ` +\n `Use one of: ${[...VALID_OL_EVENT_TYPES].join(', ')}.`,\n );\n }\n return upper;\n}\n\n/**\n * Threshold at which `--emit-openlineage-mode auto` flips from\n * `per-event` (one HTTP POST per event, easier to debug in the\n * collector UI) to `batch` (single POST with `{events: [...]}`,\n * amortizes HTTP overhead). Operators with non-default cost or\n * latency requirements can still force the mode explicitly.\n */\nexport const AUTO_MODE_THRESHOLD = 10;\n\n/**\n * Resolve `mode: 'auto'` based on the built events count. Returns the\n * concrete `per-event` / `batch` value; pass-through for non-auto.\n */\nexport function resolveAutoMode(\n mode: 'per-event' | 'batch' | 'auto',\n eventCount: number,\n): 'per-event' | 'batch' {\n if (mode !== 'auto') return mode;\n return eventCount <= AUTO_MODE_THRESHOLD ? 'per-event' : 'batch';\n}\n\n/**\n * Validate `--emit-openlineage-mode <value>` against per-event / batch / auto.\n * Returns the lower-cased value when valid. Throws with operator-facing\n * message on unknown.\n */\nexport function validateMode(raw: string): 'per-event' | 'batch' | 'auto' {\n const lower = raw.toLowerCase();\n if (lower !== 'per-event' && lower !== 'batch' && lower !== 'auto') {\n throw new Error(\n `--emit-openlineage-mode ${JSON.stringify(raw)} is not valid. ` +\n `Use one of: per-event, batch, auto.`,\n );\n }\n return lower as 'per-event' | 'batch' | 'auto';\n}\n\n/**\n * Validate `--emit-openlineage-events-out-format <fmt>` value. Returns\n * the lower-cased value when valid (`json` or `jsonl`). Throws with\n * operator-facing message when unknown.\n */\nexport function validateEventsOutFormat(raw: string): 'json' | 'jsonl' {\n const lower = raw.toLowerCase();\n if (lower !== 'json' && lower !== 'jsonl') {\n throw new Error(\n `--emit-openlineage-events-out-format ${JSON.stringify(raw)} is not valid. ` +\n `Use one of: json, jsonl.`,\n );\n }\n return lower;\n}\n\n/**\n * Validate `--emit-openlineage-event-time <iso>` as a parseable timestamp.\n * Returns the canonical ISO 8601 form (`new Date(raw).toISOString()`) so\n * the value emitted in every OL event is normalized regardless of input\n * shape (`2026-05-18T20:00:00Z` and `2026-05-18T16:00:00-04:00` both\n * normalize to the same UTC instant). Throws with operator-facing\n * message on invalid input.\n */\nexport function validateEventTime(raw: string): string {\n const parsed = new Date(raw);\n if (Number.isNaN(parsed.getTime())) {\n throw new Error(\n `--emit-openlineage-event-time ${JSON.stringify(raw)} is not a valid ISO 8601 timestamp. ` +\n `Examples: 2026-05-18T20:00:00Z, 2026-05-18T16:00:00-04:00.`,\n );\n }\n return parsed.toISOString();\n}\n\n/**\n * Parse `--emit-openlineage-header name=value` repeatable strings into\n * an `{name: value}` record. Splits on the first `=` only so values\n * containing `=` (e.g. base64-encoded tokens) survive. Strings missing\n * `=` or with empty name are skipped with a stderr note.\n */\nexport function parseHeaderArgs(\n args: readonly string[] | undefined,\n stderr: (chunk: string) => void = () => {},\n): Record<string, string> {\n const out: Record<string, string> = {};\n if (!args || args.length === 0) return out;\n for (const raw of args) {\n const eq = raw.indexOf('=');\n if (eq <= 0) {\n stderr(\n `Ignoring malformed --emit-openlineage-header value ${JSON.stringify(raw)} (expected name=value).\\n`,\n );\n continue;\n }\n const name = raw.slice(0, eq).trim();\n const value = raw.slice(eq + 1);\n if (!name) {\n stderr(`Ignoring --emit-openlineage-header with empty name.\\n`);\n continue;\n }\n out[name] = value;\n }\n return out;\n}\n\nexport interface EmitOpenLineageCliDeps {\n readFile?: (p: string) => Promise<string>;\n writeFile?: (p: string, data: string) => Promise<void>;\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch;\n now?: () => Date;\n uuid?: () => string;\n stdout?: (chunk: string) => void;\n stderr?: (chunk: string) => void;\n}\n\nexport interface EmitOpenLineageCliOutcome {\n ok: boolean;\n events: number;\n succeeded?: number;\n failed?: number;\n error?: { code: string; message: string };\n}\n\nexport async function emitOpenLineageFromCli(\n inputs: EmitOpenLineageCliInputs,\n deps: EmitOpenLineageCliDeps = {},\n): Promise<EmitOpenLineageCliOutcome> {\n const stdout = deps.stdout ?? ((c: string) => process.stdout.write(c));\n const stderr = deps.stderr ?? ((c: string) => process.stderr.write(c));\n const readFile = deps.readFile ?? ((p: string) => fs.readFile(p, 'utf8'));\n const writeFile =\n deps.writeFile ??\n (async (p: string, data: string) => {\n await fs.mkdir(path.dirname(p), { recursive: true });\n await fs.writeFile(p, data, 'utf8');\n });\n const env = deps.env ?? (process.env as Record<string, string | undefined>);\n const now = deps.now ?? (() => new Date());\n const uuid = deps.uuid ?? (() => randomUUID());\n\n let loaded: columnLineage.OpenLineageConfig | undefined;\n const configPath = inputs.configPath ?? columnLineage.DEFAULT_OPENLINEAGE_CONFIG_FILE_REL;\n try {\n const raw = await readFile(configPath);\n const json = JSON.parse(raw);\n const parsed = columnLineage.parseOpenLineageConfig(json);\n if (!parsed.config) {\n const lines = parsed.errors.map((e) => ` - ${e.path}: ${e.message}`);\n stderr(`Failed to parse ${configPath}:\\n${lines.join('\\n')}\\n`);\n return {\n ok: false,\n events: 0,\n error: { code: 'CONFIG_PARSE_FAILED', message: `${configPath} did not validate` },\n };\n }\n loaded = parsed.config;\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== 'ENOENT') {\n stderr(`Failed reading ${configPath}: ${(err as Error).message}\\n`);\n return {\n ok: false,\n events: 0,\n error: { code: 'CONFIG_READ_FAILED', message: (err as Error).message },\n };\n }\n // Missing config file is fine when the CLI flag supplies the endpoint.\n }\n\n const overrides: Partial<columnLineage.OpenLineageConfig> = {};\n if (inputs.endpointFlag) overrides.endpoint = inputs.endpointFlag;\n if (inputs.namespace) overrides.datasetNamespace = inputs.namespace;\n if (inputs.jobNamespace) overrides.jobNamespace = inputs.jobNamespace;\n if (inputs.job) overrides.jobName = inputs.job;\n // `auto` is a CLI-layer mode resolved AFTER buildOpenLineageEvents\n // based on events.length. Don't push it to the substrate; the\n // substrate's mode is always per-event or batch.\n if (inputs.mode && inputs.mode !== 'auto') overrides.mode = inputs.mode;\n if (\n typeof inputs.timeoutMs === 'number' &&\n Number.isFinite(inputs.timeoutMs) &&\n inputs.timeoutMs > 0\n ) {\n overrides.timeoutMs = Math.floor(inputs.timeoutMs);\n }\n if (inputs.token) overrides.bearerToken = inputs.token;\n if (inputs.producer && inputs.producer.trim().length > 0)\n overrides.producer = inputs.producer.trim();\n const cliHeaders = parseHeaderArgs(inputs.headerArgs, stderr);\n if (Object.keys(cliHeaders).length > 0) overrides.headers = cliHeaders;\n const merged = columnLineage.mergeOpenLineageCliOverrides(loaded, overrides);\n\n const validated = columnLineage.validateOpenLineageConfig(merged);\n if (!validated.ok) {\n const lines = validated.errors.map((e) => ` - ${e.path}: ${e.message}`);\n stderr(`OpenLineage emit refused:\\n${lines.join('\\n')}\\n`);\n return {\n ok: false,\n events: 0,\n error: { code: 'CONFIG_INCOMPLETE', message: validated.errors[0]!.message },\n };\n }\n\n const resolved = columnLineage.resolveEnvPlaceholders(validated.resolved, env);\n if (!resolved.ok) {\n const lines = resolved.missing.map((m) => ` - ${m.path}: missing env var ${m.envKey}`);\n stderr(`OpenLineage env placeholders unresolved:\\n${lines.join('\\n')}\\n`);\n return {\n ok: false,\n events: 0,\n error: { code: 'ENV_UNRESOLVED', message: resolved.missing[0]!.envKey },\n };\n }\n const final = resolved.resolved;\n\n const jobName = final.jobName ?? inputs.sourceHint ?? 'sdt-lineage';\n const eventType = inputs.eventType ?? 'COMPLETE';\n const events = columnLineage.buildOpenLineageEvents(inputs.dag, {\n runId: inputs.runId ?? uuid(),\n jobName,\n jobNamespace: final.jobNamespace ?? columnLineage.DEFAULT_OPENLINEAGE_JOB_NAMESPACE,\n ...(final.datasetNamespace ? { datasetNamespace: final.datasetNamespace } : {}),\n ...(final.producer ? { producer: final.producer } : {}),\n eventTime: inputs.eventTime ?? now().toISOString(),\n eventType,\n });\n\n if (inputs.eventsOutPath) {\n const format = inputs.eventsOutFormat ?? 'json';\n const payload =\n format === 'jsonl'\n ? events.map((e) => JSON.stringify(e)).join('\\n') + '\\n'\n : JSON.stringify({ events }, null, 2) + '\\n';\n await writeFile(inputs.eventsOutPath, payload);\n stderr(\n `OpenLineage: wrote ${events.length} event(s) to ${inputs.eventsOutPath} (${format}).\\n`,\n );\n }\n\n // CI guard: fail before dry-run / POST when events.length is below\n // the operator-supplied threshold. Catches silent lineage gaps after\n // a refactor that bypasses column-lineage extraction.\n if (\n typeof inputs.minEvents === 'number' &&\n Number.isFinite(inputs.minEvents) &&\n inputs.minEvents > 0 &&\n events.length < inputs.minEvents\n ) {\n stderr(\n `OpenLineage: built ${events.length} event(s), below --emit-openlineage-min-events=${inputs.minEvents}; refusing to emit.\\n`,\n );\n return {\n ok: false,\n events: events.length,\n error: {\n code: 'MIN_EVENTS_NOT_MET',\n message: `built ${events.length} events, expected >= ${inputs.minEvents}`,\n },\n };\n }\n\n if (inputs.dryRun) {\n stdout(JSON.stringify({ events }, null, 2) + '\\n');\n return { ok: true, events: events.length };\n }\n\n if (events.length === 0 && inputs.skipEmpty) {\n stderr(\n 'OpenLineage: 0 events to emit (DAG empty) and --emit-openlineage-skip-empty set; skipping POST.\\n',\n );\n return { ok: true, events: 0, succeeded: 0, failed: 0 };\n }\n\n // Resolve `mode: auto` AFTER buildOpenLineageEvents so the decision\n // sees the actual events count. Substrate's `final.mode` is always\n // per-event / batch — auto only lives at the CLI layer.\n const concreteMode: 'per-event' | 'batch' =\n inputs.mode === 'auto' ? resolveAutoMode('auto', events.length) : (final.mode ?? 'per-event');\n if (inputs.mode === 'auto') {\n stderr(\n `OpenLineage: --emit-openlineage-mode=auto resolved to ${concreteMode} ` +\n `(${events.length} event(s), threshold=${AUTO_MODE_THRESHOLD}).\\n`,\n );\n }\n\n const result = await columnLineage.emitOpenLineageEvents(events, final.endpoint!, {\n ...(final.bearerToken ? { bearerToken: final.bearerToken } : {}),\n ...(final.headers ? { headers: final.headers } : {}),\n mode: concreteMode,\n ...(typeof final.timeoutMs === 'number' ? { timeoutMs: final.timeoutMs } : {}),\n ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),\n });\n stderr(\n `OpenLineage: ${result.succeeded}/${result.totalEvents} events delivered to ${final.endpoint} (${result.failed} failed).\\n`,\n );\n return {\n ok: result.failed === 0,\n events: result.totalEvents,\n succeeded: result.succeeded,\n failed: result.failed,\n };\n}\n\n/**\n * `sdt lineage` — extract data-flow lineage from a `.sdtproj` or\n * `.sdtpac` and emit it as Mermaid, DOT, JSON, or Markdown.\n *\n * Distinct from `sdt graph`: graph is FQN-level dependency edges\n * (A references B). Lineage adds direction-of-data-flow classification\n * (READS_FROM vs WRITES_TO), upstream/downstream traversal, and\n * column-level best-effort tagging for view SELECT lists.\n *\n * Use cases:\n * - \"What does ANALYTICS.GOLD.CUSTOMER_360 depend on?\" (upstream slice)\n * - \"If I drop RAW.STAGING.ORDERS_RAW, what breaks?\" (downstream slice)\n * - \"Embed this graph in our PR template / docs / dashboard.\"\n *\n * Mirrors `ddt lineage`.\n */\nexport function lineageCommand(): Command {\n const cmd = new Command('lineage');\n cmd\n .description('Extract data-flow lineage from a .sdtproj, .sdtpac, or dbt manifest.json.')\n .option('--source <path>', '.sdtproj or .sdtpac to analyze.')\n .option(\n '--dbt-manifest <path>',\n 'Extract column-level lineage from a dbt target/manifest.json (or project root).',\n )\n .option(\n '--fqn <name>',\n 'Focus on this fully-qualified name; restrict the graph to its upstream + downstream slice.',\n )\n .option(\n '--direction <kind>',\n 'When --fqn is set, restrict to upstream | downstream | both. Default both.',\n 'both',\n )\n .option('--depth <n>', 'Max traversal depth from --fqn. Default unbounded.', (v) =>\n parseInt(v, 10),\n )\n .option('--format <fmt>', 'mermaid | dot | json | markdown. Default mermaid.', 'mermaid')\n .option('--columns', 'Include best-effort column-level lineage for view SELECT lists.', false)\n .option(\n '--column-dag',\n 'Build a true per-column DAG via the CLL.1/2/3 substrate. Walks every VIEW / SECURE_VIEW / MATERIALIZED_VIEW / DYNAMIC_TABLE body in the project model, extracts column-level lineage, assembles the DAG, and emits Mermaid (default) or JSON. Composes with --fqn (focus to one object) and --out.',\n false,\n )\n .option('--max-edges <n>', 'Truncate large graphs at this edge count.', (v) => parseInt(v, 10))\n .option('-o, --out <path>', 'Output file path. Defaults to stdout.')\n .option(\n '--emit-openlineage [endpoint]',\n 'CLL.7-followup: emit the column DAG as OpenLineage RunEvents to the given collector URL. When the flag is given without a value, the endpoint from .sdt/lineage.json is used. Requires --column-dag (or --dbt-manifest).',\n )\n .option(\n '--emit-openlineage-config <path>',\n 'Override the config-file location (default .sdt/lineage.json).',\n )\n .option(\n '--emit-openlineage-namespace <ns>',\n 'Override the OpenLineage dataset namespace (default snowflake).',\n )\n .option(\n '--emit-openlineage-job-namespace <ns>',\n 'Override the OpenLineage job.namespace (default sdt). Groups jobs across deployments in Marquez/OpenMetadata.',\n )\n .option(\n '--emit-openlineage-job <name>',\n 'Override the OpenLineage job.name (default derived from --source).',\n )\n .option(\n '--emit-openlineage-mode <mode>',\n 'OpenLineage emit mode: per-event (default) | batch | auto. `auto` resolves to per-event when the DAG has ≤10 events (easier to debug in Marquez UI) and batch when >10 (amortizes HTTP overhead).',\n )\n .option(\n '--emit-openlineage-timeout-ms <ms>',\n 'Override per-request timeout (positive integer, default 10000ms).',\n (v) => parseInt(v, 10),\n )\n .option(\n '--emit-openlineage-token <token>',\n 'Override bearer token (supports env:VAR_NAME for indirection).',\n )\n .option(\n '--emit-openlineage-header <k=v>',\n 'Extra HTTP header attached to every OpenLineage POST (repeatable). Format: name=value. Values support env:VAR_NAME for indirection.',\n (val: string, prev: string[]) => [...prev, val],\n [] as string[],\n )\n .option(\n '--emit-openlineage-producer <url>',\n 'Override the OpenLineage producer URL (identifies the emitting tool to the collector). Default: https://github.com/GVOrganization/SDT.',\n )\n .option(\n '--emit-openlineage-event-type <type>',\n 'OpenLineage event type: COMPLETE (default), START, RUNNING, ABORT, FAIL, or OTHER. Use START + COMPLETE pairs (sharing --emit-openlineage-run-id) for run-lifecycle modelling.',\n )\n .option(\n '--emit-openlineage-event-time <iso>',\n 'Override the OpenLineage eventTime (ISO 8601). Default: now() at emit time. Use for backfill — replay historical pipeline runs into Marquez/OpenMetadata with their original timestamps instead of \"now\".',\n )\n .option(\n '--emit-openlineage-run-id <id>',\n 'Stable runId (UUID v4). Default: generated per invocation.',\n )\n .option(\n '--emit-openlineage-dry-run',\n 'Build OpenLineage events but do not POST — print the events JSON to stdout instead.',\n false,\n )\n .option(\n '--emit-openlineage-events-out <path>',\n 'Also write the built events JSON to <path> (audit / replay). Independent of --dry-run; combined yields write-only-no-POST.',\n )\n .option(\n '--emit-openlineage-events-out-format <fmt>',\n 'Output format for --emit-openlineage-events-out: json (default, `{events: [...]}` wrapper) or jsonl (newline-delimited, one event per line). Use jsonl for streaming-ingest pipelines (Vector, Fluent Bit, OpenTelemetry collectors, Snowflake VARIANT loaders).',\n )\n .option(\n '--emit-openlineage-skip-empty',\n 'Skip the POST when the built events array is empty (default behavior is to still hit the collector with 0 events in batch mode). Useful in CI when lineage extraction may produce an empty DAG and noisy 0-event traffic is undesirable.',\n false,\n )\n .option(\n '--emit-openlineage-min-events <n>',\n 'CI assertion: refuse to emit (exit 2) when the built events count is below this threshold. Catches silent lineage gaps after a refactor that bypasses column-lineage extraction. Skip-empty composes naturally — set min-events=1 to force every run to produce ≥1 event.',\n (v) => parseInt(v, 10),\n )\n .action(async (opts) => {\n // CLL.7-followup guard (iter 193): refuse --emit-openlineage when\n // neither --column-dag nor --dbt-manifest is in play, because the\n // default FQN-level lineage path doesn't produce a column DAG and\n // the OL columnLineage facet would be empty. Silent-no-op here was\n // a footgun — operators forgetting --column-dag got 0 events.\n if (opts.emitOpenlineage && !opts.columnDag && !opts.dbtManifest) {\n process.stderr.write(\n '--emit-openlineage requires --column-dag (CLL.2 DAG) or --dbt-manifest. ' +\n 'The default FQN-level lineage path has no column-level facet to emit. ' +\n 'Re-run with --column-dag --source <path> or --dbt-manifest <path>.\\n',\n );\n process.exitCode = 2;\n return;\n }\n\n // CLL.4: column-DAG mode — uses CLL.1/2/3 substrate, distinct from\n // the FQN-graph `--columns` hint above.\n if (opts.columnDag) {\n if (!opts.source) throw new Error('--column-dag requires --source <path>.');\n const model = await loadModel(String(opts.source));\n const inputs = collectColumnLineageInputs(model);\n const dag = columnLineage.buildColumnLineageDag(inputs);\n let renderedDag = dag;\n if (opts.fqn) {\n const focus = String(opts.fqn);\n const direction = String(opts.direction ?? 'both').toLowerCase() as\n | 'upstream'\n | 'downstream'\n | 'both';\n renderedDag = columnLineage.sliceColumnDag(dag, focus, direction);\n }\n const fmt = String(opts.format ?? 'mermaid').toLowerCase();\n if (!['mermaid', 'json'].includes(fmt)) {\n throw new Error(`--column-dag only supports --format mermaid|json (got ${fmt}).`);\n }\n const payload =\n fmt === 'json'\n ? JSON.stringify(columnLineage.renderDagJson(renderedDag), null, 2)\n : columnLineage.renderDagMermaid(renderedDag);\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 + '\\n', 'utf8');\n console.error(\n `Wrote ${out} (${renderedDag.nodes.size} nodes, ${renderedDag.edges.length} edges).`,\n );\n } else if (!opts.emitOpenlineage) {\n // Suppress stdout emission when we're piping into the OL --dry-run\n // path so the events JSON is the only thing on stdout.\n process.stdout.write(payload + '\\n');\n }\n if (opts.emitOpenlineage) {\n const endpointFlag =\n typeof opts.emitOpenlineage === 'string' && opts.emitOpenlineage.length > 0\n ? String(opts.emitOpenlineage)\n : undefined;\n const sourceHint = opts.source\n ? path.basename(String(opts.source)).replace(/\\.(sdtproj|sdtpac)$/i, '')\n : undefined;\n const outcome = await emitOpenLineageFromCli({\n dag: renderedDag,\n ...(endpointFlag ? { endpointFlag } : {}),\n ...(opts.emitOpenlineageConfig\n ? { configPath: String(opts.emitOpenlineageConfig) }\n : {}),\n ...(opts.emitOpenlineageNamespace\n ? { namespace: String(opts.emitOpenlineageNamespace) }\n : {}),\n ...(opts.emitOpenlineageJobNamespace\n ? { jobNamespace: String(opts.emitOpenlineageJobNamespace) }\n : {}),\n ...(opts.emitOpenlineageJob ? { job: String(opts.emitOpenlineageJob) } : {}),\n ...(opts.emitOpenlineageMode\n ? { mode: validateMode(String(opts.emitOpenlineageMode)) }\n : {}),\n ...(opts.emitOpenlineageEventType\n ? { eventType: validateEventType(String(opts.emitOpenlineageEventType)) }\n : {}),\n ...(opts.emitOpenlineageEventTime\n ? { eventTime: validateEventTime(String(opts.emitOpenlineageEventTime)) }\n : {}),\n ...(typeof opts.emitOpenlineageTimeoutMs === 'number'\n ? { timeoutMs: opts.emitOpenlineageTimeoutMs }\n : {}),\n ...(opts.emitOpenlineageToken ? { token: String(opts.emitOpenlineageToken) } : {}),\n ...(opts.emitOpenlineageProducer\n ? { producer: String(opts.emitOpenlineageProducer) }\n : {}),\n ...(Array.isArray(opts.emitOpenlineageHeader) && opts.emitOpenlineageHeader.length > 0\n ? { headerArgs: opts.emitOpenlineageHeader as string[] }\n : {}),\n ...(opts.emitOpenlineageRunId ? { runId: String(opts.emitOpenlineageRunId) } : {}),\n ...(opts.emitOpenlineageDryRun ? { dryRun: true } : {}),\n ...(opts.emitOpenlineageEventsOut\n ? { eventsOutPath: String(opts.emitOpenlineageEventsOut) }\n : {}),\n ...(opts.emitOpenlineageEventsOutFormat\n ? {\n eventsOutFormat: validateEventsOutFormat(\n String(opts.emitOpenlineageEventsOutFormat),\n ),\n }\n : {}),\n ...(opts.emitOpenlineageSkipEmpty ? { skipEmpty: true } : {}),\n ...(typeof opts.emitOpenlineageMinEvents === 'number' &&\n opts.emitOpenlineageMinEvents > 0\n ? { minEvents: opts.emitOpenlineageMinEvents }\n : {}),\n ...(sourceHint ? { sourceHint } : {}),\n });\n if (!outcome.ok) {\n process.exitCode = 2;\n }\n }\n return;\n }\n if (opts.dbtManifest) {\n const result = await columnLineage.extractDbtManifestLineage(String(opts.dbtManifest));\n const fmt = String(opts.format ?? 'mermaid').toLowerCase();\n const payload =\n fmt === 'json'\n ? columnLineage.renderDbtLineageJson(result)\n : columnLineage.renderDbtLineageMermaid(result);\n if (result.skipped.length > 0) {\n for (const s of result.skipped) console.error(`Skipped ${s.nodeId}: ${s.reason}`);\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 + '\\n', 'utf8');\n console.error(\n `Wrote ${out} (${result.models.length} models, ${result.skipped.length} skipped).`,\n );\n } else if (!opts.emitOpenlineage) {\n process.stdout.write(payload + '\\n');\n }\n if (opts.emitOpenlineage) {\n const dag = columnLineage.buildColumnLineageDag(\n result.models.map((m) => ({ objectFqn: m.fqn, lineage: m.lineage })),\n );\n const endpointFlag =\n typeof opts.emitOpenlineage === 'string' && opts.emitOpenlineage.length > 0\n ? String(opts.emitOpenlineage)\n : undefined;\n const sourceHint = `dbt:${path.basename(String(opts.dbtManifest)).replace(/\\.json$/i, '')}`;\n const outcome = await emitOpenLineageFromCli({\n dag,\n ...(endpointFlag ? { endpointFlag } : {}),\n ...(opts.emitOpenlineageConfig\n ? { configPath: String(opts.emitOpenlineageConfig) }\n : {}),\n ...(opts.emitOpenlineageNamespace\n ? { namespace: String(opts.emitOpenlineageNamespace) }\n : {}),\n ...(opts.emitOpenlineageJobNamespace\n ? { jobNamespace: String(opts.emitOpenlineageJobNamespace) }\n : {}),\n ...(opts.emitOpenlineageJob ? { job: String(opts.emitOpenlineageJob) } : {}),\n ...(opts.emitOpenlineageMode\n ? { mode: validateMode(String(opts.emitOpenlineageMode)) }\n : {}),\n ...(opts.emitOpenlineageEventType\n ? { eventType: validateEventType(String(opts.emitOpenlineageEventType)) }\n : {}),\n ...(opts.emitOpenlineageEventTime\n ? { eventTime: validateEventTime(String(opts.emitOpenlineageEventTime)) }\n : {}),\n ...(typeof opts.emitOpenlineageTimeoutMs === 'number'\n ? { timeoutMs: opts.emitOpenlineageTimeoutMs }\n : {}),\n ...(opts.emitOpenlineageToken ? { token: String(opts.emitOpenlineageToken) } : {}),\n ...(opts.emitOpenlineageProducer\n ? { producer: String(opts.emitOpenlineageProducer) }\n : {}),\n ...(Array.isArray(opts.emitOpenlineageHeader) && opts.emitOpenlineageHeader.length > 0\n ? { headerArgs: opts.emitOpenlineageHeader as string[] }\n : {}),\n ...(opts.emitOpenlineageRunId ? { runId: String(opts.emitOpenlineageRunId) } : {}),\n ...(opts.emitOpenlineageDryRun ? { dryRun: true } : {}),\n ...(opts.emitOpenlineageEventsOut\n ? { eventsOutPath: String(opts.emitOpenlineageEventsOut) }\n : {}),\n ...(opts.emitOpenlineageEventsOutFormat\n ? {\n eventsOutFormat: validateEventsOutFormat(\n String(opts.emitOpenlineageEventsOutFormat),\n ),\n }\n : {}),\n ...(opts.emitOpenlineageSkipEmpty ? { skipEmpty: true } : {}),\n ...(typeof opts.emitOpenlineageMinEvents === 'number' &&\n opts.emitOpenlineageMinEvents > 0\n ? { minEvents: opts.emitOpenlineageMinEvents }\n : {}),\n sourceHint,\n });\n if (!outcome.ok) {\n process.exitCode = 2;\n }\n }\n return;\n }\n if (!opts.source) throw new Error('Either --source or --dbt-manifest is required.');\n const model = await loadModel(String(opts.source));\n const fullGraph = lineage.buildLineageGraph(model);\n\n const direction = String(opts.direction ?? 'both').toLowerCase() as\n | 'upstream'\n | 'downstream'\n | 'both';\n let graph = fullGraph;\n let focus: string | undefined;\n if (opts.fqn) {\n focus = String(opts.fqn);\n const depth: number = typeof opts.depth === 'number' ? opts.depth : Infinity;\n if (direction === 'upstream') {\n const keep = new Set([focus, ...lineage.upstreamOf(fullGraph, focus, depth)]);\n graph = subsetGraph(fullGraph, keep);\n } else if (direction === 'downstream') {\n const keep = new Set([focus, ...lineage.downstreamOf(fullGraph, focus, depth)]);\n graph = subsetGraph(fullGraph, keep);\n } else {\n graph = lineage.sliceAround(fullGraph, focus, depth);\n }\n }\n\n const fmt = String(opts.format ?? 'mermaid').toLowerCase() as lineage.LineageFormat;\n if (!['mermaid', 'dot', 'json', 'markdown'].includes(fmt)) {\n throw new Error(`Unknown --format: ${fmt}. Use mermaid | dot | json | markdown.`);\n }\n const renderOpts: lineage.RenderOptions = {\n ...(focus ? { focus } : {}),\n ...(typeof opts.maxEdges === 'number' ? { maxEdges: opts.maxEdges } : {}),\n ...(opts.columns ? { includeColumns: true } : {}),\n };\n const payload = lineage.renderLineage(graph, fmt, renderOpts);\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 await runExplain(\n {\n feature: 'lineage.explain',\n systemPrompt:\n 'You are a data-platform architect explaining a lineage graph. Describe the dominant data-flow shape, point out hot or fragile nodes, and recommend any decomposition / sharding the graph suggests.',\n },\n opts as { explain?: boolean },\n () => {\n const focused = focus ? ` (focused on ${focus}, direction=${direction})` : '';\n const summary = `Lineage graph${focused}: ${graph.nodes.length} nodes, ${graph.edges.length} edges.`;\n const sample = graph.edges\n .slice(0, 40)\n .map((e) => ` - ${e.from} ${e.kind === 'READS_FROM' ? '->' : '=>'} ${e.to}`);\n return [\n summary,\n '',\n 'Edges (up to 40):',\n ...sample,\n '',\n 'Narrate this graph in plain English.',\n ].join('\\n');\n },\n );\n });\n attachExplainFlag(cmd);\n return cmd;\n}\n\nfunction subsetGraph(graph: lineage.LineageGraph, keep: Set<string>): lineage.LineageGraph {\n return {\n nodes: graph.nodes.filter((n) => keep.has(n.fqn)),\n edges: graph.edges.filter((e) => keep.has(e.from) && keep.has(e.to)),\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\n/**\n * Walk a project model collecting `{objectFqn, lineage}` pairs for every\n * object whose body parses through CLL.1's `extractColumnLineage`. Covers\n * VIEW / SECURE_VIEW / MATERIALIZED_VIEW / DYNAMIC_TABLE — the four object\n * types whose `query: SqlExpression` is what downstream consumers depend\n * on for column-level lineage. CLL.4 entry point.\n */\nfunction collectColumnLineageInputs(model: unknown): columnLineage.ColumnLineageDagInput[] {\n const inputs: columnLineage.ColumnLineageDagInput[] = [];\n const matchTypes = new Set<string>(['VIEW', 'SECURE_VIEW', 'MATERIALIZED_VIEW', 'DYNAMIC_TABLE']);\n // parseProjectModel returns AnySnowflakeObject[]; readPac().model is the\n // same shape. Some legacy pac models nest objects under .objects so we\n // accept either form defensively.\n let flat: Array<{ objectType?: string; fqn?: unknown; query?: unknown }>;\n if (Array.isArray(model)) {\n flat = model as Array<{ objectType?: string; fqn?: unknown; query?: unknown }>;\n } else if (\n model &&\n typeof model === 'object' &&\n Array.isArray((model as { objects?: unknown }).objects)\n ) {\n flat = (model as { objects: Array<{ objectType?: string; fqn?: unknown; query?: unknown }> })\n .objects;\n } else {\n flat = [];\n }\n for (const obj of flat) {\n if (!obj.objectType || !matchTypes.has(obj.objectType)) continue;\n if (typeof obj.query !== 'string' || obj.query.length === 0) continue;\n const fqn =\n typeof obj.fqn === 'string'\n ? obj.fqn\n : obj.fqn && typeof obj.fqn === 'object'\n ? formatFqn(obj.fqn as { database?: string; schema?: string; name?: string })\n : '<unknown>';\n const lineage = columnLineage.extractColumnLineage(obj.query);\n inputs.push({ objectFqn: fqn, lineage });\n }\n return inputs;\n}\n\nfunction formatFqn(fqn: { database?: string; schema?: string; name?: string }): string {\n return [fqn.database, fqn.schema, fqn.name].filter(Boolean).join('.');\n}\n"],"mappings":"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","names":["model","focus","direction","fmt","payload","lineage"]}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/lint.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import {
|
|
8
|
+
lintSql,
|
|
9
|
+
lintRules,
|
|
10
|
+
loadRulePacks,
|
|
11
|
+
loadRulePacksFromDir,
|
|
12
|
+
compileRulePacks,
|
|
13
|
+
RULE_PACK_DIR
|
|
14
|
+
} from "@sdt-tools/core/deploy";
|
|
15
|
+
import { dbtCode, interop } from "@sdt-tools/core";
|
|
16
|
+
var JINJA_OPENER = /\{\{|\{%/;
|
|
17
|
+
function lintCommand() {
|
|
18
|
+
const cmd = new Command("lint");
|
|
19
|
+
cmd.description(
|
|
20
|
+
"Lint a SQL script for risky deploy patterns. Exits non-zero on any ERROR-severity finding."
|
|
21
|
+
).option("--script <path>", "Path to the SQL script to lint (default: read stdin).").option(
|
|
22
|
+
"--json",
|
|
23
|
+
"Emit JSON instead of human-readable output. (Alias for --format json.)",
|
|
24
|
+
false
|
|
25
|
+
).option(
|
|
26
|
+
"--format <fmt>",
|
|
27
|
+
"Output format: text | json | markdown | sarif. sarif emits SARIF 2.1.0 for GitHub code-scanning / Azure DevOps. Default text.",
|
|
28
|
+
"text"
|
|
29
|
+
).option("--rules", "Print the rule list and exit.", false).option(
|
|
30
|
+
"--max-warnings <n>",
|
|
31
|
+
"Fail when warning count exceeds <n>. 0 = any warning fails. Default: unbounded.",
|
|
32
|
+
"-1"
|
|
33
|
+
).option(
|
|
34
|
+
"--dbt",
|
|
35
|
+
"Force dbt-mode on: pre-mask {{ ... }} / {% ... %} Jinja blocks before linting so the lint rules see a tidy SQL document. Default: auto-detect."
|
|
36
|
+
).option(
|
|
37
|
+
"--no-dbt",
|
|
38
|
+
"Force dbt-mode off: feed Jinja text directly to the lint rules (almost always wrong on dbt models)."
|
|
39
|
+
).option(
|
|
40
|
+
"--rules-dir <path>",
|
|
41
|
+
`Directory holding custom rule-pack JSON files (RULEPACK.1). Default: <cwd>/${RULE_PACK_DIR}.`
|
|
42
|
+
).option(
|
|
43
|
+
"--no-custom-rules",
|
|
44
|
+
"Skip loading customer-defined rule packs; lint with the built-in catalog only."
|
|
45
|
+
).action(async (opts) => {
|
|
46
|
+
const extraRules = await loadCustomRules(opts);
|
|
47
|
+
if (opts.rules) {
|
|
48
|
+
for (const r of lintRules(extraRules)) {
|
|
49
|
+
console.log(`${r.id.padEnd(6)} ${r.severity.padEnd(8)} ${r.description}`);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const rawSql = opts.script ? await fs.readFile(String(opts.script), "utf8") : await readStdin();
|
|
54
|
+
const dbtActive = opts.dbt === true || opts.dbt !== false && JINJA_OPENER.test(rawSql);
|
|
55
|
+
const sql = dbtActive ? dbtCode.maskJinja(rawSql).masked : rawSql;
|
|
56
|
+
const result = lintSql(sql, extraRules);
|
|
57
|
+
const fmt = opts.json ? "json" : String(opts.format ?? "text").toLowerCase();
|
|
58
|
+
if (fmt === "json") {
|
|
59
|
+
console.log(JSON.stringify(result, null, 2));
|
|
60
|
+
} else if (fmt === "markdown") {
|
|
61
|
+
console.log(renderMarkdown(result));
|
|
62
|
+
} else if (fmt === "sarif") {
|
|
63
|
+
const uri = opts.script ? String(opts.script) : "<stdin>";
|
|
64
|
+
console.log(JSON.stringify(interop.lintResultToSarif(result, uri), null, 2));
|
|
65
|
+
} else if (fmt === "text") {
|
|
66
|
+
for (const f of result.findings) {
|
|
67
|
+
const tag = f.severity === "ERROR" ? "\u2717" : f.severity === "WARNING" ? "\u26A0" : "\u2139";
|
|
68
|
+
console.log(`${tag} ${f.rule} line ${f.line} ${f.severity}`);
|
|
69
|
+
console.log(` ${f.message}`);
|
|
70
|
+
if (f.suggestion) console.log(` \u2192 ${f.suggestion}`);
|
|
71
|
+
console.log(` ${f.statement.split("\n")[0]?.slice(0, 100) ?? ""}`);
|
|
72
|
+
console.log("");
|
|
73
|
+
}
|
|
74
|
+
console.log(
|
|
75
|
+
`Summary: ${result.errorCount} error(s), ${result.warningCount} warning(s), ${result.infoCount} info.`
|
|
76
|
+
);
|
|
77
|
+
} else {
|
|
78
|
+
throw new Error(`Unknown --format "${opts.format}". Use text | json | markdown | sarif.`);
|
|
79
|
+
}
|
|
80
|
+
const maxWarnings = parseInt(String(opts.maxWarnings), 10);
|
|
81
|
+
if (result.errorCount > 0) {
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
} else if (maxWarnings >= 0 && result.warningCount > maxWarnings) {
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return cmd;
|
|
88
|
+
}
|
|
89
|
+
async function loadCustomRules(opts) {
|
|
90
|
+
if (opts.customRules === false) return [];
|
|
91
|
+
const result = opts.rulesDir ? await loadRulePacksFromDir(path.resolve(String(opts.rulesDir))) : await loadRulePacks(process.cwd());
|
|
92
|
+
for (const s of result.skipped) {
|
|
93
|
+
console.error(
|
|
94
|
+
`\u26A0 rule pack: skipped ${s.id ? `rule '${s.id}'` : "file"} in ${s.file} \u2014 ${s.reason}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return compileRulePacks(result.packs);
|
|
98
|
+
}
|
|
99
|
+
function renderMarkdown(result) {
|
|
100
|
+
const lines = [];
|
|
101
|
+
lines.push("# Lint findings");
|
|
102
|
+
lines.push("");
|
|
103
|
+
lines.push(
|
|
104
|
+
`**Summary**: ${result.errorCount} error(s), ${result.warningCount} warning(s), ${result.infoCount} info.`
|
|
105
|
+
);
|
|
106
|
+
lines.push("");
|
|
107
|
+
if (result.findings.length === 0) {
|
|
108
|
+
lines.push("_(no findings)_");
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
lines.push("| Severity | Rule | Line | Message |");
|
|
112
|
+
lines.push("| --- | --- | ---: | --- |");
|
|
113
|
+
for (const f of result.findings) {
|
|
114
|
+
const sev = f.severity === "ERROR" ? "\u{1F6D1} ERROR" : f.severity === "WARNING" ? "\u26A0 WARN" : "\u2139 INFO";
|
|
115
|
+
const msg = f.message.replace(/\|/g, "\\|");
|
|
116
|
+
lines.push(`| ${sev} | \`${f.rule}\` | ${f.line} | ${msg} |`);
|
|
117
|
+
}
|
|
118
|
+
lines.push("");
|
|
119
|
+
for (const f of result.findings) {
|
|
120
|
+
lines.push(`### ${f.rule} (${f.severity}) \u2014 line ${f.line}`);
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push(f.message);
|
|
123
|
+
if (f.suggestion) {
|
|
124
|
+
lines.push("");
|
|
125
|
+
lines.push(`> ${f.suggestion}`);
|
|
126
|
+
}
|
|
127
|
+
lines.push("");
|
|
128
|
+
lines.push("```sql");
|
|
129
|
+
lines.push(f.statement.split("\n")[0]?.slice(0, 200) ?? "");
|
|
130
|
+
lines.push("```");
|
|
131
|
+
lines.push("");
|
|
132
|
+
}
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
async function readStdin() {
|
|
136
|
+
const chunks = [];
|
|
137
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
138
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
139
|
+
}
|
|
140
|
+
export {
|
|
141
|
+
lintCommand
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=lint-FQ2OTYTQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/lint.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport {\n lintSql,\n lintRules,\n loadRulePacks,\n loadRulePacksFromDir,\n compileRulePacks,\n RULE_PACK_DIR,\n type LintResult,\n type LintRule,\n} from '@sdt-tools/core/deploy';\nimport { dbtCode, interop } from '@sdt-tools/core';\n\n/** DBTC.4 — auto-detect dbt-mode in the lint surface. */\nconst JINJA_OPENER = /\\{\\{|\\{%/;\n\n/**\n * `sdt lint` — scan a SQL script for risky patterns before you run it.\n * Atlas-style migration linting. Sibling of `ddt lint`; SDT rules are\n * Snowflake-flavored (L001-L032 platform-agnostic, S001-S006\n * Snowflake-specific).\n */\nexport function lintCommand(): Command {\n const cmd = new Command('lint');\n cmd\n .description(\n 'Lint a SQL script for risky deploy patterns. Exits non-zero on any ERROR-severity finding.',\n )\n .option('--script <path>', 'Path to the SQL script to lint (default: read stdin).')\n .option(\n '--json',\n 'Emit JSON instead of human-readable output. (Alias for --format json.)',\n false,\n )\n .option(\n '--format <fmt>',\n 'Output format: text | json | markdown | sarif. sarif emits SARIF 2.1.0 for GitHub code-scanning / Azure DevOps. Default text.',\n 'text',\n )\n .option('--rules', 'Print the rule list and exit.', false)\n .option(\n '--max-warnings <n>',\n 'Fail when warning count exceeds <n>. 0 = any warning fails. Default: unbounded.',\n '-1',\n )\n .option(\n '--dbt',\n 'Force dbt-mode on: pre-mask {{ ... }} / {% ... %} Jinja blocks before linting so the lint rules see a tidy SQL document. Default: auto-detect.',\n )\n .option(\n '--no-dbt',\n 'Force dbt-mode off: feed Jinja text directly to the lint rules (almost always wrong on dbt models).',\n )\n .option(\n '--rules-dir <path>',\n `Directory holding custom rule-pack JSON files (RULEPACK.1). Default: <cwd>/${RULE_PACK_DIR}.`,\n )\n .option(\n '--no-custom-rules',\n 'Skip loading customer-defined rule packs; lint with the built-in catalog only.',\n )\n .action(async (opts) => {\n // RULEPACK.1 — customer-defined rules merge into the built-in catalog.\n // Invalid definitions are reported to stderr but never abort the lint.\n const extraRules = await loadCustomRules(opts);\n\n if (opts.rules) {\n for (const r of lintRules(extraRules)) {\n console.log(`${r.id.padEnd(6)} ${r.severity.padEnd(8)} ${r.description}`);\n }\n return;\n }\n const rawSql = opts.script\n ? await fs.readFile(String(opts.script), 'utf8')\n : await readStdin();\n // DBTC.4 — pre-mask Jinja so {{ ref(...) }} doesn't trip lint rules\n // that scan for keyword shapes. Default auto-detects on a Jinja\n // opener; --dbt / --no-dbt force the choice.\n const dbtActive = opts.dbt === true || (opts.dbt !== false && JINJA_OPENER.test(rawSql));\n const sql = dbtActive ? dbtCode.maskJinja(rawSql).masked : rawSql;\n const result = lintSql(sql, extraRules);\n\n const fmt = opts.json ? 'json' : String(opts.format ?? 'text').toLowerCase();\n if (fmt === 'json') {\n console.log(JSON.stringify(result, null, 2));\n } else if (fmt === 'markdown') {\n console.log(renderMarkdown(result));\n } else if (fmt === 'sarif') {\n const uri = opts.script ? String(opts.script) : '<stdin>';\n console.log(JSON.stringify(interop.lintResultToSarif(result, uri), null, 2));\n } else if (fmt === 'text') {\n for (const f of result.findings) {\n const tag = f.severity === 'ERROR' ? '✗' : f.severity === 'WARNING' ? '⚠' : 'ℹ';\n console.log(`${tag} ${f.rule} line ${f.line} ${f.severity}`);\n console.log(` ${f.message}`);\n if (f.suggestion) console.log(` → ${f.suggestion}`);\n console.log(` ${f.statement.split('\\n')[0]?.slice(0, 100) ?? ''}`);\n console.log('');\n }\n console.log(\n `Summary: ${result.errorCount} error(s), ${result.warningCount} warning(s), ${result.infoCount} info.`,\n );\n } else {\n throw new Error(`Unknown --format \"${opts.format}\". Use text | json | markdown | sarif.`);\n }\n\n const maxWarnings = parseInt(String(opts.maxWarnings), 10);\n if (result.errorCount > 0) {\n process.exitCode = 1;\n } else if (maxWarnings >= 0 && result.warningCount > maxWarnings) {\n process.exitCode = 1;\n }\n });\n return cmd;\n}\n\n/**\n * RULEPACK.1 — load + compile custom rule packs based on the CLI flags.\n * `--no-custom-rules` disables; `--rules-dir <dir>` reads packs from an\n * explicit directory instead of the default `<cwd>/.sdt/rules`. Skipped\n * (invalid) definitions are reported to stderr but never abort the lint.\n */\nasync function loadCustomRules(opts: {\n customRules?: boolean;\n rulesDir?: string;\n}): Promise<LintRule[]> {\n if (opts.customRules === false) return [];\n const result = opts.rulesDir\n ? await loadRulePacksFromDir(path.resolve(String(opts.rulesDir)))\n : await loadRulePacks(process.cwd());\n for (const s of result.skipped) {\n console.error(\n `⚠ rule pack: skipped ${s.id ? `rule '${s.id}'` : 'file'} in ${s.file} — ${s.reason}`,\n );\n }\n return compileRulePacks(result.packs);\n}\n\nfunction renderMarkdown(result: LintResult): string {\n const lines: string[] = [];\n lines.push('# Lint findings');\n lines.push('');\n lines.push(\n `**Summary**: ${result.errorCount} error(s), ${result.warningCount} warning(s), ${result.infoCount} info.`,\n );\n lines.push('');\n if (result.findings.length === 0) {\n lines.push('_(no findings)_');\n return lines.join('\\n');\n }\n lines.push('| Severity | Rule | Line | Message |');\n lines.push('| --- | --- | ---: | --- |');\n for (const f of result.findings) {\n const sev =\n f.severity === 'ERROR' ? '🛑 ERROR' : f.severity === 'WARNING' ? '⚠ WARN' : 'ℹ INFO';\n const msg = f.message.replace(/\\|/g, '\\\\|');\n lines.push(`| ${sev} | \\`${f.rule}\\` | ${f.line} | ${msg} |`);\n }\n lines.push('');\n for (const f of result.findings) {\n lines.push(`### ${f.rule} (${f.severity}) — line ${f.line}`);\n lines.push('');\n lines.push(f.message);\n if (f.suggestion) {\n lines.push('');\n lines.push(`> ${f.suggestion}`);\n }\n lines.push('');\n lines.push('```sql');\n lines.push(f.statement.split('\\n')[0]?.slice(0, 200) ?? '');\n lines.push('```');\n lines.push('');\n }\n return lines.join('\\n');\n}\n\nasync function readStdin(): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const c of process.stdin) chunks.push(c as Buffer);\n return Buffer.concat(chunks).toString('utf8');\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/mcp.ts
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import {
|
|
7
|
+
CompareEngine,
|
|
8
|
+
PacSource,
|
|
9
|
+
renderDocsReport,
|
|
10
|
+
renderErdMarkdown
|
|
11
|
+
} from "@sdt-tools/core/compare";
|
|
12
|
+
import { loadProject, parseProjectModel, discoverObjectFiles } from "@sdt-tools/core/project";
|
|
13
|
+
import { readPac } from "@sdt-tools/core/pac";
|
|
14
|
+
import { assess, explainFinding } from "@sdt-tools/core/safety";
|
|
15
|
+
import { detectPiiCandidates } from "@sdt-tools/core/pii";
|
|
16
|
+
import { features } from "@sdt-tools/core";
|
|
17
|
+
var SERVER_NAME = "sdt";
|
|
18
|
+
var SERVER_VERSION = "0.2.5";
|
|
19
|
+
var PROTOCOL_VERSION = "2024-11-05";
|
|
20
|
+
var TOOLS = [
|
|
21
|
+
{
|
|
22
|
+
name: "sdt.validate",
|
|
23
|
+
description: "Validate a .sdtproj file: parse JSON, check schema with zod, list object files. Returns a JSON summary of project structure and any validation errors.",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
projectPath: { type: "string", description: "Absolute path to a .sdtproj file." }
|
|
28
|
+
},
|
|
29
|
+
required: ["projectPath"]
|
|
30
|
+
},
|
|
31
|
+
handler: async (args) => {
|
|
32
|
+
const projectPath = path.resolve(String(args.projectPath));
|
|
33
|
+
const loaded = await loadProject(projectPath);
|
|
34
|
+
const files = await discoverObjectFiles(loaded);
|
|
35
|
+
return JSON.stringify(
|
|
36
|
+
{
|
|
37
|
+
ok: true,
|
|
38
|
+
name: loaded.project.name,
|
|
39
|
+
version: loaded.project.version,
|
|
40
|
+
scope: loaded.project.scope,
|
|
41
|
+
objectFileCount: files.length,
|
|
42
|
+
profiles: Object.keys(loaded.project.deploymentProfiles ?? {})
|
|
43
|
+
},
|
|
44
|
+
null,
|
|
45
|
+
2
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "sdt.compare",
|
|
51
|
+
description: "Compare two .sdtpac files (offline). Returns the diff JSON: added / removed / modified objects, with per-object field changes.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
sourcePac: { type: "string", description: "Absolute path to the source .sdtpac." },
|
|
56
|
+
targetPac: { type: "string", description: "Absolute path to the target .sdtpac." }
|
|
57
|
+
},
|
|
58
|
+
required: ["sourcePac", "targetPac"]
|
|
59
|
+
},
|
|
60
|
+
handler: async (args) => {
|
|
61
|
+
const source = new PacSource(path.resolve(String(args.sourcePac)));
|
|
62
|
+
const target = new PacSource(path.resolve(String(args.targetPac)));
|
|
63
|
+
const engine = new CompareEngine();
|
|
64
|
+
const result = await engine.compare(source, target);
|
|
65
|
+
return JSON.stringify(result, null, 2);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "sdt.docs",
|
|
70
|
+
description: "Generate self-contained HTML schema documentation from a .sdtproj or .sdtpac. Returns the HTML body as a string.",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
source: { type: "string", description: "Absolute path to a .sdtproj or .sdtpac." },
|
|
75
|
+
title: { type: "string", description: "Optional doc title." }
|
|
76
|
+
},
|
|
77
|
+
required: ["source"]
|
|
78
|
+
},
|
|
79
|
+
handler: async (args) => {
|
|
80
|
+
const model = await loadModel(String(args.source));
|
|
81
|
+
return renderDocsReport(model, {
|
|
82
|
+
title: typeof args.title === "string" ? args.title : "Schema docs"
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "sdt.erd",
|
|
88
|
+
description: "Generate a Mermaid ER diagram (Markdown) from a .sdtproj or .sdtpac.",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
source: { type: "string", description: "Absolute path to a .sdtproj or .sdtpac." },
|
|
93
|
+
title: { type: "string", description: "Optional diagram title." }
|
|
94
|
+
},
|
|
95
|
+
required: ["source"]
|
|
96
|
+
},
|
|
97
|
+
handler: async (args) => {
|
|
98
|
+
const model = await loadModel(String(args.source));
|
|
99
|
+
return renderErdMarkdown(
|
|
100
|
+
model,
|
|
101
|
+
typeof args.title === "string" ? args.title : "Schema ER diagram"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "sdt.safety_assess",
|
|
107
|
+
description: "Given two .sdtpac files, run the safety classifier on the diff. Returns UNRECOVERABLE / DESTRUCTIVE / EXPENSIVE / WARNING findings with the gate option each would need.",
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
sourcePac: { type: "string", description: "Source .sdtpac (the desired state)." },
|
|
112
|
+
targetPac: {
|
|
113
|
+
type: "string",
|
|
114
|
+
description: "Target .sdtpac (current state to migrate from)."
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
required: ["sourcePac", "targetPac"]
|
|
118
|
+
},
|
|
119
|
+
handler: async (args) => {
|
|
120
|
+
const source = new PacSource(path.resolve(String(args.sourcePac)));
|
|
121
|
+
const target = new PacSource(path.resolve(String(args.targetPac)));
|
|
122
|
+
const engine = new CompareEngine();
|
|
123
|
+
const diff = await engine.compare(source, target);
|
|
124
|
+
const assessment = assess(diff);
|
|
125
|
+
return JSON.stringify(assessment, null, 2);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "sdt.narrate_diff",
|
|
130
|
+
description: "Build a structured payload (diff summary + per-object changes + safety findings + a system-prompt scaffold) the calling agent's LLM can use to generate a plain-English narration of a schema diff. Returns JSON the agent should compose into its own completion.",
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
sourcePac: { type: "string", description: "Source .sdtpac (desired state)." },
|
|
135
|
+
targetPac: { type: "string", description: "Target .sdtpac (current state)." }
|
|
136
|
+
},
|
|
137
|
+
required: ["sourcePac", "targetPac"]
|
|
138
|
+
},
|
|
139
|
+
handler: async (args) => {
|
|
140
|
+
const source = new PacSource(path.resolve(String(args.sourcePac)));
|
|
141
|
+
const target = new PacSource(path.resolve(String(args.targetPac)));
|
|
142
|
+
const engine = new CompareEngine();
|
|
143
|
+
const diff = await engine.compare(source, target);
|
|
144
|
+
const assessment = assess(diff);
|
|
145
|
+
const payload = {
|
|
146
|
+
summary: diff.summary,
|
|
147
|
+
objects: diff.objects.map((o) => ({
|
|
148
|
+
kind: o.kind,
|
|
149
|
+
objectType: o.identity.objectType,
|
|
150
|
+
fqn: o.identity.fqn
|
|
151
|
+
})),
|
|
152
|
+
safety: assessment,
|
|
153
|
+
promptScaffold: {
|
|
154
|
+
system: "You are a senior Snowflake DBA reviewing a schema migration. Narrate the change in 2-3 short paragraphs: (1) the intent, (2) the safest order of operations, (3) the risks to watch for. Be concrete; reference the FQNs.",
|
|
155
|
+
userTemplate: "Here is the diff JSON and the safety assessment. Generate the narration."
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
return JSON.stringify(payload, null, 2);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "sdt.detect_pii",
|
|
163
|
+
description: "Scan a project or pac for columns that look like PII (email, phone, ssn, dob, credit-card, national-id, health, ip-address, name, address, generic). Returns each candidate with its category, confidence tier, and the reason the heuristic flagged it.",
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
source: { type: "string", description: "Absolute path to a .sdtproj or .sdtpac." }
|
|
168
|
+
},
|
|
169
|
+
required: ["source"]
|
|
170
|
+
},
|
|
171
|
+
handler: async (args) => {
|
|
172
|
+
const model = await loadModel(String(args.source));
|
|
173
|
+
const candidates = detectPiiCandidates(model);
|
|
174
|
+
return JSON.stringify({ count: candidates.length, candidates }, null, 2);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "sdt.suggest_safer",
|
|
179
|
+
description: `For every UNRECOVERABLE / DESTRUCTIVE finding the safety classifier produced on a diff, return the catalog's saferAlternatives entries (e.g. "instead of DROP TABLE, RENAME to _archived_<ts> and let the retention policy expire it"). The agent can paste these into a PR comment or CodeLens suggestion.`,
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: {
|
|
183
|
+
sourcePac: { type: "string", description: "Source .sdtpac (desired)." },
|
|
184
|
+
targetPac: { type: "string", description: "Target .sdtpac (current)." }
|
|
185
|
+
},
|
|
186
|
+
required: ["sourcePac", "targetPac"]
|
|
187
|
+
},
|
|
188
|
+
handler: async (args) => {
|
|
189
|
+
const source = new PacSource(path.resolve(String(args.sourcePac)));
|
|
190
|
+
const target = new PacSource(path.resolve(String(args.targetPac)));
|
|
191
|
+
const engine = new CompareEngine();
|
|
192
|
+
const diff = await engine.compare(source, target);
|
|
193
|
+
const assessment = assess(diff);
|
|
194
|
+
const dangerous = [...assessment.unrecoverable, ...assessment.destructive];
|
|
195
|
+
const suggestions = dangerous.map((f) => {
|
|
196
|
+
const explanation = explainFinding(f.code);
|
|
197
|
+
return {
|
|
198
|
+
finding: { code: f.code, category: f.category, fqn: f.fqn, reason: f.reason },
|
|
199
|
+
saferAlternatives: explanation?.saferAlternatives ?? [],
|
|
200
|
+
requiredGates: explanation?.requiredGates ?? []
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
return JSON.stringify({ count: suggestions.length, suggestions }, null, 2);
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: "sdt.feature_search",
|
|
208
|
+
description: "Search the SDT feature catalog by keyword. Returns features whose id, name, summary, or synonyms match the query. Useful for discovering which tier unlocks a capability or finding the right command for a task.",
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {
|
|
212
|
+
query: {
|
|
213
|
+
type: "string",
|
|
214
|
+
description: 'Search term \u2014 feature name, keyword, or capability (e.g. "drift", "slice", "AI").'
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
required: ["query"]
|
|
218
|
+
},
|
|
219
|
+
handler: async (args) => {
|
|
220
|
+
const q = String(args.query ?? "").toLowerCase().trim();
|
|
221
|
+
const matches = features.SDT_FEATURE_CATALOG.filter(
|
|
222
|
+
(f) => f.id.includes(q) || f.name.toLowerCase().includes(q) || f.summary.toLowerCase().includes(q) || f.synonyms?.some((s) => s.toLowerCase().includes(q)) || f.whenToUse?.toLowerCase().includes(q)
|
|
223
|
+
);
|
|
224
|
+
return JSON.stringify(
|
|
225
|
+
{
|
|
226
|
+
count: matches.length,
|
|
227
|
+
features: matches.map((f) => ({
|
|
228
|
+
id: f.id,
|
|
229
|
+
name: f.name,
|
|
230
|
+
tier: f.tier,
|
|
231
|
+
status: f.status,
|
|
232
|
+
summary: f.summary,
|
|
233
|
+
unlockHow: f.unlockHow,
|
|
234
|
+
useCases: f.useCases,
|
|
235
|
+
relatedFeatures: f.relatedFeatures
|
|
236
|
+
}))
|
|
237
|
+
},
|
|
238
|
+
null,
|
|
239
|
+
2
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
];
|
|
244
|
+
async function loadModel(sourcePath) {
|
|
245
|
+
const abs = path.resolve(sourcePath);
|
|
246
|
+
if (abs.endsWith(".sdtpac")) {
|
|
247
|
+
const pacContents = await readPac(abs);
|
|
248
|
+
return pacContents.model;
|
|
249
|
+
}
|
|
250
|
+
const loaded = await loadProject(abs);
|
|
251
|
+
const parsed = await parseProjectModel(loaded);
|
|
252
|
+
return parsed;
|
|
253
|
+
}
|
|
254
|
+
function mcpCommand() {
|
|
255
|
+
const cmd = new Command("mcp");
|
|
256
|
+
cmd.description(
|
|
257
|
+
"Start the SDT Model Context Protocol server on stdio (for Claude / Cursor / agents)."
|
|
258
|
+
).action(async () => {
|
|
259
|
+
await runMcpStdio();
|
|
260
|
+
});
|
|
261
|
+
return cmd;
|
|
262
|
+
}
|
|
263
|
+
async function runMcpStdio() {
|
|
264
|
+
await new Promise((resolve) => {
|
|
265
|
+
let buffer = "";
|
|
266
|
+
process.stdin.setEncoding("utf8");
|
|
267
|
+
process.stdin.on("data", (chunk) => {
|
|
268
|
+
buffer += chunk;
|
|
269
|
+
let nl = buffer.indexOf("\n");
|
|
270
|
+
while (nl !== -1) {
|
|
271
|
+
const line = buffer.slice(0, nl).trim();
|
|
272
|
+
buffer = buffer.slice(nl + 1);
|
|
273
|
+
if (line.length > 0) {
|
|
274
|
+
void handleLine(line);
|
|
275
|
+
}
|
|
276
|
+
nl = buffer.indexOf("\n");
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
process.stdin.on("end", () => resolve());
|
|
280
|
+
process.stdin.on("close", () => resolve());
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async function handleLine(line) {
|
|
284
|
+
let req;
|
|
285
|
+
try {
|
|
286
|
+
req = JSON.parse(line);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
send({
|
|
289
|
+
jsonrpc: "2.0",
|
|
290
|
+
id: null,
|
|
291
|
+
error: { code: -32700, message: "Parse error", data: String(err) }
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const id = req.id ?? null;
|
|
296
|
+
try {
|
|
297
|
+
const result = await dispatch(req);
|
|
298
|
+
if (id !== null) {
|
|
299
|
+
send({ jsonrpc: "2.0", id, result });
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
303
|
+
send({ jsonrpc: "2.0", id, error: { code: -32603, message } });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function dispatch(req) {
|
|
307
|
+
switch (req.method) {
|
|
308
|
+
case "initialize":
|
|
309
|
+
return {
|
|
310
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
311
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
312
|
+
capabilities: { tools: {} }
|
|
313
|
+
};
|
|
314
|
+
case "notifications/initialized":
|
|
315
|
+
return void 0;
|
|
316
|
+
case "tools/list":
|
|
317
|
+
return {
|
|
318
|
+
tools: TOOLS.map((t) => ({
|
|
319
|
+
name: t.name,
|
|
320
|
+
description: t.description,
|
|
321
|
+
inputSchema: t.inputSchema
|
|
322
|
+
}))
|
|
323
|
+
};
|
|
324
|
+
case "tools/call": {
|
|
325
|
+
const params = req.params ?? {};
|
|
326
|
+
const tool = TOOLS.find((t) => t.name === params.name);
|
|
327
|
+
if (!tool) throw new Error(`Unknown tool: ${String(params.name)}`);
|
|
328
|
+
const output = await tool.handler(params.arguments ?? {});
|
|
329
|
+
return { content: [{ type: "text", text: output }] };
|
|
330
|
+
}
|
|
331
|
+
case "ping":
|
|
332
|
+
return {};
|
|
333
|
+
default:
|
|
334
|
+
throw new Error(`Method not found: ${req.method}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function send(msg) {
|
|
338
|
+
process.stdout.write(`${JSON.stringify(msg)}
|
|
339
|
+
`);
|
|
340
|
+
}
|
|
341
|
+
export {
|
|
342
|
+
mcpCommand
|
|
343
|
+
};
|
|
344
|
+
//# sourceMappingURL=mcp-3QI4TH4N.js.map
|