diff --git a/packages/opencode/src/altimate/native/sql/register.ts b/packages/opencode/src/altimate/native/sql/register.ts index caae23c83..c65911127 100644 --- a/packages/opencode/src/altimate/native/sql/register.ts +++ b/packages/opencode/src/altimate/native/sql/register.ts @@ -45,6 +45,7 @@ register("sql.analyze", async (params) => { for (const f of lint.findings ?? []) { issues.push({ type: "lint", + rule: f.rule, severity: f.severity ?? "warning", message: f.message ?? f.rule ?? "", recommendation: f.suggestion ?? "", diff --git a/packages/opencode/src/altimate/native/types.ts b/packages/opencode/src/altimate/native/types.ts index dc6108393..8d0f3978f 100644 --- a/packages/opencode/src/altimate/native/types.ts +++ b/packages/opencode/src/altimate/native/types.ts @@ -30,6 +30,7 @@ export interface SqlAnalyzeParams { export interface SqlAnalyzeIssue { type: string + rule?: string severity: string message: string recommendation: string diff --git a/packages/opencode/src/altimate/telemetry/index.ts b/packages/opencode/src/altimate/telemetry/index.ts index 25ae60415..da7cac4bb 100644 --- a/packages/opencode/src/altimate/telemetry/index.ts +++ b/packages/opencode/src/altimate/telemetry/index.ts @@ -405,6 +405,21 @@ export namespace Telemetry { masked_args?: string duration_ms: number } + // altimate_change start — sql quality telemetry for issue prevention metrics + | { + type: "sql_quality" + timestamp: number + session_id: string + tool_name: string + tool_category: string + finding_count: number + /** JSON-encoded Record — count per issue category */ + by_category: string + has_schema: boolean + dialect?: string + duration_ms: number + } + // altimate_change end const ERROR_PATTERNS: Array<{ class: Telemetry.Event & { type: "core_failure" } extends { error_class: infer C } ? C : never @@ -774,6 +789,22 @@ export namespace Telemetry { } } + // altimate_change start — sql quality telemetry types + /** Lightweight finding record for quality telemetry. Only category — never SQL content. */ + export interface Finding { + category: string + } + + /** Aggregate an array of findings into category counts suitable for the sql_quality event. */ + export function aggregateFindings(findings: Finding[]): Record { + const by_category: Record = {} + for (const f of findings) { + by_category[f.category] = (by_category[f.category] ?? 0) + 1 + } + return by_category + } + // altimate_change end + export async function shutdown() { // Wait for init to complete so we know whether telemetry is enabled // and have a valid endpoint to flush to. init() is fire-and-forget diff --git a/packages/opencode/src/altimate/tools/altimate-core-check.ts b/packages/opencode/src/altimate/tools/altimate-core-check.ts index 3785888f4..6678fcb33 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-check.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-check.ts @@ -1,6 +1,7 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" +import type { Telemetry } from "../telemetry" export const AltimateCoreCheckTool = Tool.define("altimate_core_check", { description: @@ -11,6 +12,7 @@ export const AltimateCoreCheckTool = Tool.define("altimate_core_check", { schema_context: z.record(z.string(), z.any()).optional().describe("Inline schema definition"), }), async execute(args, ctx) { + const hasSchema = !!(args.schema_path || (args.schema_context && Object.keys(args.schema_context).length > 0)) try { const result = await Dispatcher.call("altimate_core.check", { sql: args.sql, @@ -19,14 +21,34 @@ export const AltimateCoreCheckTool = Tool.define("altimate_core_check", { }) const data = (result.data ?? {}) as Record const error = result.error ?? data.error + // altimate_change start — sql quality findings for telemetry + const findings: Telemetry.Finding[] = [] + for (const err of data.validation?.errors ?? []) { + findings.push({ category: "validation_error" }) + } + for (const f of data.lint?.findings ?? []) { + findings.push({ category: f.rule ?? "lint" }) + } + for (const t of data.safety?.threats ?? []) { + findings.push({ category: t.type ?? "safety_threat" }) + } + for (const p of data.pii?.findings ?? []) { + findings.push({ category: "pii_detected" }) + } + // altimate_change end return { title: `Check: ${formatCheckTitle(data)}`, - metadata: { success: result.success, ...(error && { error }) }, + metadata: { + success: result.success, + has_schema: hasSchema, + ...(error && { error }), + ...(findings.length > 0 && { findings }), + }, output: formatCheck(data), } } catch (e) { const msg = e instanceof Error ? e.message : String(e) - return { title: "Check: ERROR", metadata: { success: false, error: msg }, output: `Failed: ${msg}` } + return { title: "Check: ERROR", metadata: { success: false, has_schema: hasSchema, error: msg }, output: `Failed: ${msg}` } } }, }) diff --git a/packages/opencode/src/altimate/tools/altimate-core-correct.ts b/packages/opencode/src/altimate/tools/altimate-core-correct.ts index 92135e34a..b318f3736 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-correct.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-correct.ts @@ -1,6 +1,7 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" +import type { Telemetry } from "../telemetry" export const AltimateCoreCorrectTool = Tool.define("altimate_core_correct", { description: @@ -11,6 +12,7 @@ export const AltimateCoreCorrectTool = Tool.define("altimate_core_correct", { schema_context: z.record(z.string(), z.any()).optional().describe("Inline schema definition"), }), async execute(args, ctx) { + const hasSchema = !!(args.schema_path || (args.schema_context && Object.keys(args.schema_context).length > 0)) try { const result = await Dispatcher.call("altimate_core.correct", { sql: args.sql, @@ -19,14 +21,26 @@ export const AltimateCoreCorrectTool = Tool.define("altimate_core_correct", { }) const data = (result.data ?? {}) as Record const error = result.error ?? data.error ?? extractCorrectErrors(data) + // altimate_change start — sql quality findings for telemetry + const changes = Array.isArray(data.changes) ? data.changes : [] + const findings: Telemetry.Finding[] = changes.map(() => ({ + category: "correction_applied", + })) + // altimate_change end return { title: `Correct: ${data.success ? "CORRECTED" : "COULD NOT CORRECT"}`, - metadata: { success: result.success, iterations: data.iterations, ...(error && { error }) }, + metadata: { + success: result.success, + iterations: data.iterations, + has_schema: hasSchema, + ...(error && { error }), + ...(findings.length > 0 && { findings }), + }, output: formatCorrect(data), } } catch (e) { const msg = e instanceof Error ? e.message : String(e) - return { title: "Correct: ERROR", metadata: { success: false, iterations: 0, error: msg }, output: `Failed: ${msg}` } + return { title: "Correct: ERROR", metadata: { success: false, iterations: 0, has_schema: hasSchema, error: msg }, output: `Failed: ${msg}` } } }, }) diff --git a/packages/opencode/src/altimate/tools/altimate-core-equivalence.ts b/packages/opencode/src/altimate/tools/altimate-core-equivalence.ts index fa0f8b267..4584ece6b 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-equivalence.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-equivalence.ts @@ -1,6 +1,7 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" +import type { Telemetry } from "../telemetry" export const AltimateCoreEquivalenceTool = Tool.define("altimate_core_equivalence", { description: @@ -12,9 +13,10 @@ export const AltimateCoreEquivalenceTool = Tool.define("altimate_core_equivalenc schema_context: z.record(z.string(), z.any()).optional().describe("Inline schema definition"), }), async execute(args, ctx) { - if (!args.schema_path && (!args.schema_context || Object.keys(args.schema_context).length === 0)) { + const hasSchema = !!(args.schema_path || (args.schema_context && Object.keys(args.schema_context).length > 0)) + if (!hasSchema) { const error = "No schema provided. Provide schema_context or schema_path so table/column references can be resolved." - return { title: "Equivalence: NO SCHEMA", metadata: { success: false, equivalent: false, error }, output: `Error: ${error}` } + return { title: "Equivalence: NO SCHEMA", metadata: { success: false, equivalent: false, has_schema: false, error }, output: `Error: ${error}` } } try { const result = await Dispatcher.call("altimate_core.equivalence", { @@ -28,14 +30,28 @@ export const AltimateCoreEquivalenceTool = Tool.define("altimate_core_equivalenc // "Not equivalent" is a valid analysis result, not a failure. // Only treat it as failure when there's an actual error. const isRealFailure = !!error + // altimate_change start — sql quality findings for telemetry + const findings: Telemetry.Finding[] = [] + if (!data.equivalent && data.differences?.length) { + for (const d of data.differences) { + findings.push({ category: "equivalence_difference" }) + } + } + // altimate_change end return { title: isRealFailure ? "Equivalence: ERROR" : `Equivalence: ${data.equivalent ? "EQUIVALENT" : "DIFFERENT"}`, - metadata: { success: !isRealFailure, equivalent: data.equivalent, ...(error && { error }) }, + metadata: { + success: !isRealFailure, + equivalent: data.equivalent, + has_schema: hasSchema, + ...(error && { error }), + ...(findings.length > 0 && { findings }), + }, output: formatEquivalence(data), } } catch (e) { const msg = e instanceof Error ? e.message : String(e) - return { title: "Equivalence: ERROR", metadata: { success: false, equivalent: false, error: msg }, output: `Failed: ${msg}` } + return { title: "Equivalence: ERROR", metadata: { success: false, equivalent: false, has_schema: hasSchema, error: msg }, output: `Failed: ${msg}` } } }, }) diff --git a/packages/opencode/src/altimate/tools/altimate-core-fix.ts b/packages/opencode/src/altimate/tools/altimate-core-fix.ts index e4002101c..b2ce392c9 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-fix.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-fix.ts @@ -1,6 +1,7 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" +import type { Telemetry } from "../telemetry" export const AltimateCoreFixTool = Tool.define("altimate_core_fix", { description: @@ -12,6 +13,7 @@ export const AltimateCoreFixTool = Tool.define("altimate_core_fix", { max_iterations: z.number().optional().describe("Maximum fix iterations (default: 5)"), }), async execute(args, ctx) { + const hasSchema = !!(args.schema_path || (args.schema_context && Object.keys(args.schema_context).length > 0)) try { const result = await Dispatcher.call("altimate_core.fix", { sql: args.sql, @@ -24,14 +26,29 @@ export const AltimateCoreFixTool = Tool.define("altimate_core_fix", { // post_fix_valid=true with no errors means SQL was already valid (nothing to fix) const alreadyValid = data.post_fix_valid && !error const success = result.success || alreadyValid + // altimate_change start — sql quality findings for telemetry + const findings: Telemetry.Finding[] = [] + for (const fix of data.fixes_applied ?? data.changes ?? []) { + findings.push({ category: "fix_applied" }) + } + for (const err of data.unfixable_errors ?? []) { + findings.push({ category: "unfixable_error" }) + } + // altimate_change end return { title: `Fix: ${alreadyValid ? "ALREADY VALID" : data.fixed ? "FIXED" : "COULD NOT FIX"}`, - metadata: { success, fixed: !!data.fixed_sql, ...(error && { error }) }, + metadata: { + success, + fixed: !!data.fixed_sql, + has_schema: hasSchema, + ...(error && { error }), + ...(findings.length > 0 && { findings }), + }, output: formatFix(data), } } catch (e) { const msg = e instanceof Error ? e.message : String(e) - return { title: "Fix: ERROR", metadata: { success: false, fixed: false, error: msg }, output: `Failed: ${msg}` } + return { title: "Fix: ERROR", metadata: { success: false, fixed: false, has_schema: hasSchema, error: msg }, output: `Failed: ${msg}` } } }, }) diff --git a/packages/opencode/src/altimate/tools/altimate-core-policy.ts b/packages/opencode/src/altimate/tools/altimate-core-policy.ts index 26763a5fe..99bc056f8 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-policy.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-policy.ts @@ -1,6 +1,7 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" +import type { Telemetry } from "../telemetry" export const AltimateCorePolicyTool = Tool.define("altimate_core_policy", { description: @@ -12,6 +13,7 @@ export const AltimateCorePolicyTool = Tool.define("altimate_core_policy", { schema_context: z.record(z.string(), z.any()).optional().describe("Inline schema definition"), }), async execute(args, ctx) { + const hasSchema = !!(args.schema_path || (args.schema_context && Object.keys(args.schema_context).length > 0)) try { const result = await Dispatcher.call("altimate_core.policy", { sql: args.sql, @@ -21,14 +23,26 @@ export const AltimateCorePolicyTool = Tool.define("altimate_core_policy", { }) const data = (result.data ?? {}) as Record const error = result.error ?? data.error + // altimate_change start — sql quality findings for telemetry + const violations = Array.isArray(data.violations) ? data.violations : [] + const findings: Telemetry.Finding[] = violations.map((v: any) => ({ + category: v.rule ?? "policy_violation", + })) + // altimate_change end return { title: `Policy: ${data.pass ? "PASS" : "VIOLATIONS FOUND"}`, - metadata: { success: result.success, pass: data.pass, ...(error && { error }) }, + metadata: { + success: true, // engine ran — violations are findings, not failures + pass: data.pass, + has_schema: hasSchema, + ...(error && { error }), + ...(findings.length > 0 && { findings }), + }, output: formatPolicy(data), } } catch (e) { const msg = e instanceof Error ? e.message : String(e) - return { title: "Policy: ERROR", metadata: { success: false, pass: false, error: msg }, output: `Failed: ${msg}` } + return { title: "Policy: ERROR", metadata: { success: false, pass: false, has_schema: hasSchema, error: msg }, output: `Failed: ${msg}` } } }, }) diff --git a/packages/opencode/src/altimate/tools/altimate-core-semantics.ts b/packages/opencode/src/altimate/tools/altimate-core-semantics.ts index 42257f7d6..0211b7c80 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-semantics.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-semantics.ts @@ -1,6 +1,7 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" +import type { Telemetry } from "../telemetry" export const AltimateCoreSemanticsTool = Tool.define("altimate_core_semantics", { description: @@ -11,9 +12,10 @@ export const AltimateCoreSemanticsTool = Tool.define("altimate_core_semantics", schema_context: z.record(z.string(), z.any()).optional().describe("Inline schema definition"), }), async execute(args, ctx) { - if (!args.schema_path && (!args.schema_context || Object.keys(args.schema_context).length === 0)) { + const hasSchema = !!(args.schema_path || (args.schema_context && Object.keys(args.schema_context).length > 0)) + if (!hasSchema) { const error = "No schema provided. Provide schema_context or schema_path so table/column references can be resolved." - return { title: "Semantics: NO SCHEMA", metadata: { success: false, valid: false, issue_count: 0, error }, output: `Error: ${error}` } + return { title: "Semantics: NO SCHEMA", metadata: { success: false, valid: false, issue_count: 0, has_schema: false, error }, output: `Error: ${error}` } } try { const result = await Dispatcher.call("altimate_core.semantics", { @@ -25,14 +27,27 @@ export const AltimateCoreSemanticsTool = Tool.define("altimate_core_semantics", const issueCount = data.issues?.length ?? 0 const error = result.error ?? data.error ?? extractSemanticsErrors(data) const hasError = Boolean(error) + // altimate_change start — sql quality findings for telemetry + const issues = Array.isArray(data.issues) ? data.issues : [] + const findings: Telemetry.Finding[] = issues.map(() => ({ + category: "semantic_issue", + })) + // altimate_change end return { title: hasError ? "Semantics: ERROR" : `Semantics: ${data.valid ? "VALID" : `${issueCount} issues`}`, - metadata: { success: result.success, valid: data.valid, issue_count: issueCount, ...(error && { error }) }, + metadata: { + success: true, // engine ran — semantic issues are findings, not failures + valid: data.valid, + issue_count: issueCount, + has_schema: hasSchema, + ...(error && { error }), + ...(findings.length > 0 && { findings }), + }, output: formatSemantics(hasError ? { ...data, error } : data), } } catch (e) { const msg = e instanceof Error ? e.message : String(e) - return { title: "Semantics: ERROR", metadata: { success: false, valid: false, issue_count: 0, error: msg }, output: `Failed: ${msg}` } + return { title: "Semantics: ERROR", metadata: { success: false, valid: false, issue_count: 0, has_schema: hasSchema, error: msg }, output: `Failed: ${msg}` } } }, }) diff --git a/packages/opencode/src/altimate/tools/altimate-core-validate.ts b/packages/opencode/src/altimate/tools/altimate-core-validate.ts index c96d0ed61..9ff9da7bc 100644 --- a/packages/opencode/src/altimate/tools/altimate-core-validate.ts +++ b/packages/opencode/src/altimate/tools/altimate-core-validate.ts @@ -1,6 +1,7 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" +import type { Telemetry } from "../telemetry" export const AltimateCoreValidateTool = Tool.define("altimate_core_validate", { description: @@ -11,10 +12,11 @@ export const AltimateCoreValidateTool = Tool.define("altimate_core_validate", { schema_context: z.record(z.string(), z.any()).optional().describe("Inline schema definition"), }), async execute(args, ctx) { - const noSchema = !args.schema_path && (!args.schema_context || Object.keys(args.schema_context).length === 0) + const hasSchema = !!(args.schema_path || (args.schema_context && Object.keys(args.schema_context).length > 0)) + const noSchema = !hasSchema if (noSchema) { const error = "No schema provided. Provide schema_context or schema_path so table/column references can be resolved." - return { title: "Validate: NO SCHEMA", metadata: { success: false, valid: false, error }, output: `Error: ${error}` } + return { title: "Validate: NO SCHEMA", metadata: { success: false, valid: false, has_schema: false, error }, output: `Error: ${error}` } } try { const result = await Dispatcher.call("altimate_core.validate", { @@ -24,14 +26,26 @@ export const AltimateCoreValidateTool = Tool.define("altimate_core_validate", { }) const data = (result.data ?? {}) as Record const error = result.error ?? data.error ?? extractValidationErrors(data) + // altimate_change start — sql quality findings for telemetry + const errors = Array.isArray(data.errors) ? data.errors : [] + const findings: Telemetry.Finding[] = errors.map((err: any) => ({ + category: classifyValidationError(err.message ?? ""), + })) + // altimate_change end return { title: `Validate: ${data.valid ? "VALID" : "INVALID"}`, - metadata: { success: result.success, valid: data.valid, ...(error && { error }) }, + metadata: { + success: true, // engine ran — validation errors are findings, not failures + valid: data.valid, + has_schema: hasSchema, + ...(error && { error }), + ...(findings.length > 0 && { findings }), + }, output: formatValidate(data), } } catch (e) { const msg = e instanceof Error ? e.message : String(e) - return { title: "Validate: ERROR", metadata: { success: false, valid: false, error: msg }, output: `Failed: ${msg}` } + return { title: "Validate: ERROR", metadata: { success: false, valid: false, has_schema: hasSchema, error: msg }, output: `Failed: ${msg}` } } }, }) @@ -44,6 +58,16 @@ function extractValidationErrors(data: Record): string | undefined return undefined } +function classifyValidationError(message: string): string { + const lower = message.toLowerCase() + // Column check before table — "column not found in table" would match both + if (lower.includes("column") && lower.includes("not found")) return "missing_column" + if (lower.includes("table") && lower.includes("not found")) return "missing_table" + if (lower.includes("syntax")) return "syntax_error" + if (lower.includes("type")) return "type_mismatch" + return "validation_error" +} + function formatValidate(data: Record): string { if (data.error) return `Error: ${data.error}` if (data.valid) return "SQL is valid." diff --git a/packages/opencode/src/altimate/tools/impact-analysis.ts b/packages/opencode/src/altimate/tools/impact-analysis.ts index b219678a2..d99ad751f 100644 --- a/packages/opencode/src/altimate/tools/impact-analysis.ts +++ b/packages/opencode/src/altimate/tools/impact-analysis.ts @@ -5,6 +5,7 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" +import type { Telemetry } from "../telemetry" export const ImpactAnalysisTool = Tool.define("impact_analysis", { description: [ @@ -129,6 +130,18 @@ export const ImpactAnalysisTool = Tool.define("impact_analysis", { ? "MEDIUM" : "HIGH" + // altimate_change start — sql quality findings for telemetry + const findings: Telemetry.Finding[] = [] + if (totalAffected > 0) { + findings.push({ category: `impact_${severity.toLowerCase()}` }) + for (const d of direct) { + findings.push({ category: "impact_direct_dependent" }) + } + for (const t of transitive) { + findings.push({ category: "impact_transitive_dependent" }) + } + } + // altimate_change end return { title: `Impact: ${severity} — ${totalAffected} downstream model${totalAffected !== 1 ? "s" : ""} affected`, metadata: { @@ -138,6 +151,8 @@ export const ImpactAnalysisTool = Tool.define("impact_analysis", { transitive_count: transitive.length, test_count: affectedTestCount, column_impact: columnImpact.length, + has_schema: false, + ...(findings.length > 0 && { findings }), }, output, } @@ -145,7 +160,7 @@ export const ImpactAnalysisTool = Tool.define("impact_analysis", { const msg = e instanceof Error ? e.message : String(e) return { title: "Impact: ERROR", - metadata: { success: false, error: msg }, + metadata: { success: false, has_schema: false, error: msg }, output: `Failed to analyze impact: ${msg}\n\nEnsure the dbt manifest exists (run \`dbt compile\`) and the dispatcher is running.`, } } diff --git a/packages/opencode/src/altimate/tools/schema-diff.ts b/packages/opencode/src/altimate/tools/schema-diff.ts index a1a42362b..68feab287 100644 --- a/packages/opencode/src/altimate/tools/schema-diff.ts +++ b/packages/opencode/src/altimate/tools/schema-diff.ts @@ -2,6 +2,7 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" import type { SchemaDiffResult, ColumnChange } from "../native/types" +import type { Telemetry } from "../telemetry" export const SchemaDiffTool = Tool.define("schema_diff", { description: @@ -31,6 +32,11 @@ export const SchemaDiffTool = Tool.define("schema_diff", { const changeCount = result.changes.length const breakingCount = result.changes.filter((c) => c.severity === "breaking").length + // altimate_change start — sql quality findings for telemetry + const findings: Telemetry.Finding[] = result.changes.map((c) => ({ + category: c.change_type ?? (c.severity === "breaking" ? "breaking_change" : "schema_change"), + })) + // altimate_change end return { title: `Schema Diff: ${result.success ? `${changeCount} change${changeCount !== 1 ? "s" : ""}${breakingCount > 0 ? ` (${breakingCount} BREAKING)` : ""}` : "PARSE ERROR"}`, metadata: { @@ -38,6 +44,9 @@ export const SchemaDiffTool = Tool.define("schema_diff", { changeCount, breakingCount, hasBreakingChanges: result.has_breaking_changes, + has_schema: false, + dialect: args.dialect, + ...(findings.length > 0 && { findings }), }, output: formatSchemaDiff(result), } @@ -45,7 +54,7 @@ export const SchemaDiffTool = Tool.define("schema_diff", { const msg = e instanceof Error ? e.message : String(e) return { title: "Schema Diff: ERROR", - metadata: { success: false, changeCount: 0, breakingCount: 0, hasBreakingChanges: false, error: msg }, + metadata: { success: false, changeCount: 0, breakingCount: 0, hasBreakingChanges: false, has_schema: false, dialect: args.dialect, error: msg }, output: `Failed to diff schema: ${msg}\n\nCheck your connection configuration and try again.`, } } diff --git a/packages/opencode/src/altimate/tools/sql-analyze.ts b/packages/opencode/src/altimate/tools/sql-analyze.ts index bccba80d3..42050d29d 100644 --- a/packages/opencode/src/altimate/tools/sql-analyze.ts +++ b/packages/opencode/src/altimate/tools/sql-analyze.ts @@ -1,6 +1,7 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" +import type { Telemetry } from "../telemetry" import type { SqlAnalyzeResult } from "../native/types" export const SqlAnalyzeTool = Tool.define("sql_analyze", { @@ -32,13 +33,21 @@ export const SqlAnalyzeTool = Tool.define("sql_analyze", { // reported via issues/issue_count). Only treat it as a failure when // there's an actual error (e.g. parse failure). const isRealFailure = !!result.error + // altimate_change start — sql quality findings for telemetry + const findings: Telemetry.Finding[] = result.issues.map((issue) => ({ + category: issue.rule ?? issue.type, + })) + // altimate_change end return { title: `Analyze: ${result.error ? "ERROR" : `${result.issue_count} issue${result.issue_count !== 1 ? "s" : ""}`} [${result.confidence}]`, metadata: { success: !isRealFailure, issueCount: result.issue_count, confidence: result.confidence, + dialect: args.dialect, + has_schema: false, ...(result.error && { error: result.error }), + ...(findings.length > 0 && { findings }), }, output: formatAnalysis(result), } @@ -46,7 +55,7 @@ export const SqlAnalyzeTool = Tool.define("sql_analyze", { const msg = e instanceof Error ? e.message : String(e) return { title: "Analyze: ERROR", - metadata: { success: false, issueCount: 0, confidence: "unknown", error: msg }, + metadata: { success: false, issueCount: 0, confidence: "unknown", dialect: args.dialect, has_schema: false, error: msg }, output: `Failed to analyze SQL: ${msg}\n\nCheck your connection configuration and try again.`, } } diff --git a/packages/opencode/src/altimate/tools/sql-optimize.ts b/packages/opencode/src/altimate/tools/sql-optimize.ts index bb8fe9163..3d1be06ef 100644 --- a/packages/opencode/src/altimate/tools/sql-optimize.ts +++ b/packages/opencode/src/altimate/tools/sql-optimize.ts @@ -2,6 +2,7 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" import type { SqlOptimizeResult, SqlOptimizeSuggestion, SqlAntiPattern } from "../native/types" +import type { Telemetry } from "../telemetry" export const SqlOptimizeTool = Tool.define("sql_optimize", { description: @@ -31,6 +32,13 @@ export const SqlOptimizeTool = Tool.define("sql_optimize", { const suggestionCount = result.suggestions.length const antiPatternCount = result.anti_patterns.length + // altimate_change start — sql quality findings for telemetry + const hasSchema = !!(args.schema_context && Object.keys(args.schema_context).length > 0) + const findings: Telemetry.Finding[] = [ + ...result.anti_patterns.map((ap) => ({ category: ap.type ?? "anti_pattern" })), + ...result.suggestions.map((s) => ({ category: s.type ?? "optimization_suggestion" })), + ] + // altimate_change end return { title: `Optimize: ${result.success ? `${suggestionCount} suggestion${suggestionCount !== 1 ? "s" : ""}, ${antiPatternCount} anti-pattern${antiPatternCount !== 1 ? "s" : ""}` : "PARSE ERROR"} [${result.confidence}]`, metadata: { @@ -39,6 +47,9 @@ export const SqlOptimizeTool = Tool.define("sql_optimize", { antiPatternCount, hasOptimizedSql: !!result.optimized_sql, confidence: result.confidence, + has_schema: hasSchema, + dialect: args.dialect, + ...(findings.length > 0 && { findings }), }, output: formatOptimization(result), } @@ -46,7 +57,7 @@ export const SqlOptimizeTool = Tool.define("sql_optimize", { const msg = e instanceof Error ? e.message : String(e) return { title: "Optimize: ERROR", - metadata: { success: false, suggestionCount: 0, antiPatternCount: 0, hasOptimizedSql: false, confidence: "unknown", error: msg }, + metadata: { success: false, suggestionCount: 0, antiPatternCount: 0, hasOptimizedSql: false, confidence: "unknown", has_schema: false, dialect: args.dialect, error: msg }, output: `Failed to optimize SQL: ${msg}\n\nCheck your connection configuration and try again.`, } } diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index bde5e3d5e..3003bc434 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -158,6 +158,25 @@ export namespace Tool { duration_ms: durationMs, }) } + // altimate_change start — emit sql_quality when tools report findings + // Only emit for successful tool runs — soft failures already emit core_failure + const findings = result.metadata?.findings as Telemetry.Finding[] | undefined + if (!isSoftFailure && Array.isArray(findings) && findings.length > 0) { + const by_category = Telemetry.aggregateFindings(findings) + Telemetry.track({ + type: "sql_quality", + timestamp: Date.now(), + session_id: ctx.sessionID, + tool_name: id, + tool_category: toolCategory, + finding_count: findings.length, + by_category: JSON.stringify(by_category), + has_schema: result.metadata?.has_schema ?? false, + ...(result.metadata?.dialect && { dialect: result.metadata.dialect as string }), + duration_ms: durationMs, + }) + } + // altimate_change end } catch { // Telemetry must never break tool execution } diff --git a/packages/opencode/test/altimate/sql-quality-telemetry.test.ts b/packages/opencode/test/altimate/sql-quality-telemetry.test.ts new file mode 100644 index 000000000..46e5ca831 --- /dev/null +++ b/packages/opencode/test/altimate/sql-quality-telemetry.test.ts @@ -0,0 +1,330 @@ +/** + * SQL Quality Telemetry Tests + * + * Verifies the aggregation logic, event payload shape, and finding + * extraction patterns used for the `sql_quality` telemetry event, and + * that scenarios with no findings result in empty finding arrays (the + * condition used by tool.ts to decide not to emit the event). + */ + +import { describe, expect, test } from "bun:test" +import { Telemetry } from "../../src/altimate/telemetry" + +// --------------------------------------------------------------------------- +// 1. aggregateFindings +// --------------------------------------------------------------------------- +describe("Telemetry.aggregateFindings", () => { + test("aggregates findings by category", () => { + const findings: Telemetry.Finding[] = [ + { category: "missing_table" }, + { category: "missing_column" }, + { category: "lint" }, + { category: "missing_table" }, + ] + const result = Telemetry.aggregateFindings(findings) + expect(result).toEqual({ + missing_table: 2, + missing_column: 1, + lint: 1, + }) + }) + + test("returns empty object for empty findings", () => { + const result = Telemetry.aggregateFindings([]) + expect(result).toEqual({}) + }) + + test("handles single finding", () => { + const findings: Telemetry.Finding[] = [ + { category: "syntax_error" }, + ] + const result = Telemetry.aggregateFindings(findings) + expect(result).toEqual({ syntax_error: 1 }) + }) + + test("handles all same category", () => { + const findings: Telemetry.Finding[] = [ + { category: "lint" }, + { category: "lint" }, + { category: "lint" }, + ] + const result = Telemetry.aggregateFindings(findings) + expect(result).toEqual({ lint: 3 }) + }) +}) + +// --------------------------------------------------------------------------- +// 2. sql_quality event shape validation +// --------------------------------------------------------------------------- +describe("sql_quality event shape", () => { + test("by_category serializes to valid JSON string", () => { + const findings: Telemetry.Finding[] = [ + { category: "lint" }, + { category: "lint" }, + { category: "safety" }, + ] + const by_category = Telemetry.aggregateFindings(findings) + const json = JSON.stringify(by_category) + + // Should round-trip through JSON + expect(JSON.parse(json)).toEqual({ lint: 2, safety: 1 }) + }) + + test("aggregated counts match finding_count", () => { + const findings: Telemetry.Finding[] = [ + { category: "a" }, + { category: "b" }, + { category: "c" }, + { category: "a" }, + ] + const by_category = Telemetry.aggregateFindings(findings) + const total = Object.values(by_category).reduce((a, b) => a + b, 0) + expect(total).toBe(findings.length) + }) +}) + +// --------------------------------------------------------------------------- +// 3. Finding extraction patterns (validates what tools produce) +// --------------------------------------------------------------------------- +describe("tool finding extraction patterns", () => { + test("sql_analyze issues use rule for lint, fall back to type otherwise", () => { + // Lint issues have rule (e.g. "select_star"), semantic/safety don't + const issues = [ + { type: "lint", rule: "select_star", severity: "warning", message: "...", recommendation: "...", confidence: "high" }, + { type: "lint", rule: "filter_has_func", severity: "warning", message: "...", recommendation: "...", confidence: "high" }, + { type: "semantic", severity: "warning", message: "...", recommendation: "...", confidence: "medium" }, + { type: "safety", severity: "high", message: "...", recommendation: "...", confidence: "high" }, + ] + const findings: Telemetry.Finding[] = issues.map((i: any) => ({ + category: i.rule ?? i.type, + })) + expect(findings).toEqual([ + { category: "select_star" }, + { category: "filter_has_func" }, + { category: "semantic" }, + { category: "safety" }, + ]) + }) + + test("validate errors map to findings with classification", () => { + const errors = [ + { message: "Table 'users' not found in schema" }, + { message: "Column 'email' not found in table 'orders'" }, + { message: "Syntax error near 'SELCT'" }, + ] + // Simulates classifyValidationError logic (column check before table check) + function classify(msg: string): string { + const lower = msg.toLowerCase() + if (lower.includes("column") && lower.includes("not found")) return "missing_column" + if (lower.includes("table") && lower.includes("not found")) return "missing_table" + if (lower.includes("syntax")) return "syntax_error" + return "validation_error" + } + const findings: Telemetry.Finding[] = errors.map((e) => ({ + category: classify(e.message), + })) + const by_category = Telemetry.aggregateFindings(findings) + expect(by_category).toEqual({ + missing_table: 1, + missing_column: 1, + syntax_error: 1, + }) + }) + + test("semantics issues all map to semantic_issue category", () => { + // Semantic findings don't have rule/type — always "semantic_issue" + const issues = [ + { severity: "error", message: "..." }, + { severity: "warning", message: "..." }, + { severity: "warning", message: "..." }, + ] + const findings: Telemetry.Finding[] = issues.map(() => ({ + category: "semantic_issue", + })) + expect(findings).toEqual([ + { category: "semantic_issue" }, + { category: "semantic_issue" }, + { category: "semantic_issue" }, + ]) + const by_category = Telemetry.aggregateFindings(findings) + expect(by_category).toEqual({ semantic_issue: 3 }) + }) + + test("fix tool produces fix_applied and unfixable_error categories", () => { + const data = { + fixes_applied: [{ description: "Fixed typo" }, { description: "Fixed reference" }], + unfixable_errors: [{ error: { message: "Cannot resolve" } }], + } + const findings: Telemetry.Finding[] = [] + for (const _ of data.fixes_applied) { + findings.push({ category: "fix_applied" }) + } + for (const _ of data.unfixable_errors) { + findings.push({ category: "unfixable_error" }) + } + const by_category = Telemetry.aggregateFindings(findings) + expect(by_category).toEqual({ fix_applied: 2, unfixable_error: 1 }) + }) + + test("equivalence differences produce findings only when not equivalent", () => { + // Equivalent — no findings + const equivData = { equivalent: true, differences: [] } + const equivFindings: Telemetry.Finding[] = [] + if (!equivData.equivalent && equivData.differences?.length) { + for (const _ of equivData.differences) { + equivFindings.push({ category: "equivalence_difference" }) + } + } + expect(equivFindings).toEqual([]) + + // Different — findings + const diffData = { equivalent: false, differences: [{ description: "..." }, { description: "..." }] } + const diffFindings: Telemetry.Finding[] = [] + if (!diffData.equivalent && diffData.differences?.length) { + for (const _ of diffData.differences) { + diffFindings.push({ category: "equivalence_difference" }) + } + } + expect(diffFindings.length).toBe(2) + const by_category = Telemetry.aggregateFindings(diffFindings) + expect(by_category).toEqual({ equivalence_difference: 2 }) + }) + + test("correct tool changes produce findings", () => { + const data = { changes: [{ description: "a" }, { description: "b" }] } + const findings: Telemetry.Finding[] = data.changes.map(() => ({ + category: "correction_applied", + })) + expect(findings.length).toBe(2) + const by_category = Telemetry.aggregateFindings(findings) + expect(by_category).toEqual({ correction_applied: 2 }) + }) + + test("check tool aggregates validation, lint, safety, and pii findings", () => { + const data = { + validation: { valid: false, errors: [{ message: "syntax error" }] }, + lint: { clean: false, findings: [{ rule: "select_star", severity: "warning", message: "..." }, { rule: "filter_has_func", severity: "warning", message: "..." }] }, + safety: { safe: false, threats: [{ type: "sql_injection", severity: "high", description: "..." }] }, + pii: { findings: [{ column: "email", category: "email", confidence: "high" }] }, + } + const findings: Telemetry.Finding[] = [] + for (const _ of data.validation.errors) findings.push({ category: "validation_error" }) + for (const f of data.lint.findings) findings.push({ category: f.rule ?? "lint" }) + for (const t of data.safety.threats) findings.push({ category: (t as any).type ?? "safety_threat" }) + for (const _ of data.pii.findings) findings.push({ category: "pii_detected" }) + const by_category = Telemetry.aggregateFindings(findings) + expect(by_category).toEqual({ + validation_error: 1, + select_star: 1, + filter_has_func: 1, + sql_injection: 1, + pii_detected: 1, + }) + }) + + test("policy violations use rule as category", () => { + const data = { + pass: false, + violations: [ + { rule: "no_select_star", severity: "error", message: "..." }, + { rule: "require_where", severity: "error", message: "..." }, + { severity: "warning", message: "..." }, // no rule + ], + } + const findings: Telemetry.Finding[] = data.violations.map((v: any) => ({ + category: v.rule ?? "policy_violation", + })) + const by_category = Telemetry.aggregateFindings(findings) + expect(by_category).toEqual({ + no_select_star: 1, + require_where: 1, + policy_violation: 1, + }) + }) + + test("schema diff uses change_type as category", () => { + const changes = [ + { severity: "breaking", change_type: "column_dropped", message: "..." }, + { severity: "warning", change_type: "type_changed", message: "..." }, + { severity: "info", change_type: "column_added", message: "..." }, + { severity: "breaking", change_type: "column_dropped", message: "..." }, + ] + const findings: Telemetry.Finding[] = changes.map((c) => ({ + category: c.change_type ?? (c.severity === "breaking" ? "breaking_change" : "schema_change"), + })) + const by_category = Telemetry.aggregateFindings(findings) + expect(by_category).toEqual({ + column_dropped: 2, + type_changed: 1, + column_added: 1, + }) + }) + + test("optimize tool combines anti-patterns and suggestions", () => { + const result = { + anti_patterns: [ + { type: "cartesian_product", severity: "error", message: "..." }, + { type: "select_star", severity: "warning", message: "..." }, + ], + suggestions: [ + { type: "cte_elimination", impact: "high", description: "..." }, + ], + } + const findings: Telemetry.Finding[] = [ + ...result.anti_patterns.map((ap) => ({ category: ap.type ?? "anti_pattern" })), + ...result.suggestions.map((s) => ({ category: s.type ?? "optimization_suggestion" })), + ] + const by_category = Telemetry.aggregateFindings(findings) + expect(by_category).toEqual({ + cartesian_product: 1, + select_star: 1, + cte_elimination: 1, + }) + }) + + test("impact analysis produces findings only when downstream affected", () => { + // No impact — no findings + const safeFindings: Telemetry.Finding[] = [] + expect(safeFindings).toEqual([]) + + // High impact — findings per dependent + const findings: Telemetry.Finding[] = [] + const direct = [{ name: "model_a" }, { name: "model_b" }] + const transitive = [{ name: "model_c" }] + const totalAffected = direct.length + transitive.length + if (totalAffected > 0) { + findings.push({ category: "impact_medium" }) + for (const _ of direct) findings.push({ category: "impact_direct_dependent" }) + for (const _ of transitive) findings.push({ category: "impact_transitive_dependent" }) + } + const by_category = Telemetry.aggregateFindings(findings) + expect(by_category).toEqual({ + impact_medium: 1, + impact_direct_dependent: 2, + impact_transitive_dependent: 1, + }) + }) +}) + +// --------------------------------------------------------------------------- +// 4. No findings = no event +// --------------------------------------------------------------------------- +describe("no findings = no sql_quality event", () => { + test("empty issues array produces empty findings", () => { + const issues: any[] = [] + const findings: Telemetry.Finding[] = issues.map((i: any) => ({ + category: i.type, + })) + expect(findings.length).toBe(0) + // tool.ts guards: !isSoftFailure && Array.isArray(findings) && findings.length > 0 + // So no event would be emitted + }) + + test("valid SQL with no errors produces no findings", () => { + const data = { valid: true, errors: [] } + const findings: Telemetry.Finding[] = (data.errors ?? []).map(() => ({ + category: "validation_error", + })) + expect(findings.length).toBe(0) + }) +})