Skip to content

Commit 8fd0c7b

Browse files
anandgupta42claude
andauthored
fix: dbt tool regression — schema format mismatch, silent failures, wrong results (#263)
* fix: dbt tool regression — schema format mismatch, silent failures, wrong results (#261) Root cause: `schema-resolver.ts` passed flat schema_context (`{"table": {"col": "TYPE"}}`) directly to `Schema.fromJson()`, which expects the Rust `SchemaDefinition` format (`{"tables": {"table": {"columns": [{"name": "col", "type": "TYPE"}]}}}`). The parse silently failed, falling back to an empty schema, causing all schema-dependent tools to produce empty/wrong results. Fixes: - `schema-resolver.ts`: Auto-detect and normalize 3 schema_context variants (flat map, array-of-columns, SchemaDefinition) before calling `Schema.fromJson()` - `sql-analyze.ts`: Check `result.error` instead of `!result.success` to distinguish "issues found" from "analysis failed" - `altimate-core-grade.ts`: Map Rust field names (`overall_grade`, `scores.overall`) instead of `grade`/`score` - `altimate-core-complete.ts`: Use `data.items` (Rust field) with fallback to `data.suggestions` - `altimate-core-schema-diff.ts`: Use `has_breaking_changes` (Rust) and format tagged enum changes correctly - `altimate-core-rewrite.ts`: Use `data.suggestions` (Rust) with fallback to `data.rewrites` - `altimate-core-fix.ts`: Map `fixes_applied`/`unfixable_errors` (Rust fields) instead of `changes`/`errors` - `altimate-core-column-lineage.ts`: Show `column_dict` and use `lens_type` (Rust) for transform info - `sql/register.ts`: Populate `can_auto_apply`, `original_fragment`, `rewritten_fragment` in `sql.rewrite` handler Closes #261 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: additional tool field name mismatches found via full simulation Full simulation of 77 tests across all altimate-core tools revealed 6 more field name mismatches between Rust napi output and TS formatters: - `altimate_core.transpile`: QUALIFY post-processing checked `data.sql` but Rust returns `transpiled_sql` (array) - `altimate_core.resolve_term`: Rust returns array, `toData()` converts to `{'0': ...}` object — now wraps as `{ matches: [...] }` - `altimate_core_classify_pii`: checked `data.findings` but Rust returns `columns` with `classification`/`suggested_masking` fields - `altimate_core_query_pii`: checked `data.exposures` but Rust returns `pii_columns` with `risk_level`/`suggested_alternatives` - `altimate_core_testgen`: test cases have `inputs`/`expected` fields, not `sql` — formatter now shows input values and expectations - `altimate_core_prune_schema`: checked `data.pruned` but Rust returns `pruned_schema_yaml`/`relevant_tables`/`tables_pruned` Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add 168 simulation tests for altimate-core tool coverage Two new test files exercising all 36 altimate-core napi functions through the dispatcher layer: - `issue-261-e2e.test.ts`: 14 tests reproducing all 11 broken tools from #261 plus schema format variant tests - `edge-cases-simulation.test.ts`: 76 tests covering empty inputs, complex SQL patterns (nested subqueries, CTEs, window functions, correlated subqueries), column lineage through transformations, schema diff with large schemas, PII detection, multi-dialect transpilation, DDL roundtrip, equivalence checks, policy enforcement, and migration safety analysis Total: 192 tests, 404 assertions, 0 failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add 96-test stress simulation + fix empty table schema crash Stress simulation covering: SQL pattern variants (recursive CTEs, LATERAL joins, GROUPING SETS), lint anti-pattern detection, column lineage through 10 transformation types, schema diff mutation matrix (100-column tables, special chars), 10-dialect transpile matrix, grading consistency, fuzzy fix matching, testgen feature coverage, cursor-position completions, PII detection, and term resolution. Also fixes schema-resolver to skip tables with empty column lists (Rust engine requires >= 1 column per table). Total simulation suite: 288 tests, 867 assertions, 0 failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: consolidate simulations into e2e tests with `-e2e.test.ts` naming Moved all simulation tests into proper e2e test files following the project convention (`*-e2e.test.ts` in `test/altimate/`): - `altimate-core-e2e.test.ts` — 100 tests: all dispatcher methods, schema format variants, error recovery, complex SQL patterns - `altimate-core-stress-e2e.test.ts` — 80 tests: dialect matrix, fuzzy fix matching, grading consistency, testgen coverage, cursor completions, PII detection, lineage tracing, schema diffs - `issue-261-e2e.test.ts` — 14 tests: regression tests for #261 Removed standalone simulation files (full-simulation, edge-cases, stress-simulation) — content consolidated into the e2e files above. Total e2e suite: 180 tests, 539 assertions, 0 failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review comments — schema detection false positive and PII field fallbacks 1. `isSchemaDefinitionFormat`: now validates that values under `tables` contain a `columns` array, preventing false positives when a flat schema has a table literally named "tables" 2. `formatQueryPii` / `formatClassifyPii`: added fallbacks for missing `table`/`column` properties to prevent "undefined.column_name" output 3. Added test for the "tables" false-positive scenario Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Sentry comment + fix CI test isolation failures 1. `formatFix`: changed condition from `data.fixed && data.fixed_sql` to `data.fixed_sql && data.fixed !== false` so a valid `fixed_sql` is not silently discarded when `fixed` field is absent 2. `sql/register.ts`: wrapped all 10 composite SQL handlers in exportable `registerAllSql()` function (mirrors `registerAll()` pattern from `altimate-core.ts`) 3. All 3 e2e test files now call both `core.registerAll()` and `sql.registerAllSql()` in `beforeAll` to survive `Dispatcher.reset()` from other test files — fixes CI "No native handler" failures 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 ad52d87 commit 8fd0c7b

21 files changed

Lines changed: 2404 additions & 55 deletions

packages/opencode/src/altimate/native/altimate-core.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,15 @@ register("altimate_core.transpile", async (params) => {
131131
// Post-process QUALIFY for targets that lack native support
132132
const targetLower = params.to_dialect.toLowerCase()
133133
if (QUALIFY_TARGETS.has(targetLower)) {
134-
const translated =
135-
(data.sql as string) || (data.translated_sql as string) || ""
136-
if (translated && translated.toUpperCase().includes("QUALIFY")) {
137-
const fixed = postprocessQualify(translated)
138-
if ("sql" in data) {
134+
// Rust returns transpiled_sql as string[] — use first element
135+
const transpiled = Array.isArray(data.transpiled_sql)
136+
? (data.transpiled_sql as string[])[0]
137+
: (data.transpiled_sql as string) || (data.sql as string) || (data.translated_sql as string) || ""
138+
if (transpiled && transpiled.toUpperCase().includes("QUALIFY")) {
139+
const fixed = postprocessQualify(transpiled)
140+
if (Array.isArray(data.transpiled_sql)) {
141+
;(data.transpiled_sql as string[])[0] = fixed
142+
} else if ("sql" in data) {
139143
data.sql = fixed
140144
} else {
141145
data.translated_sql = fixed
@@ -326,12 +330,14 @@ register("altimate_core.query_pii", async (params) => {
326330
}
327331
})
328332

329-
// 19. altimate_core.resolve_term
333+
// 19. altimate_core.resolve_term — returns array, must wrap
330334
register("altimate_core.resolve_term", async (params) => {
331335
try {
332336
const schema = schemaOrEmpty(params.schema_path, params.schema_context)
333337
const raw = core.resolveTerm(params.term, schema)
334-
return ok(true, toData(raw))
338+
// Rust returns an array of matches — wrap for consistent object shape
339+
const matches = Array.isArray(raw) ? JSON.parse(JSON.stringify(raw)) : []
340+
return ok(matches.length > 0, { matches })
335341
} catch (e) {
336342
return fail(e)
337343
}

packages/opencode/src/altimate/native/schema-resolver.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,94 @@
33
*
44
* Translates the bridge protocol's `schema_path` / `schema_context` parameters
55
* into altimate-core `Schema` objects.
6+
*
7+
* Tools pass `schema_context` in two possible formats:
8+
*
9+
* 1. **Flat format** (used by most tools):
10+
* `{ "table_name": { "col_name": "TYPE", ... } }`
11+
*
12+
* 2. **SchemaDefinition format** (matches Rust struct):
13+
* `{ "tables": { "table_name": { "columns": [{ "name": "col", "type": "TYPE" }] } } }`
14+
*
15+
* This module normalizes both formats into the SchemaDefinition format
16+
* expected by `Schema.fromJson()`.
617
*/
718

819
import { Schema } from "@altimateai/altimate-core"
920

21+
/**
22+
* Detect whether a schema_context object is in flat format or SchemaDefinition format.
23+
*
24+
* Flat format: `{ "table_name": { "col_name": "TYPE" } }`
25+
* SchemaDefinition format: has a `tables` key with nested structure.
26+
*/
27+
function isSchemaDefinitionFormat(ctx: Record<string, any>): boolean {
28+
if (!("tables" in ctx) || typeof ctx.tables !== "object" || ctx.tables === null) {
29+
return false
30+
}
31+
// Verify at least one value under `tables` looks like a table definition
32+
// (has a `columns` array), not a flat column map like { "col": "TYPE" }.
33+
// This prevents false positives when a flat schema has a table named "tables".
34+
const values = Object.values(ctx.tables)
35+
if (values.length === 0) return true // empty tables is valid SchemaDefinition
36+
return values.some((v: any) => Array.isArray(v?.columns))
37+
}
38+
39+
/**
40+
* Convert flat schema format to SchemaDefinition format.
41+
*
42+
* Handles three input variants:
43+
*
44+
* 1. Flat map: `{ "customers": { "id": "INTEGER", "name": "VARCHAR" } }`
45+
* 2. Array form: `{ "customers": [{ "name": "id", "data_type": "INTEGER" }] }`
46+
* 3. Partial SD: `{ "customers": { "columns": [{ "name": "id", "type": "INTEGER" }] } }`
47+
*
48+
* Output: `{ "tables": { "customers": { "columns": [{ "name": "id", "type": "INTEGER" }, ...] } } }`
49+
*/
50+
function flatToSchemaDefinition(flat: Record<string, any>): Record<string, any> {
51+
const tables: Record<string, any> = {}
52+
for (const [tableName, colsOrDef] of Object.entries(flat)) {
53+
if (colsOrDef === null || colsOrDef === undefined) continue
54+
55+
// Variant 2: array of column definitions
56+
if (Array.isArray(colsOrDef)) {
57+
if (colsOrDef.length === 0) continue // skip empty tables
58+
const columns = colsOrDef.map((c: any) => ({
59+
name: c.name,
60+
type: c.type ?? c.data_type ?? "VARCHAR",
61+
}))
62+
tables[tableName] = { columns }
63+
} else if (typeof colsOrDef === "object") {
64+
// Variant 3: already has a `columns` array
65+
if (Array.isArray(colsOrDef.columns)) {
66+
if (colsOrDef.columns.length === 0) continue // skip empty tables
67+
tables[tableName] = colsOrDef
68+
} else {
69+
// Variant 1: flat map { "col_name": "TYPE", ... }
70+
const entries = Object.entries(colsOrDef)
71+
if (entries.length === 0) continue // skip empty tables
72+
const columns = entries.map(([colName, colType]) => ({
73+
name: colName,
74+
type: String(colType),
75+
}))
76+
tables[tableName] = { columns }
77+
}
78+
}
79+
}
80+
return { tables }
81+
}
82+
83+
/**
84+
* Normalize a schema_context into SchemaDefinition JSON format.
85+
* Accepts both flat and SchemaDefinition formats.
86+
*/
87+
function normalizeSchemaContext(ctx: Record<string, any>): string {
88+
if (isSchemaDefinitionFormat(ctx)) {
89+
return JSON.stringify(ctx)
90+
}
91+
return JSON.stringify(flatToSchemaDefinition(ctx))
92+
}
93+
1094
/**
1195
* Resolve a Schema from a file path or inline JSON context.
1296
* Returns null when neither source is provided.
@@ -19,7 +103,7 @@ export function resolveSchema(
19103
return Schema.fromFile(schemaPath)
20104
}
21105
if (schemaContext && Object.keys(schemaContext).length > 0) {
22-
return Schema.fromJson(JSON.stringify(schemaContext))
106+
return Schema.fromJson(normalizeSchemaContext(schemaContext))
23107
}
24108
return null
25109
}

packages/opencode/src/altimate/native/sql/register.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ import type {
2020
SchemaDiffResult,
2121
} from "../types"
2222

23+
/** Register all composite SQL handlers with the Dispatcher.
24+
* Exported so tests can re-register after Dispatcher.reset(). */
25+
export function registerAllSql(): void {
26+
2327
// ---------------------------------------------------------------------------
2428
// sql.analyze — lint + semantics + safety
2529
// ---------------------------------------------------------------------------
@@ -362,8 +366,10 @@ register("sql.rewrite", async (params) => {
362366
rewritten_sql: result.suggestions?.[0]?.rewritten_sql ?? null,
363367
rewrites_applied: result.suggestions?.map((s: any) => ({
364368
rule: s.rule,
365-
description: s.explanation,
366-
rewritten_sql: s.rewritten_sql,
369+
original_fragment: params.sql,
370+
rewritten_fragment: s.rewritten_sql ?? params.sql,
371+
explanation: s.explanation ?? s.improvement ?? "",
372+
can_auto_apply: (s.confidence ?? 0) >= 0.7,
367373
})) ?? [],
368374
}
369375
} catch (e) {
@@ -430,3 +436,8 @@ register("lineage.check", async (params) => {
430436
} satisfies LineageCheckResult
431437
}
432438
})
439+
440+
} // end registerAllSql
441+
442+
// Auto-register on module load
443+
registerAllSql()

packages/opencode/src/altimate/tools/altimate-core-classify-pii.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export const AltimateCoreClassifyPiiTool = Tool.define("altimate_core_classify_p
1616
schema_context: args.schema_context,
1717
})
1818
const data = result.data as Record<string, any>
19-
const findingCount = data.findings?.length ?? 0
19+
const piiColumns = data.columns ?? data.findings ?? []
20+
const findingCount = piiColumns.length
2021
return {
2122
title: `PII Classification: ${findingCount} finding(s)`,
2223
metadata: { success: result.success, finding_count: findingCount },
@@ -31,10 +32,20 @@ export const AltimateCoreClassifyPiiTool = Tool.define("altimate_core_classify_p
3132

3233
function formatClassifyPii(data: Record<string, any>): string {
3334
if (data.error) return `Error: ${data.error}`
34-
if (!data.findings?.length) return "No PII columns detected."
35-
const lines = ["PII columns found:\n"]
36-
for (const f of data.findings) {
37-
lines.push(` ${f.table}.${f.column}: ${f.category} (${f.confidence} confidence)`)
35+
const piiColumns = data.columns ?? data.findings ?? []
36+
if (!piiColumns.length) return "No PII columns detected."
37+
const lines: string[] = []
38+
if (data.risk_level) lines.push(`Risk level: ${data.risk_level}`)
39+
if (data.pii_count != null) lines.push(`PII columns: ${data.pii_count} of ${data.total_columns}`)
40+
lines.push("")
41+
lines.push("PII columns found:")
42+
for (const f of piiColumns) {
43+
const classification = f.classification ?? f.category ?? "PII"
44+
const confidence = f.confidence ?? "high"
45+
const table = f.table ?? "unknown"
46+
const column = f.column ?? "unknown"
47+
lines.push(` ${table}.${column}: ${classification} (${confidence} confidence)`)
48+
if (f.suggested_masking) lines.push(` Masking: ${f.suggested_masking}`)
3849
}
3950
return lines.join("\n")
4051
}

packages/opencode/src/altimate/tools/altimate-core-column-lineage.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,26 @@ export const AltimateCoreColumnLineageTool = Tool.define("altimate_core_column_l
3535

3636
function formatColumnLineage(data: Record<string, any>): string {
3737
if (data.error) return `Error: ${data.error}`
38-
if (!data.column_lineage?.length) return "No column lineage edges found."
39-
const lines = ["Column lineage:\n"]
40-
for (const edge of data.column_lineage) {
41-
lines.push(` ${edge.source} -> ${edge.target}${edge.transform ? ` (${edge.transform})` : ""}`)
38+
if (!data.column_lineage?.length && !data.column_dict) return "No column lineage edges found."
39+
const lines: string[] = []
40+
41+
// column_dict: output columns -> source columns mapping
42+
if (data.column_dict && Object.keys(data.column_dict).length > 0) {
43+
lines.push("Column Mappings:")
44+
for (const [target, sources] of Object.entries(data.column_dict)) {
45+
const srcList = Array.isArray(sources) ? (sources as string[]).join(", ") : JSON.stringify(sources)
46+
lines.push(` ${target}${srcList}`)
47+
}
48+
lines.push("")
49+
}
50+
51+
if (data.column_lineage?.length) {
52+
lines.push("Lineage Edges:")
53+
for (const edge of data.column_lineage) {
54+
const transform = edge.lens_type ?? edge.transform_type ?? edge.transform ?? ""
55+
lines.push(` ${edge.source}${edge.target}${transform ? ` (${transform})` : ""}`)
56+
}
4257
}
43-
return lines.join("\n")
58+
59+
return lines.length ? lines.join("\n") : "No column lineage edges found."
4460
}

packages/opencode/src/altimate/tools/altimate-core-complete.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const AltimateCoreCompleteTool = Tool.define("altimate_core_complete", {
2020
schema_context: args.schema_context,
2121
})
2222
const data = result.data as Record<string, any>
23-
const count = data.suggestions?.length ?? 0
23+
const count = data.items?.length ?? data.suggestions?.length ?? 0
2424
return {
2525
title: `Complete: ${count} suggestion(s)`,
2626
metadata: { success: result.success, suggestion_count: count },
@@ -35,9 +35,10 @@ export const AltimateCoreCompleteTool = Tool.define("altimate_core_complete", {
3535

3636
function formatComplete(data: Record<string, any>): string {
3737
if (data.error) return `Error: ${data.error}`
38-
if (!data.suggestions?.length) return "No completions available."
38+
const items = data.items ?? data.suggestions ?? []
39+
if (!items.length) return "No completions available."
3940
const lines = ["Suggestions:\n"]
40-
for (const s of data.suggestions) {
41+
for (const s of items) {
4142
lines.push(` ${s.label ?? s.text} (${s.kind ?? s.type ?? "unknown"})`)
4243
}
4344
return lines.join("\n")

packages/opencode/src/altimate/tools/altimate-core-fix.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,23 @@ export const AltimateCoreFixTool = Tool.define("altimate_core_fix", {
3535
function formatFix(data: Record<string, any>): string {
3636
if (data.error) return `Error: ${data.error}`
3737
const lines: string[] = []
38-
if (data.fixed_sql) {
38+
if (data.fixed_sql && data.fixed !== false) {
3939
lines.push("Fixed SQL:")
4040
lines.push(data.fixed_sql)
41-
if (data.changes?.length) {
41+
const fixes = data.fixes_applied ?? data.changes ?? []
42+
if (fixes.length) {
4243
lines.push("\nChanges applied:")
43-
for (const c of data.changes) {
44-
lines.push(` - ${c.description ?? c}`)
44+
for (const c of fixes) {
45+
lines.push(` - ${c.description ?? c.message ?? c}`)
4546
}
4647
}
4748
} else {
4849
lines.push("Could not auto-fix the SQL.")
49-
if (data.errors?.length) {
50+
const unfixable = data.unfixable_errors ?? data.errors ?? []
51+
if (unfixable.length) {
5052
lines.push("\nErrors found:")
51-
for (const e of data.errors) {
52-
lines.push(` - ${e.message ?? e}`)
53+
for (const e of unfixable) {
54+
lines.push(` - ${e.message ?? e.reason ?? e}`)
5355
}
5456
}
5557
}

packages/opencode/src/altimate/tools/altimate-core-grade.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ export const AltimateCoreGradeTool = Tool.define("altimate_core_grade", {
1818
schema_context: args.schema_context,
1919
})
2020
const data = result.data as Record<string, any>
21+
const grade = data.overall_grade ?? data.grade
22+
const score = data.scores?.overall != null ? Math.round(data.scores.overall * 100) : data.score
2123
return {
22-
title: `Grade: ${data.grade ?? "?"}`,
23-
metadata: { success: result.success, grade: data.grade, score: data.score },
24+
title: `Grade: ${grade ?? "?"}`,
25+
metadata: { success: result.success, grade, score },
2426
output: formatGrade(data),
2527
}
2628
} catch (e) {
@@ -33,8 +35,20 @@ export const AltimateCoreGradeTool = Tool.define("altimate_core_grade", {
3335
function formatGrade(data: Record<string, any>): string {
3436
if (data.error) return `Error: ${data.error}`
3537
const lines: string[] = []
36-
lines.push(`Grade: ${data.grade}`)
37-
if (data.score != null) lines.push(`Score: ${data.score}/100`)
38+
const grade = data.overall_grade ?? data.grade
39+
lines.push(`Grade: ${grade}`)
40+
const scores = data.scores
41+
if (scores) {
42+
const overall = scores.overall != null ? Math.round(scores.overall * 100) : null
43+
if (overall != null) lines.push(`Score: ${overall}/100`)
44+
lines.push("\nCategory scores:")
45+
if (scores.syntax != null) lines.push(` syntax: ${Math.round(scores.syntax * 100)}/100`)
46+
if (scores.style != null) lines.push(` style: ${Math.round(scores.style * 100)}/100`)
47+
if (scores.safety != null) lines.push(` safety: ${Math.round(scores.safety * 100)}/100`)
48+
if (scores.complexity != null) lines.push(` complexity: ${Math.round(scores.complexity * 100)}/100`)
49+
} else if (data.score != null) {
50+
lines.push(`Score: ${data.score}/100`)
51+
}
3852
if (data.categories) {
3953
lines.push("\nCategory scores:")
4054
for (const [cat, score] of Object.entries(data.categories)) {

packages/opencode/src/altimate/tools/altimate-core-prune-schema.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ export const AltimateCorePruneSchemaTool = Tool.define("altimate_core_prune_sche
3232

3333
function formatPruneSchema(data: Record<string, any>): string {
3434
if (data.error) return `Error: ${data.error}`
35-
if (data.pruned) return JSON.stringify(data.pruned, null, 2)
36-
return JSON.stringify(data, null, 2)
35+
const lines: string[] = []
36+
if (data.tables_pruned != null) {
37+
lines.push(`Pruned ${data.tables_pruned} of ${data.total_tables} tables to ${data.relevant_tables?.length ?? "?"} relevant.`)
38+
}
39+
if (data.relevant_tables?.length) {
40+
lines.push(`Relevant tables: ${data.relevant_tables.join(", ")}`)
41+
}
42+
if (data.pruned_schema_yaml) {
43+
lines.push("")
44+
lines.push(data.pruned_schema_yaml)
45+
} else if (data.pruned) {
46+
lines.push("")
47+
lines.push(JSON.stringify(data.pruned, null, 2))
48+
}
49+
return lines.length > 0 ? lines.join("\n") : JSON.stringify(data, null, 2)
3750
}

packages/opencode/src/altimate/tools/altimate-core-query-pii.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export const AltimateCoreQueryPiiTool = Tool.define("altimate_core_query_pii", {
1818
schema_context: args.schema_context,
1919
})
2020
const data = result.data as Record<string, any>
21-
const exposureCount = data.exposures?.length ?? 0
21+
const piiCols = data.pii_columns ?? data.exposures ?? []
22+
const exposureCount = piiCols.length
2223
return {
2324
title: `Query PII: ${exposureCount === 0 ? "CLEAN" : `${exposureCount} exposure(s)`}`,
2425
metadata: { success: result.success, exposure_count: exposureCount },
@@ -33,10 +34,23 @@ export const AltimateCoreQueryPiiTool = Tool.define("altimate_core_query_pii", {
3334

3435
function formatQueryPii(data: Record<string, any>): string {
3536
if (data.error) return `Error: ${data.error}`
36-
if (!data.exposures?.length) return "Query does not access PII columns."
37-
const lines = ["PII exposure detected:\n"]
38-
for (const e of data.exposures) {
39-
lines.push(` ${e.column}: ${e.category} (${e.risk ?? "medium"} risk)`)
37+
const piiCols = data.pii_columns ?? data.exposures ?? []
38+
if (!piiCols.length) return "Query does not access PII columns."
39+
const lines: string[] = []
40+
if (data.risk_level) lines.push(`Risk level: ${data.risk_level}`)
41+
lines.push("PII exposure detected:\n")
42+
for (const e of piiCols) {
43+
const classification = e.classification ?? e.category ?? "PII"
44+
const table = e.table ?? "unknown"
45+
const column = e.column ?? "unknown"
46+
lines.push(` ${table}.${column}: ${classification}`)
47+
if (e.suggested_masking) lines.push(` Masking: ${e.suggested_masking}`)
48+
}
49+
if (data.suggested_alternatives?.length) {
50+
lines.push("\nSuggested alternatives:")
51+
for (const alt of data.suggested_alternatives) {
52+
lines.push(` - ${alt}`)
53+
}
4054
}
4155
return lines.join("\n")
4256
}

0 commit comments

Comments
 (0)