Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/meta/commit.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
<<<<<<< Updated upstream
fix: add missing `altimate_change` markers for `experimental` block in `opencode.jsonc`

The `experimental` config added in #311 was missing upstream markers,
causing the Marker Guard CI check to fail on main.
=======
fix: add try/catch and input sanitization to TUI install/create (#341)

Root cause of silent failures: `onConfirm` async callbacks had no
try/catch, so any thrown error was swallowed and no result toast shown.

Fixes:
- Wrap all install/create logic in try/catch with error toast
- Strip trailing dots from input (textarea was appending `.`)
- Strip `.git` suffix from URLs (users paste from browser)
- Trim whitespace and validate before proceeding
- "Installing..." toast now shows 60s duration with helpful text
("This may take a moment while the repo is cloned")
- Empty input shows immediate error instead of proceeding
>>>>>>> Stashed changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 28 additions & 5 deletions docs/docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,43 @@ Auto-detects your dbt projects, warehouse credentials, and installed tools. See

---

## Step 4: Build Your First Artifact
## Step 4: Verify It Works

In the TUI, try these prompts or describe your own use case:
In the TUI, type a simple prompt to confirm everything is connected:

```
What SQL anti-patterns does this query have: SELECT * FROM orders o JOIN customers c ON o.id = c.order_id WHERE UPPER(c.name) = 'ACME'
```

Look at my snowflake account and do a comprehensive Analysis our Snowflake credit consumption over the last 30 days. After doing this generate a dashboard for my consumption.
If you connected a warehouse with `/discover`, try:

```

Show me the tables in my warehouse
```

Build me a real time, interactive dashboard for my macbook system metrics and health. Use python, iceberg, dbt for various time slices.
If you have a dbt project, try:

```
Scan my dbt project and summarize the models
```
Comment on lines +76 to +90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add languages to these fenced examples.

markdownlint is already flagging Lines 76, 82, and 88 for missing fence languages. Mark them as text so the docs stay lint-clean.

Suggested markdown fix
-```
+```text
 What SQL anti-patterns does this query have: SELECT * FROM orders o JOIN customers c ON o.id = c.order_id WHERE UPPER(c.name) = 'ACME'

@@
- +text
Show me the tables in my warehouse

@@
-```
+```text
Scan my dbt project and summarize the models
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.21.0)</summary>

[warning] 76-76: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

---

[warning] 82-82: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

---

[warning] 88-88: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/docs/quickstart.md around lines 76 - 90, Update the three fenced code
blocks that currently lack a language tag by adding the language specifier
text to each opening fence; specifically change the blocks containing the
lines "What SQL anti-patterns does this query have: SELECT * FROM orders o JOIN
customers c ON o.id = c.order_id WHERE UPPER(c.name) = 'ACME'", "Show me the
tables in my warehouse", and "Scan my dbt project and summarize the models" so
their opening fences read text instead of to satisfy markdownlint.


</details>

<!-- fingerprinting:phantom:medusa:grasshopper -->

<!-- This is an auto-generated comment by CodeRabbit -->


---

## Step 5: Explore Data Engineering Features

Once basics are working, explore these commands:

| Command | What it does |
|---------|-------------|
| `/sql-review` | Review SQL for correctness, performance, and best practices |
| `/cost-report` | Analyze warehouse spending and find optimization opportunities |
| `/dbt-docs` | Generate or improve dbt model documentation |
| `/generate-tests` | Auto-generate dbt tests for your models |
| `/migrate-sql` | Translate SQL between warehouse dialects |
| `/ci-check` | Run pre-merge SQL quality validation on changed files |
| `/train @docs/style-guide.md` | Import team standards from documentation |

**Pro tip:** Use `impact_analysis` before making breaking changes to understand which downstream dbt models will be affected.

---

Expand All @@ -92,3 +114,4 @@ Build me a real time, interactive dashboard for my macbook system metrics and he
- [Full Setup](getting-started.md): All warehouse configs, LLM providers, advanced setup
- [Agent Modes](data-engineering/agent-modes.md): Choose the right agent for your task
- [CI & Automation](data-engineering/guides/ci-headless.md): Run altimate in automated pipelines
- Train your AI teammate: Use `/teach` and `/train` to build team-specific knowledge that persists across sessions
294 changes: 294 additions & 0 deletions packages/opencode/src/altimate/tools/impact-analysis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
// altimate_change - Impact analysis tool for dbt DAG-aware change assessment
//
// Combines dbt manifest parsing with column-level lineage to show downstream
// impact of model/column changes across the entire DAG.
import z from "zod"
import { Tool } from "../../tool/tool"
import { Dispatcher } from "../native"

