Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
14 changes: 7 additions & 7 deletions bun.lock

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

2 changes: 1 addition & 1 deletion packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"@ai-sdk/togetherai": "2.0.41",
"@ai-sdk/vercel": "2.0.39",
"@ai-sdk/xai": "3.0.82",
"@altimateai/altimate-core": "0.5.1",
"@altimateai/altimate-core": "0.7.0",
"@altimateai/drivers": "workspace:*",
"@aws-sdk/credential-providers": "3.1057.0",
"@clack/prompts": "1.0.0-alpha.1",
Expand Down
54 changes: 42 additions & 12 deletions packages/opencode/src/altimate/native/altimate-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

import * as core from "@altimateai/altimate-core"
import { EngineCoerce } from "./engine-coerce"
import { register } from "./dispatcher"
import { schemaOrEmpty, resolveSchema } from "./schema-resolver"
import type { AltimateCoreResult } from "./types"
Expand Down Expand Up @@ -180,11 +181,39 @@ export function registerAll(): void {
params.schema_context ? JSON.stringify(params.schema_context) : undefined,
)
: core.lint(params.sql, schema)
const safety = core.scanSql(params.sql)
// Diff-scope safety like lint: threats present in the base SQL are
// pre-existing, not introduced by this change — subtract them by
// (rule, matched_pattern) identity when a base is supplied.
let safety = core.scanSql(params.sql)
if (params.base_sql) {
try {
const baseKeys = new Set(
core.scanSql(params.base_sql).threats.map((t: any) => `${t.rule}|${t.matched_pattern}`),
)
safety = {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
...safety,
threats: safety.threats.filter((t: any) => !baseKeys.has(`${t.rule}|${t.matched_pattern}`)),
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Comment thread
anandgupta42 marked this conversation as resolved.
Outdated
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
}
Comment thread
cursor[bot] marked this conversation as resolved.
} catch {
// Unscannable base — keep the full head scan (fail open to MORE findings).
}
}
// PII exposure for the composite check — the tool has always rendered a
// PII section; previously nothing populated it. Additive: a PII failure
// must not fail the whole composite.
let pii: Record<string, unknown>
try {
pii = toData(core.checkQueryPii(params.sql, schema))
Comment thread
anandgupta42 marked this conversation as resolved.
} catch (e) {
// Mark as an abstention — an empty object would render "No PII
// detected", a false-clean verdict.
pii = { parse_error: String(e) }
}
const data: Record<string, unknown> = {
validation: toData(validation),
lint: toData(lintResult),
safety: toData(safety),
pii,
Comment thread
anandgupta42 marked this conversation as resolved.
}
return ok(true, data)
} catch (e) {
Expand Down Expand Up @@ -323,10 +352,10 @@ export function registerAll(): void {
// Pass the optional dialect hint so dialect-specific compiled warehouse SQL
// (e.g. Snowflake semi-structured `col:field`) parses and the pair is
// decidable instead of abstaining on a syntax error. Supported since
// altimate-core@0.5.1. Use `|| undefined` (not `??`) so the default empty
// string from ReviewConfig.dialect coerces to "no hint": the engine throws
// on an unknown dialect "", and "" must mean auto-detect, not a real dialect.
const raw = await core.checkEquivalence(params.sql1, params.sql2, schema, params.dialect || undefined)
// altimate-core@0.5.1. dialectHint coerces "" (the ReviewConfig default)
// to undefined: the engine throws on an unknown dialect "", and "" must
// mean auto-detect, not a real dialect.
const raw = await core.checkEquivalence(params.sql1, params.sql2, schema, EngineCoerce.dialectHint(params.dialect))
const data = toData(raw)
return ok(true, data)
} catch (e) {
Expand All @@ -337,8 +366,9 @@ export function registerAll(): void {
// 12. altimate_core.migration
register("altimate_core.migration", async (params) => {
try {
// Build schema from old_ddl, analyze new_ddl against it
const schema = core.Schema.fromDdl(params.old_ddl, params.dialect ?? undefined)
// Build schema from old_ddl, analyze new_ddl against it. dialectHint
// coerces "" to auto-detect (the engine throws on an unknown dialect "").
const schema = core.Schema.fromDdl(params.old_ddl, EngineCoerce.dialectHint(params.dialect))
const raw = core.analyzeMigration(params.new_ddl, schema)
const data = toData(raw)
return ok(true, data)
Expand Down Expand Up @@ -432,7 +462,7 @@ export function registerAll(): void {
register("altimate_core.column_lineage", async (params) => {
try {
const schema = resolveSchema(params.schema_path, params.schema_context)
const raw = core.columnLineage(params.sql, params.dialect ?? undefined, schema ?? undefined)
const raw = core.columnLineage(params.sql, EngineCoerce.dialectHint(params.dialect), schema ?? undefined)
return ok(true, toData(raw))
} catch (e) {
return fail(e)
Expand All @@ -453,7 +483,7 @@ export function registerAll(): void {
// 22. altimate_core.format
register("altimate_core.format", async (params) => {
try {
const raw = core.formatSql(params.sql, params.dialect ?? undefined)
const raw = core.formatSql(params.sql, EngineCoerce.dialectHint(params.dialect))
const data = toData(raw)
return ok(true, data)
} catch (e) {
Expand All @@ -464,7 +494,7 @@ export function registerAll(): void {
// 23. altimate_core.metadata
register("altimate_core.metadata", async (params) => {
try {
const raw = core.extractMetadata(params.sql, params.dialect ?? undefined)
const raw = core.extractMetadata(params.sql, EngineCoerce.dialectHint(params.dialect))
return ok(true, toData(raw))
} catch (e) {
return fail(e)
Expand All @@ -474,7 +504,7 @@ export function registerAll(): void {
// 24. altimate_core.compare
register("altimate_core.compare", async (params) => {
try {
const raw = core.compareQueries(params.left_sql, params.right_sql, params.dialect ?? undefined)
const raw = core.compareQueries(params.left_sql, params.right_sql, EngineCoerce.dialectHint(params.dialect))
return ok(true, toData(raw))
} catch (e) {
return fail(e)
Expand Down Expand Up @@ -528,7 +558,7 @@ export function registerAll(): void {
// 29. altimate_core.import_ddl — returns Schema, must serialize
register("altimate_core.import_ddl", async (params) => {
try {
const schema = core.importDdl(params.ddl, params.dialect ?? undefined)
const schema = core.importDdl(params.ddl, EngineCoerce.dialectHint(params.dialect))
const jsonObj = schema.toJson()
return ok(true, { success: true, schema: toData(jsonObj) })
} catch (e) {
Expand Down
62 changes: 62 additions & 0 deletions packages/opencode/src/altimate/native/engine-coerce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Shared coercions for altimate-core engine values.
*
* The engine's napi surface has recurring foot-guns for consumers:
* - `PiiClassification` is a string OR `{ Custom: string }` — naive string
* interpolation renders `[object Object]`.
* - dialect parameters throw on the empty string (`unknown dialect ''`);
* `""` must mean "auto-detect" and be passed as undefined.
* - confidence is numeric (0..1) while several consumers declare string bands.
*
* This module must stay free of `@altimateai/altimate-core` imports so tool
* modules can use it without eagerly loading the native NAPI binding.
*/

/** PiiClassification is 'Email' | … | { Custom: string } | 'None'. */
export function classificationToString(c: unknown, fallback = "UNKNOWN"): string {
if (typeof c === "string") return c
if (c && typeof c === "object" && typeof (c as { Custom?: unknown }).Custom === "string") {
return (c as { Custom: string }).Custom
}
return fallback
}

/** Coerce an optional dialect to an engine-safe hint: "" and null mean auto-detect. */
export function dialectHint(dialect: string | undefined | null): string | undefined {
return dialect || undefined
}

/**
* Map the engine's numeric confidence (0..1) to a string band.
* Missing or non-numeric confidence is unknown, not low — band it "medium".
*/
export function bandConfidence(c: unknown): "high" | "medium" | "low" {
Comment thread
anandgupta42 marked this conversation as resolved.
Comment thread
anandgupta42 marked this conversation as resolved.
if (typeof c === "string") {
const s = c.toLowerCase()
if (s === "high" || s === "medium" || s === "low") return s
}
const n = typeof c === "number" && Number.isFinite(c) ? c : 0.5
if (n >= 0.8) return "high"
if (n >= 0.5) return "medium"
return "low"
}

/**
* Extract the real PII rows from an engine PiiReport.
*
* The engine returns `{ columns, pii_count, risk_level, total_columns }` with
* a row for EVERY column — classification "None" means not PII.
*
* Fails closed: a report without an array `columns` field is malformed (the
* engine always emits one) and throws instead of silently yielding zero
* findings — silent-empty output is exactly the bug class this fixes.
*/
export function piiColumnsFromReport(piiData: unknown): Array<Record<string, any>> {
const columns = (piiData as Record<string, any> | null | undefined)?.columns
if (!Array.isArray(columns)) {
throw new TypeError("malformed PiiReport: missing columns array")
}
return columns.filter((c) => c && c.classification !== "None")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export * as EngineCoerce from "./engine-coerce"
71 changes: 41 additions & 30 deletions packages/opencode/src/altimate/native/schema/pii-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import * as core from "@altimateai/altimate-core"
import { EngineCoerce } from "../engine-coerce"
import { getCache } from "./cache"
import * as Registry from "../connections/registry"
import type {
Expand All @@ -12,6 +13,9 @@ import type {
PiiFinding,
} from "../types"

/** Re-exported for tests and legacy importers; lives in engine-coerce. */
export const piiColumnsFromReport = EngineCoerce.piiColumnsFromReport
Comment thread
anandgupta42 marked this conversation as resolved.

/**
* Detect PII in cached schema metadata by running altimate-core's
* classifyPii() on column names and types.
Expand Down Expand Up @@ -43,6 +47,7 @@ export async function detectPii(params: PiiDetectParams): Promise<PiiDetectResul

const findings: PiiFinding[] = []
let columnsScanned = 0
let scanErrors = 0
const tablesWithPii = new Set<string>()

for (const wh of targetWarehouses) {
Expand Down Expand Up @@ -72,22 +77,25 @@ export async function detectPii(params: PiiDetectParams): Promise<PiiDetectResul
const result = core.classifyPii(schema)
const piiData = JSON.parse(JSON.stringify(result))

if (piiData && piiData.findings && piiData.findings.length > 0) {
for (const finding of piiData.findings) {
findings.push({
warehouse: col.warehouse,
schema: col.schema_name,
table: col.table,
column: col.name,
data_type: col.data_type,
pii_category: finding.category || finding.pii_type || "UNKNOWN",
confidence: finding.confidence || "medium",
})
tablesWithPii.add(`${col.warehouse}.${col.schema_name}.${col.table}`)
}
// Engine PiiReport: { columns, pii_count, risk_level, total_columns };
// every column gets a row — classification "None" means not PII.
const piiColumns = EngineCoerce.piiColumnsFromReport(piiData)
for (const piiCol of piiColumns) {
findings.push({
warehouse: col.warehouse,
schema: col.schema_name,
table: col.table,
column: col.name,
data_type: col.data_type,
pii_category: EngineCoerce.classificationToString(piiCol.classification, "UNKNOWN"),
confidence: EngineCoerce.bandConfidence(piiCol.confidence),
})
tablesWithPii.add(`${col.warehouse}.${col.schema_name}.${col.table}`)
}
} catch {
// classifyPii may not find PII — that is expected
// classifyPii threw or returned a malformed report — record it so the
// scan fails closed instead of silently reporting fewer findings.
scanErrors++
}
}
}
Expand All @@ -99,7 +107,7 @@ export async function detectPii(params: PiiDetectParams): Promise<PiiDetectResul
}

return {
success: true,
success: scanErrors === 0,
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
anandgupta42 marked this conversation as resolved.
Comment thread
anandgupta42 marked this conversation as resolved.
findings,
finding_count: findings.length,
columns_scanned: columnsScanned,
Expand Down Expand Up @@ -134,6 +142,7 @@ async function detectPiiLive(params: PiiDetectParams): Promise<PiiDetectResult>

const findings: PiiFinding[] = []
let columnsScanned = 0
let scanErrors = 0
const tablesWithPii = new Set<string>()

for (const schemaName of schemas) {
Expand Down Expand Up @@ -165,22 +174,24 @@ async function detectPiiLive(params: PiiDetectParams): Promise<PiiDetectResult>
const result = core.classifyPii(schema)
const piiData = JSON.parse(JSON.stringify(result))

if (piiData?.findings) {
for (const finding of piiData.findings) {
findings.push({
warehouse: params.warehouse!,
schema: schemaName,
table: tableInfo.name,
column: finding.column || "",
data_type: finding.data_type,
pii_category: finding.category || finding.pii_type || "UNKNOWN",
confidence: finding.confidence || "medium",
})
tablesWithPii.add(`${params.warehouse}.${schemaName}.${tableInfo.name}`)
}
// Engine PiiReport: { columns, … } with a row per column; "None" = not PII.
const piiColumns = EngineCoerce.piiColumnsFromReport(piiData)
for (const piiCol of piiColumns) {
findings.push({
warehouse: params.warehouse!,
schema: schemaName,
table: tableInfo.name,
column: piiCol.column || "",
data_type: columns.find((c) => c.name === piiCol.column)?.data_type,
pii_category: EngineCoerce.classificationToString(piiCol.classification, "UNKNOWN"),
confidence: EngineCoerce.bandConfidence(piiCol.confidence),
})
tablesWithPii.add(`${params.warehouse}.${schemaName}.${tableInfo.name}`)
}
} catch {
// ignore
// classifyPii threw or returned a malformed report — record it so
// the scan fails closed instead of silently reporting fewer findings.
scanErrors++
}
}
}
Expand All @@ -191,7 +202,7 @@ async function detectPiiLive(params: PiiDetectParams): Promise<PiiDetectResult>
}

return {
success: true,
success: scanErrors === 0,
findings,
finding_count: findings.length,
columns_scanned: columnsScanned,
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/altimate/native/sql/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as core from "@altimateai/altimate-core"
import { register } from "../dispatcher"
import { schemaOrEmpty, resolveSchema } from "../schema-resolver"
import { preprocessIff, postprocessQualify } from "../altimate-core"
import { EngineCoerce } from "../engine-coerce"
import type {
SqlAnalyzeResult,
SqlAnalyzeIssue,
Expand Down Expand Up @@ -183,7 +184,7 @@ export function registerAllSql(): void {
// ---------------------------------------------------------------------------
register("sql.format", async (params) => {
try {
const raw = core.formatSql(params.sql, params.dialect)
const raw = core.formatSql(params.sql, EngineCoerce.dialectHint(params.dialect))
const result = JSON.parse(JSON.stringify(raw))
return {
success: result.success ?? true,
Expand Down Expand Up @@ -462,7 +463,7 @@ export function registerAllSql(): void {
register("lineage.check", async (params) => {
try {
const schema = params.schema_context ? (resolveSchema(undefined, params.schema_context) ?? undefined) : undefined
const raw = core.columnLineage(params.sql, params.dialect ?? undefined, schema ?? undefined)
const raw = core.columnLineage(params.sql, EngineCoerce.dialectHint(params.dialect), schema ?? undefined)
const result = JSON.parse(JSON.stringify(raw))
return {
success: true,
Expand Down
Loading
Loading