Skip to content

Commit a706430

Browse files
anandgupta42claude
andauthored
feat: show validation value even on clean PRs (#6)
Replace the old "Check | Result | Details" table with a new "What We Checked | How | Result" validation table that communicates what was validated and the technology used (DataFusion, AST analysis, pattern scan, column classification). Key changes: - New `ValidationSummary` and `QueryProfile` types in `types.ts` - `extractValidationSummary()` in `cli-check.ts` with `CATEGORY_META` mapping for all 7 check categories - New `query-profile.ts` module for regex-based SQL structure extraction (complexity, JOINs, CTEs, subqueries, window functions, aggregation) - Rewritten executive line: "validated" instead of "modified", "N downstream safe" instead of "all checks passed", "N findings" instead of separate critical/warning counts - Collapsible Query Profile section with per-file metadata table - Footer now includes "Validated without hitting your warehouse" - Always post comment when `filesAnalyzed > 0` (previously skipped clean PRs, which showed zero value) - `runCheckCommand` now returns `CheckCommandResult` with both issues and validation summary - Version bumped to 0.4.0 - Updated all unit and e2e tests for new format Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6c1970b commit a706430

File tree

12 files changed

+1576
-244
lines changed

12 files changed

+1576
-244
lines changed

dist/index.js

Lines changed: 302 additions & 47 deletions
Large diffs are not rendered by default.

dist/index.js.map

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/analysis/cli-check.ts

Lines changed: 137 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { runCLI } from "../util/cli.js";
2-
import { Severity, type SQLIssue, type ChangedFile } from "./types.js";
2+
import {
3+
Severity,
4+
type SQLIssue,
5+
type ChangedFile,
6+
type ValidationSummary,
7+
type CategorySummary,
8+
} from "./types.js";
39
import * as core from "@actions/core";
410

511
/** Structured output from `altimate-code check --format json`. */
@@ -46,6 +52,61 @@ export interface CheckCommandOptions {
4652
severity?: string;
4753
}
4854

55+
/** Result from `runCheckCommand` including issues and validation metadata. */
56+
export interface CheckCommandResult {
57+
issues: SQLIssue[];
58+
validationSummary: ValidationSummary;
59+
}
60+
61+
/** Static metadata about each check category for display in PR comments. */
62+
export const CATEGORY_META: Record<
63+
string,
64+
{ label: string; method: string; ruleCount: number; examples: string[] }
65+
> = {
66+
lint: {
67+
label: "Anti-Patterns",
68+
ruleCount: 26,
69+
method: "AST analysis",
70+
examples: ["SELECT *", "cartesian joins", "missing GROUP BY", "non-deterministic functions"],
71+
},
72+
safety: {
73+
label: "Injection Safety",
74+
ruleCount: 10,
75+
method: "Pattern scan",
76+
examples: ["SQL injection", "stacked queries", "tautology", "UNION-based"],
77+
},
78+
validate: {
79+
label: "SQL Syntax",
80+
ruleCount: 0,
81+
method: "DataFusion",
82+
examples: [],
83+
},
84+
pii: {
85+
label: "PII Exposure",
86+
ruleCount: 9,
87+
method: "Column classification",
88+
examples: ["email", "SSN", "phone", "credit card", "IP address"],
89+
},
90+
policy: {
91+
label: "Policy Guardrails",
92+
ruleCount: 0,
93+
method: "YAML policy rules",
94+
examples: [],
95+
},
96+
semantic: {
97+
label: "Semantic Checks",
98+
ruleCount: 10,
99+
method: "Plan analysis",
100+
examples: ["cartesian products", "wrong JOINs", "NULL misuse"],
101+
},
102+
grade: {
103+
label: "Quality Grade",
104+
ruleCount: 0,
105+
method: "Composite scoring",
106+
examples: [],
107+
},
108+
};
109+
49110
/**
50111
* Detect whether the `altimate-code` CLI is available and supports the
51112
* `check` subcommand. Returns true if the CLI responds to `check --help`.
@@ -60,15 +121,16 @@ export async function isCheckCommandAvailable(): Promise<boolean> {
60121
}
61122

62123
/**
63-
* Run `altimate-code check` on the given files and return structured issues.
124+
* Run `altimate-code check` on the given files and return structured issues
125+
* along with a validation summary that describes what was checked and how.
64126
*
65127
* Invokes the CLI once with all files and all requested checks, parses the
66128
* JSON output, and maps findings to the common `SQLIssue[]` format.
67129
*/
68130
export async function runCheckCommand(
69131
files: ChangedFile[],
70132
options: CheckCommandOptions = {},
71-
): Promise<SQLIssue[]> {
133+
): Promise<CheckCommandResult> {
72134
const filePaths = files.map((f) => f.filename);
73135
const checksArg = (options.checks ?? ["lint", "safety"]).join(",");
74136

@@ -91,15 +153,84 @@ export async function runCheckCommand(
91153

92154
if (result.exitCode !== 0 && !result.json) {
93155
core.warning(`altimate-code check failed (exit ${result.exitCode}): ${result.stderr}`);
94-
return [];
156+
return { issues: [], validationSummary: buildEmptyValidationSummary(checksArg.split(",")) };
95157
}
96158

97159
if (!result.json) {
98160
core.warning("altimate-code check produced no JSON output");
99-
return [];
161+
return { issues: [], validationSummary: buildEmptyValidationSummary(checksArg.split(",")) };
162+
}
163+
164+
const output = result.json as CheckOutput;
165+
return {
166+
issues: parseCheckOutput(output),
167+
validationSummary: extractValidationSummary(output),
168+
};
169+
}
170+
171+
/**
172+
* Extract a structured validation summary from CLI check output.
173+
* This captures what was checked, how, and whether each category passed.
174+
*/
175+
export function extractValidationSummary(output: CheckOutput): ValidationSummary {
176+
const categories: Record<string, CategorySummary> = {};
177+
178+
const checksRun = output.checks_run ?? [];
179+
const schemaResolved = output.schema_resolved ?? false;
180+
181+
for (const check of checksRun) {
182+
const meta = CATEGORY_META[check];
183+
const result = output.results?.[check];
184+
const findingsCount = result?.findings?.length ?? 0;
185+
186+
if (meta) {
187+
const methodWithContext =
188+
check === "validate" && schemaResolved && output.files_checked > 0
189+
? `${meta.method} against ${output.files_checked} table schemas`
190+
: meta.method;
191+
192+
categories[check] = {
193+
label: meta.ruleCount > 0 ? `${meta.label} (${meta.ruleCount} rules)` : meta.label,
194+
method:
195+
findingsCount === 0 && meta.examples.length > 0
196+
? `${methodWithContext}: ${meta.examples.join(", ")}, ...`
197+
: methodWithContext,
198+
rulesChecked: meta.ruleCount,
199+
findingsCount,
200+
passed: findingsCount === 0,
201+
};
202+
} else {
203+
categories[check] = {
204+
label: check.charAt(0).toUpperCase() + check.slice(1),
205+
method: "Static analysis",
206+
rulesChecked: 0,
207+
findingsCount,
208+
passed: findingsCount === 0,
209+
};
210+
}
100211
}
101212

102-
return parseCheckOutput(result.json as CheckOutput);
213+
return { checksRun, schemaResolved, categories };
214+
}
215+
216+
/** Build a minimal validation summary when CLI output is unavailable. */
217+
function buildEmptyValidationSummary(checks: string[]): ValidationSummary {
218+
const categories: Record<string, CategorySummary> = {};
219+
for (const check of checks) {
220+
const meta = CATEGORY_META[check];
221+
categories[check] = {
222+
label: meta
223+
? meta.ruleCount > 0
224+
? `${meta.label} (${meta.ruleCount} rules)`
225+
: meta.label
226+
: check,
227+
method: meta?.method ?? "Static analysis",
228+
rulesChecked: meta?.ruleCount ?? 0,
229+
findingsCount: 0,
230+
passed: true,
231+
};
232+
}
233+
return { checksRun: checks, schemaResolved: false, categories };
103234
}
104235

105236
/**

src/analysis/query-profile.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import type { QueryProfile } from "./types.js";
2+
3+
/**
4+
* Extract structural metadata from a SQL file using regex-based heuristics.
5+
* This gives users a "Query Profile" showing complexity, JOINs, CTEs, etc.
6+
*/
7+
export function extractQueryProfile(file: string, sql: string): QueryProfile {
8+
const joinTypes = extractJoinTypes(sql);
9+
10+
return {
11+
file,
12+
complexity: computeComplexity(sql),
13+
tablesReferenced: countTables(sql),
14+
joinCount: joinTypes.length,
15+
joinTypes: [...new Set(joinTypes)],
16+
hasAggregation: /\bGROUP\s+BY\b/i.test(sql) || hasAggregateFunctions(sql),
17+
hasSubquery: /\(\s*SELECT\b/i.test(sql),
18+
hasWindowFunction: /\bOVER\s*\(/i.test(sql),
19+
hasCTE: /\bWITH\s+\w+\s+AS\s*\(/i.test(sql),
20+
};
21+
}
22+
23+
/** Count the number of JOIN clauses by type. */
24+
function extractJoinTypes(sql: string): string[] {
25+
const types: string[] = [];
26+
const joinPattern =
27+
/\b(INNER|LEFT\s+OUTER|RIGHT\s+OUTER|FULL\s+OUTER|LEFT|RIGHT|FULL|CROSS)?\s*JOIN\b/gi;
28+
let match;
29+
while ((match = joinPattern.exec(sql)) !== null) {
30+
const prefix = (match[1] ?? "INNER").trim().toUpperCase();
31+
// Normalize multi-word types
32+
if (prefix.startsWith("LEFT")) types.push("LEFT");
33+
else if (prefix.startsWith("RIGHT")) types.push("RIGHT");
34+
else if (prefix.startsWith("FULL")) types.push("FULL");
35+
else if (prefix === "CROSS") types.push("CROSS");
36+
else types.push("INNER");
37+
}
38+
return types;
39+
}
40+
41+
/** Count tables referenced via FROM and JOIN clauses. */
42+
function countTables(sql: string): number {
43+
const tables = new Set<string>();
44+
45+
// FROM <table>
46+
const fromPattern = /\bFROM\s+([a-zA-Z_][\w.]*)/gi;
47+
let match;
48+
while ((match = fromPattern.exec(sql)) !== null) {
49+
tables.add(match[1].toLowerCase());
50+
}
51+
52+
// JOIN <table>
53+
const joinPattern = /\bJOIN\s+([a-zA-Z_][\w.]*)/gi;
54+
while ((match = joinPattern.exec(sql)) !== null) {
55+
tables.add(match[1].toLowerCase());
56+
}
57+
58+
return tables.size;
59+
}
60+
61+
/** Check for aggregate functions like COUNT, SUM, AVG, MIN, MAX. */
62+
function hasAggregateFunctions(sql: string): boolean {
63+
return /\b(COUNT|SUM|AVG|MIN|MAX)\s*\(/i.test(sql);
64+
}
65+
66+
/** Estimate query complexity based on structural features. */
67+
function computeComplexity(sql: string): "Low" | "Medium" | "High" {
68+
let score = 0;
69+
70+
const joinCount = (sql.match(/\bJOIN\b/gi) ?? []).length;
71+
score += joinCount;
72+
73+
if (/\bGROUP\s+BY\b/i.test(sql)) score += 1;
74+
if (/\bHAVING\b/i.test(sql)) score += 1;
75+
if (/\bOVER\s*\(/i.test(sql)) score += 2;
76+
if (/\(\s*SELECT\b/i.test(sql)) score += 2;
77+
if (/\bWITH\s+\w+\s+AS\s*\(/i.test(sql)) score += 1;
78+
if (/\bUNION\b/i.test(sql)) score += 2;
79+
if (/\bCASE\b/i.test(sql)) score += 1;
80+
81+
// Count number of CTEs
82+
const cteCount = (sql.match(/\bAS\s*\(\s*SELECT\b/gi) ?? []).length;
83+
if (cteCount > 2) score += 1;
84+
85+
if (score <= 2) return "Low";
86+
if (score <= 5) return "Medium";
87+
return "High";
88+
}

src/analysis/sql-review.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ async function analyzeWithRuleEngine(
7474
severity: altimateConfig.sql_review.severity_threshold,
7575
dialect: altimateConfig.dialect !== "auto" ? altimateConfig.dialect : undefined,
7676
};
77-
const issues = await runCheckCommand(files, options);
78-
core.info(`CLI check found ${issues.length} issue(s) total`);
79-
return issues;
77+
const result = await runCheckCommand(files, options);
78+
core.info(`CLI check found ${result.issues.length} issue(s) total`);
79+
return result.issues;
8080
}
8181

8282
// Fallback: built-in regex rules

src/analysis/types.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,52 @@ export interface CostEstimate {
6666
explanation?: string;
6767
}
6868

69+
/** Summary of what was validated across all check categories. */
70+
export interface ValidationSummary {
71+
/** List of check categories that were executed. */
72+
checksRun: string[];
73+
/** Whether table schemas were resolved for SQL validation. */
74+
schemaResolved: boolean;
75+
/** Per-category validation metadata. */
76+
categories: Record<string, CategorySummary>;
77+
}
78+
79+
/** Metadata for a single check category (e.g. lint, safety, pii). */
80+
export interface CategorySummary {
81+
/** Display label, e.g. "Anti-Patterns (26 rules)". */
82+
label: string;
83+
/** Technology/method used, e.g. "AST analysis: SELECT *, cartesian joins, ...". */
84+
method: string;
85+
/** Number of rules checked in this category. */
86+
rulesChecked: number;
87+
/** Number of findings produced. */
88+
findingsCount: number;
89+
/** Whether this category passed (zero findings). */
90+
passed: boolean;
91+
}
92+
93+
/** Structural metadata extracted from a SQL file. */
94+
export interface QueryProfile {
95+
/** Relative file path. */
96+
file: string;
97+
/** Estimated complexity bucket. */
98+
complexity: "Low" | "Medium" | "High";
99+
/** Number of tables/sources referenced. */
100+
tablesReferenced: number;
101+
/** Number of JOIN clauses. */
102+
joinCount: number;
103+
/** Types of JOINs used (e.g. ["INNER", "LEFT"]). */
104+
joinTypes: string[];
105+
/** Whether the query uses GROUP BY / aggregate functions. */
106+
hasAggregation: boolean;
107+
/** Whether the query contains a subquery. */
108+
hasSubquery: boolean;
109+
/** Whether the query uses window functions (OVER). */
110+
hasWindowFunction: boolean;
111+
/** Whether the query uses CTEs (WITH ... AS). */
112+
hasCTE: boolean;
113+
}
114+
69115
/** Aggregated review report for the entire PR. */
70116
export interface ReviewReport {
71117
/** All SQL issues found across files. */
@@ -88,6 +134,10 @@ export interface ReviewReport {
88134
mode: ReviewMode;
89135
/** Timestamp of the analysis. */
90136
timestamp: string;
137+
/** Structured validation summary from CLI check output. */
138+
validationSummary?: ValidationSummary;
139+
/** Query structure profiles extracted from SQL content. */
140+
queryProfiles?: QueryProfile[];
91141
}
92142

93143
/** Parsed input configuration for the action. */

0 commit comments

Comments
 (0)