export const ImpactAnalysisTool = Tool.define("impact_analysis", {
description: [
"Analyze the downstream impact of a model or column change across the dbt DAG.",
"Combines dbt manifest parsing with column-level lineage to show all affected",
"models, tests, exposures, and sources. Use before making breaking changes to",
"understand blast radius.",
Comment on lines +10 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The tool never reports exposures or sources.

The description says this covers affected “exposures, and sources”, but the execution path only gathers models, tests, and columnImpact. That under-reports real consumers of breaking changes and makes the output misleading until manifest.exposures and sources are traversed too.

Also applies to: 115-145

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/altimate/tools/impact-analysis.ts` around lines 10 -
14, The impact analysis currently collects models, tests and columnImpact but
never traverses manifest.exposures or manifest.sources, so update the execution
path (the code that builds the impact result alongside columnImpact and impacted
models/tests) to also iterate manifest.exposures and manifest.sources, compute
whether each exposure/source is affected by the same lineage/columnImpact logic,
and include them in the final result object (the impact summary returned by the
function that aggregates models/tests/columnImpact). Use the same
mapping/filtering logic you apply to models and tests so exposures and sources
are added to the impactedConsumers list (or equivalent fields) and ensure the
final output includes these new arrays.

"",
"Examples:",
'- impact_analysis({ model: "stg_orders", change_type: "remove" })',
'- impact_analysis({ model: "stg_orders", column: "order_id", change_type: "rename" })',
'- impact_analysis({ manifest_path: "target/manifest.json", model: "dim_customers", change_type: "retype" })',
].join("\n"),
parameters: z.object({
model: z
.string()
.describe("dbt model name to analyze impact for (e.g., 'stg_orders', 'dim_customers')"),
column: z
.string()
.optional()
.describe("Specific column to trace impact for. If omitted, analyzes model-level impact."),
change_type: z
.enum(["remove", "rename", "retype", "add", "modify"])
.describe("Type of change being considered"),
manifest_path: z
.string()
.optional()
.default("target/manifest.json")
.describe("Path to dbt manifest.json file"),
dialect: z
.string()
.optional()
.default("snowflake")
.describe("SQL dialect for lineage analysis"),
}),
// @ts-expect-error tsgo TS2719 false positive — identical pattern works in other tools
async execute(args, ctx) {
try {
// Step 1: Parse the dbt manifest to get the full DAG
const manifest = await Dispatcher.call("dbt.manifest", { path: args.manifest_path })

if (!manifest.models || manifest.models.length === 0) {
return {
title: "Impact: NO MANIFEST",
metadata: { success: false },
output: `No models found in manifest at ${args.manifest_path}. Run \`dbt compile\` first to generate the manifest.`,
}
}

// Step 2: Find the target model and its downstream dependents
const targetModel = manifest.models.find(
(m: { name: string }) => m.name === args.model || m.name.endsWith(`.${args.model}`),
)

if (!targetModel) {
const available = manifest.models
.slice(0, 10)
.map((m: { name: string }) => m.name)
.join(", ")
return {
title: "Impact: MODEL NOT FOUND",
metadata: { success: false },
output: `Model "${args.model}" not found in manifest. Available models: ${available}${manifest.models.length > 10 ? ` (+${manifest.models.length - 10} more)` : ""}`,
}
}

// Step 3: Build the dependency graph and find all downstream models
const modelsByName = new Map<string, any>()
for (const m of manifest.models) {
modelsByName.set(m.name, m)
}

// Find all models that depend on the target (direct + transitive)
const downstream = findDownstream(args.model, manifest.models)
const direct = downstream.filter((d) => d.depth === 1)
const transitive = downstream.filter((d) => d.depth > 1)

// Step 4: Report test count (manifest has test_count but not individual tests)
const affectedTestCount = manifest.test_count ?? 0
Comment on lines +85 to +86
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

affectedTestCount reports total project tests, not tests specific to the changed model.

manifest.test_count is the total test count for the entire project, not tests that depend on or reference the target model. This misleads users into thinking all project tests are affected by the change. Either filter tests by their depends_on to find those referencing the target model, or clarify in the output that this is the total project test count.

🔧 Suggested approach

If the manifest includes individual test nodes with depends_on arrays, filter them:

-      // Step 4: Report test count (manifest has test_count but not individual tests)
-      const affectedTestCount = manifest.test_count ?? 0
+      // Step 4: Find tests that depend on the target model or its downstream
+      const affectedModels = new Set([args.model, ...downstream.map(d => d.name)])
+      const affectedTests = (manifest.tests ?? []).filter((t: { depends_on?: string[] }) =>
+        t.depends_on?.some(dep => {
+          const leafName = dep.split(".").pop()
+          return leafName && affectedModels.has(leafName)
+        })
+      )
+      const affectedTestCount = affectedTests.length

Or update the output label to clarify it's the total project count.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/altimate/tools/impact-analysis.ts` around lines 85 -
86, The variable affectedTestCount is set from manifest.test_count which is the
total project test count, not tests referencing the target model; update
impact-analysis.ts so that when manifest contains individual test nodes you
compute affectedTestCount by filtering those test nodes by their depends_on
arrays for references to the target model (use the same target identifier used
earlier in the function), falling back to manifest.test_count only if individual
test data is absent, or alternatively change the output label to explicitly
state "total project test count" when using manifest.test_count; refer to
affectedTestCount, manifest.test_count and the tests' depends_on fields to
implement this change.


// Step 5: If column specified, attempt column-level lineage
let columnImpact: string[] = []
if (args.column) {
try {
const lineageResult = await Dispatcher.call("lineage.check", {
sql: `SELECT * FROM ${args.model}`, // Use model reference for lineage tracing
dialect: args.dialect,
})
if (lineageResult.data?.column_dict) {
// Find which downstream columns reference our target column
for (const [outCol, sources] of Object.entries(lineageResult.data.column_dict)) {
const srcArray = Array.isArray(sources) ? sources : [sources]
if (srcArray.some((s: any) => JSON.stringify(s).includes(args.column!))) {
columnImpact.push(outCol)
}
}
Comment on lines +91 to +103
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Column lineage approach won't identify actual downstream column consumers.

Using SELECT * FROM ${args.model} only analyzes the model's own output columns, not how downstream models consume them. The column_dict from this query shows the target model's output columns, not which downstream models reference args.column. To properly trace column impact, you'd need to analyze each downstream model's SQL and check if it references the target column.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/altimate/tools/impact-analysis.ts` around lines 91 -
103, The current approach calls Dispatcher.call("lineage.check") with `SELECT *
FROM ${args.model}` which only returns the model's own output columns; instead,
for accurate downstream consumers you must fetch the list of downstream models
and run `Dispatcher.call("lineage.check")` on each downstream model's SQL (or
its compiled SQL) to inspect their `column_dict` for references to
`args.column`, then add matching downstream output columns or model identifiers
to `columnImpact`; update the logic around `Dispatcher.call("lineage.check")`,
replace the single-query check with iterating downstream models, and use
`args.model`, `args.column`, `Dispatcher.call`, `columnImpact`, and the
downstream model identifiers to locate and update the impacted entries.

}
} catch {
// Column lineage is best-effort — continue without it
}
}

// Step 6: Format the impact report
const output = formatImpactReport({
model: args.model,
column: args.column,
changeType: args.change_type,
direct,
transitive,
affectedTestCount,
columnImpact,
totalModels: manifest.model_count,
})

const totalAffected = downstream.length
const severity =
totalAffected === 0
? "SAFE"
: totalAffected <= 3
? "LOW"
: totalAffected <= 10
? "MEDIUM"
: "HIGH"

return {
title: `Impact: ${severity} — ${totalAffected} downstream model${totalAffected !== 1 ? "s" : ""} affected`,
metadata: {
success: true,
severity,
direct_count: direct.length,
transitive_count: transitive.length,
test_count: affectedTestCount,
column_impact: columnImpact.length,
},
Comment on lines +88 to +141
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Column-specific runs still report model-level blast radius.

When args.column is set, downstream, affectedTests, and the severity/title are still computed from every descendant of the model before any column filtering happens. A rename or removal of an unused column will therefore be reported as impacting the whole subtree; this needs to narrow the downstream set to actual column consumers or fall back to model-level analysis only when no column is provided.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/altimate/tools/impact-analysis.ts` around lines 92 -
145, When args.column is provided, narrow the reported blast radius to only
models that actually consume that column instead of using the full downstream
set: after computing columnImpact, derive a filteredDownstream set (use
columnImpact entries to identify affected downstream model names) and, if
filteredDownstream is non-empty, use it to compute totalAffected, severity,
title and metadata counts (direct/transitive/test counts and column_impact) and
to filter affectedTests to tests that target those filtered models; if
columnImpact is empty, fall back to the existing model-level analysis. Update
any usages of downstream, affectedTests, totalAffected, severity, and the
title/metadata before calling formatImpactReport so the report reflects
column-level consumers when args.column is set.

output,
}
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
return {
title: "Impact: ERROR",
metadata: { success: false },
output: `Failed to analyze impact: ${msg}\n\nEnsure the dbt manifest exists (run \`dbt compile\`) and the dispatcher is running.`,
}
}
},
})

