Skip to content

Commit 79d7c56

Browse files
anandgupta42claude
andauthored
feat: add altimate-code check CLI command for deterministic SQL checks (#453)
* feat: add `altimate-code check` CLI command for deterministic SQL checks Add a new `check` subcommand that runs deterministic SQL analysis (lint, validate, safety, policy, PII, semantic, grade) without requiring an LLM. The command: - Accepts SQL files as positional args or globs (defaults to `**/*.sql`) - Calls `Dispatcher.call()` for each check category (no direct napi imports) - Processes files in batches of 10 for concurrency - Supports `--format json|text`, `--severity`, `--fail-on` flags - Outputs diagnostics to stderr, structured JSON to stdout - Does NOT call `bootstrap()` — no session/db/server needed - Registered in `index.ts` with proper `altimate_change` markers * docs: add comprehensive documentation and tests for `altimate-code check` command - Add `docs/docs/usage/check.md` with full documentation covering all 7 check types, CLI options, JSON output schema, policy file format, schema file format, severity levels, CI/CD integration (GitHub Actions, pre-commit, GitLab CI), and real-world usage examples - Update `docs/docs/usage/cli.md` to reference the `check` command in the subcommands table - Add `check` page to `docs/mkdocs.yml` navigation under Interfaces - Extract helper functions (`normalizeSeverity`, `filterBySeverity`, `toCategoryResult`, `formatText`, `buildCheckOutput`) into `check-helpers.ts` for testability - Refactor `check.ts` to import from `check-helpers.ts` (no behavior change) - Add 53 unit tests covering: severity normalization, severity filtering, category result building, text formatting, output assembly, constants validation, and edge cases Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add comprehensive E2E + adversarial tests for `check` command - 58 new tests covering full handler flow with mocked `Dispatcher.call()` - E2E: all 7 check types, `--fail-on` exit codes, `--severity` filtering, `--policy` validation, unknown check names, schema resolution, batching, text/JSON output, mixed success/failure across checks - Adversarial: null bytes, shell metacharacters, 100K-char lines, Unicode/emoji, CRLF, spaces in filenames, deeply nested paths, malformed policy JSON, 1MB policy files, undefined/null finding fields, 5000 findings, XSS-like content, non-string severity, `__proto__` pollution, binary `.sql` content, symlinks, directory with `.sql` extension, duplicate file args, empty checks string - Fix: `normalizeSeverity()` now handles non-string inputs without crashing (previously threw on numeric/boolean/object severity from Dispatcher) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address 4 major review findings in `check` command 1. `--fail-on` now evaluates UNFILTERED findings before `--severity` filtering. Previously, `--severity error --fail-on warning` would false-pass because warnings were filtered out before exit logic. 2. Removed unused CLI options (`--dialect`, `--dbt-project`, `--manifest`) that were documented but had no effect — misleading for CI users. 3. `runPii` now checks `result.success` before processing data, consistent with all other check runners. 4. Dispatcher failures now emit error-severity findings instead of silently returning `[]`. A broken native bridge no longer reports PASS with zero findings. Also: - Use `buildCheckOutput()` helper instead of duplicated inline logic - Remove dead `|| ("error" as const)` fallbacks after `normalizeSeverity` - Add 4 new tests: severity/fail-on interaction, runPii failure, Dispatcher failure exit code Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address CodeRabbit review comments 1. Replace `process.exit()` with `process.exitCode` + `return` so the outer `finally` block in `index.ts` can run `Telemetry.shutdown()`. 2. Preserve A-F grade value from `altimate_core.grade` response in `CheckCategoryResult` metadata (`grade`, `score` fields). 3. Skip DB migration for `check` command — it only needs `Dispatcher` and has zero database dependencies. Prevents startup failure on read-only CI environments and removes multi-minute overhead. 4. Remove unused `--dialect`, `--dbt-project`, `--manifest` from docs options table (code already removed in prior commit). 5. Add `text` language specifier to fenced code block in `check.md`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove unnecessary `undefined as any` casts for optional `schema_context` `schema_context` is already optional (`?`) in the Dispatcher types. The `undefined as any` casts were unnecessary type safety bypasses. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cfe1bad commit 79d7c56

File tree

8 files changed

+2790
-2
lines changed

8 files changed

+2790
-2
lines changed

docs/docs/usage/check.md

Lines changed: 448 additions & 0 deletions
Large diffs are not rendered by default.

docs/docs/usage/cli.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ altimate --agent analyst
2222
| Command | Description |
2323
| ----------- | ------------------------------ |
2424
| `run` | Run a prompt non-interactively |
25+
| `check` | Run deterministic SQL checks (no LLM required) -- see [SQL Check](check.md) |
2526
| `serve` | Start the HTTP API server |
2627
| `web` | Start the web UI |
2728
| `agent` | Agent management |

docs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ nav:
101101
- Interfaces:
102102
- TUI: usage/tui.md
103103
- CLI: usage/cli.md
104+
- SQL Check: usage/check.md
104105
- Web UI: usage/web.md
105106
- CI: usage/ci-headless.md
106107
- IDE: usage/ide.md
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// altimate_change start — check-helpers: extracted helpers for deterministic SQL check command
2+
// These are exported separately so they can be unit-tested without importing
3+
// the full CLI command (which has side-effects via yargs).
4+
5+
// ---------------------------------------------------------------------------
6+
// Types
7+
// ---------------------------------------------------------------------------
8+
9+
export interface Finding {
10+
file: string
11+
line?: number
12+
column?: number
13+
code?: string
14+
rule?: string
15+
severity: "error" | "warning" | "info"
16+
message: string
17+
suggestion?: string
18+
}
19+
20+
export interface CheckCategoryResult {
21+
findings: Finding[]
22+
error_count: number
23+
warning_count: number
24+
[key: string]: unknown
25+
}
26+
27+
export interface CheckOutput {
28+
version: 1
29+
files_checked: number
30+
checks_run: string[]
31+
schema_resolved: boolean
32+
results: Record<string, CheckCategoryResult>
33+
summary: {
34+
total_findings: number
35+
errors: number
36+
warnings: number
37+
info: number
38+
pass: boolean
39+
}
40+
}
41+
42+
export type Severity = "error" | "warning" | "info"
43+
44+
export const SEVERITY_RANK: Record<Severity, number> = { error: 2, warning: 1, info: 0 }
45+
46+
export const VALID_CHECKS = new Set(["lint", "validate", "safety", "policy", "pii", "semantic", "grade"])
47+
48+
// ---------------------------------------------------------------------------
49+
// Helpers
50+
// ---------------------------------------------------------------------------
51+
52+
export function normalizeSeverity(s?: string | unknown): Severity {
53+
if (!s || typeof s !== "string") return "warning"
54+
const lower = s.toLowerCase()
55+
if (lower === "error" || lower === "fatal" || lower === "critical") return "error"
56+
if (lower === "warning" || lower === "warn") return "warning"
57+
return "info"
58+
}
59+
60+
export function filterBySeverity(findings: Finding[], minSeverity: Severity): Finding[] {
61+
const minRank = SEVERITY_RANK[minSeverity]
62+
return findings.filter((f) => SEVERITY_RANK[f.severity] >= minRank)
63+
}
64+
65+
export function toCategoryResult(findings: Finding[]): CheckCategoryResult {
66+
return {
67+
findings,
68+
error_count: findings.filter((f) => f.severity === "error").length,
69+
warning_count: findings.filter((f) => f.severity === "warning").length,
70+
}
71+
}
72+
73+
// ---------------------------------------------------------------------------
74+
// Text formatter
75+
// ---------------------------------------------------------------------------
76+
77+
export function formatText(output: CheckOutput): string {
78+
const lines: string[] = []
79+
80+
lines.push(`Checked ${output.files_checked} file(s) with [${output.checks_run.join(", ")}]`)
81+
if (output.schema_resolved) {
82+
lines.push("Schema: resolved")
83+
}
84+
lines.push("")
85+
86+
for (const [category, catResult] of Object.entries(output.results)) {
87+
if (catResult.findings.length === 0) continue
88+
lines.push(`--- ${category.toUpperCase()} ---`)
89+
for (const f of catResult.findings) {
90+
const loc = f.line ? `:${f.line}${f.column ? `:${f.column}` : ""}` : ""
91+
const rule = f.rule ? ` [${f.rule}]` : ""
92+
lines.push(` ${f.severity.toUpperCase()} ${f.file}${loc}${rule}: ${f.message}`)
93+
if (f.suggestion) {
94+
lines.push(` suggestion: ${f.suggestion}`)
95+
}
96+
}
97+
lines.push("")
98+
}
99+
100+
const s = output.summary
101+
lines.push(`${s.total_findings} finding(s): ${s.errors} error(s), ${s.warnings} warning(s), ${s.info} info`)
102+
lines.push(s.pass ? "PASS" : "FAIL")
103+
104+
return lines.join("\n")
105+
}
106+
107+
// ---------------------------------------------------------------------------
108+
// Output builder
109+
// ---------------------------------------------------------------------------
110+
111+
export function buildCheckOutput(opts: {
112+
filesChecked: number
113+
checksRun: string[]
114+
schemaResolved: boolean
115+
results: Record<string, CheckCategoryResult>
116+
failOn: "none" | "warning" | "error"
117+
}): CheckOutput {
118+
const allFindings = Object.values(opts.results).flatMap((r) => r.findings)
119+
const errors = allFindings.filter((f) => f.severity === "error").length
120+
const warnings = allFindings.filter((f) => f.severity === "warning").length
121+
const info = allFindings.filter((f) => f.severity === "info").length
122+
123+
let pass = true
124+
if (opts.failOn === "error" && errors > 0) pass = false
125+
if (opts.failOn === "warning" && (errors > 0 || warnings > 0)) pass = false
126+
127+
return {
128+
version: 1,
129+
files_checked: opts.filesChecked,
130+
checks_run: opts.checksRun,
131+
schema_resolved: opts.schemaResolved,
132+
results: opts.results,
133+
summary: {
134+
total_findings: allFindings.length,
135+
errors,
136+
warnings,
137+
info,
138+
pass,
139+
},
140+
}
141+
}
142+
// altimate_change end

0 commit comments

Comments
 (0)