interface DownstreamModel {
name: string
depth: number
materialized?: string
path: string[]
}

function findDownstream(
targetName: string,
models: Array<{ name: string; depends_on: string[]; materialized?: string }>,
): DownstreamModel[] {
const results: DownstreamModel[] = []
const visited = new Set<string>()

function walk(name: string, depth: number, path: string[]) {
for (const model of models) {
if (visited.has(model.name)) continue
const deps = model.depends_on.map((d) => d.split(".").pop())
if (deps.includes(name)) {
visited.add(model.name)
const newPath = [...path, model.name]
results.push({
name: model.name,
depth,
materialized: model.materialized,
path: newPath,
})
walk(model.name, depth + 1, newPath)
}
}
}

walk(targetName, 1, [targetName])
return results
}

function formatImpactReport(data: {
model: string
column?: string
changeType: string
direct: DownstreamModel[]
transitive: DownstreamModel[]
affectedTestCount: number
columnImpact: string[]
totalModels: number
}): string {
const lines: string[] = []

// Header
const target = data.column ? `${data.model}.${data.column}` : data.model
lines.push(`Impact Analysis: ${data.changeType.toUpperCase()} ${target}`)
lines.push("".padEnd(60, "="))

const totalAffected = data.direct.length + data.transitive.length
const pct = data.totalModels > 0 ? ((totalAffected / data.totalModels) * 100).toFixed(1) : "0"
lines.push(`Blast radius: ${totalAffected}/${data.totalModels} models (${pct}%)`)
lines.push("")

// Risk assessment
if (data.changeType === "remove" && totalAffected > 0) {
lines.push("WARNING: This is a BREAKING change. All downstream models will fail.")
lines.push("")
} else if (data.changeType === "rename" && totalAffected > 0) {
lines.push("WARNING: Rename requires updating all downstream references.")
lines.push("")
} else if (data.changeType === "retype" && totalAffected > 0) {
lines.push("CAUTION: Type change may cause implicit casts or failures in downstream models.")
lines.push("")
}

// Direct dependents
if (data.direct.length > 0) {
lines.push(`Direct Dependents (${data.direct.length})`)
lines.push("".padEnd(40, "-"))
for (const d of data.direct) {
const mat = d.materialized ? ` [${d.materialized}]` : ""
lines.push(` ${d.name}${mat}`)
}
lines.push("")
}

// Transitive dependents
if (data.transitive.length > 0) {
lines.push(`Transitive Dependents (${data.transitive.length})`)
lines.push("".padEnd(40, "-"))
for (const d of data.transitive) {
const mat = d.materialized ? ` [${d.materialized}]` : ""
const path = d.path.join(" → ")
lines.push(` ${d.name}${mat} (via: ${path})`)
}
lines.push("")
}

// Column impact
if (data.column && data.columnImpact.length > 0) {
lines.push(`Affected Output Columns (${data.columnImpact.length})`)
lines.push("".padEnd(40, "-"))
for (const col of data.columnImpact) {
lines.push(` ${col}`)
}
lines.push("")
}

// Affected tests
if (data.affectedTestCount > 0) {
lines.push(`Tests in project: ${data.affectedTestCount}`)
lines.push("".padEnd(40, "-"))
lines.push(` Run \`dbt test\` to verify all ${data.affectedTestCount} tests still pass after this change.`)
lines.push("")
}

// No impact
if (totalAffected === 0) {
lines.push("No downstream models depend on this. Change is safe to make.")
}

// Recommendations
if (totalAffected > 0) {
lines.push("Recommended Actions")
lines.push("".padEnd(40, "-"))
if (data.changeType === "remove") {
lines.push("1. Update all downstream models to remove references")
lines.push("2. Run `dbt test` to verify no broken references")
lines.push("3. Consider deprecation period before removal")
} else if (data.changeType === "rename") {
lines.push("1. Update all downstream SQL references to new name")
lines.push("2. Run `dbt compile` to verify all models compile")
lines.push("3. Run `dbt test` to verify correctness")
} else if (data.changeType === "retype") {
lines.push("1. Check downstream models for implicit type casts")
lines.push("2. Verify aggregations and joins still work correctly")
lines.push("3. Run `dbt test` with data validation")
} else {
lines.push("1. Review downstream models for compatibility")
lines.push("2. Run `dbt compile` and `dbt test`")
}
}

return lines.join("\n")
}
Loading
Loading