mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-25 12:01:03 +02:00
refactor: enforce ktx naming and AGENTS.md compliance sweep (#289)
Align the tree with AGENTS.md/CLAUDE.md conventions: - Rewrite user-facing strings, docs, and tests to lowercase `ktx` (no bare uppercase `KTX` tokens remain outside literal identifiers). - Drop the legacy `historicSql` migration path and its now-unused helpers, per the no-backward-compat rule. - Remove `as unknown as` / `any` casts: narrow `BaseTool` generics to `z.ZodObject`, add a typed `createLookerClient`, and delete the dead `getParametersSchema`/`toAnthropicFormat` pre-AI-SDK helpers. - Use `InvalidArgumentError` for Commander parse failures. - Finish the adapter→connector prose conversion in the `ktx.yaml` docs while keeping the literal `adapters` config key.
This commit is contained in:
parent
005c5fc860
commit
00cdf2de90
237 changed files with 844 additions and 974 deletions
|
|
@ -88,13 +88,18 @@ const defaultLogger: LookerClientLogger = {
|
|||
|
||||
class InlineLookerSettings extends NodeSettings {
|
||||
constructor(private readonly params: LookerConnectionParams) {
|
||||
super('', {
|
||||
// @looker/sdk-rtl boundary: NodeSettings consumes a string-valued config
|
||||
// section (read back via the readConfig override below), but its constructor
|
||||
// is typed to accept a fully-realized IApiSettings. The string record is the
|
||||
// shape the library actually reads, so narrow to IApiSection first.
|
||||
const settings: IApiSection = {
|
||||
base_url: normalizeBaseUrl(params.base_url),
|
||||
client_id: params.client_id,
|
||||
client_secret: params.client_secret, // pragma: allowlist secret
|
||||
verify_ssl: 'true',
|
||||
timeout: '120',
|
||||
} as unknown as IApiSettings);
|
||||
};
|
||||
super('', settings as IApiSection & IApiSettings);
|
||||
}
|
||||
|
||||
override readConfig(_section?: string): IApiSection {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,16 @@ export class DefaultLookerConnectionClientFactory implements LookerConnectionCli
|
|||
) {}
|
||||
|
||||
async createClient(lookerConnectionId: string): Promise<LookerRuntimeClient> {
|
||||
return this.createLookerClient(lookerConnectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link createClient} but preserves the concrete {@link LookerClient}
|
||||
* type, so callers that need methods outside the `LookerRuntimeClient`
|
||||
* contract (e.g. `listLookerConnections`, `testConnection`) keep them without
|
||||
* a cast.
|
||||
*/
|
||||
async createLookerClient(lookerConnectionId: string): Promise<LookerClient> {
|
||||
const credentials = await this.resolver.resolve(lookerConnectionId);
|
||||
return new LookerClient(credentials, this.deps);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ export function validateLookerMappings(args: {
|
|||
if (!args.knownKtxConnectionIds.has(mapping.ktxConnectionId)) {
|
||||
errors.push({
|
||||
key: mapping.lookerConnectionName,
|
||||
reason: `KTX connection ${mapping.ktxConnectionId} does not exist`,
|
||||
reason: `ktx connection ${mapping.ktxConnectionId} does not exist`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class MetabaseApiError extends Error {
|
|||
* Strip Metabase `[[ ... {{ var }} ... ]]` optional-clause blocks from native SQL.
|
||||
*
|
||||
* The bracketed blocks are emitted only when the embedded `{{ var }}` is supplied at
|
||||
* Metabase query time. For KTX semantic-layer ingest there's no such runtime
|
||||
* Metabase query time. For ktx semantic-layer ingest there's no such runtime
|
||||
* parameter — chat-time filters are composed by the SL query planner — so the optional
|
||||
* block must be removed before the SQL becomes a permanent SL source. Substituting a
|
||||
* dummy value (the alternative) bakes a placeholder filter into the source and silently
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export function metabaseRuntimeConfigFromLocalConnection(
|
|||
}
|
||||
if (hasNetworkProxy(connection)) {
|
||||
throw new Error(
|
||||
`Standalone KTX does not support proxy-bearing Metabase connections yet. Use hosted Metabase ingest for "${connectionId}" until the KTX Metabase proxy support spec lands.`,
|
||||
`Standalone ktx does not support proxy-bearing Metabase connections yet. Use hosted Metabase ingest for "${connectionId}" until the ktx Metabase proxy support spec lands.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ export function validateMetabaseMappings(args: {
|
|||
continue;
|
||||
}
|
||||
if (!args.knownKtxConnectionIds.has(connectionId)) {
|
||||
errors.push({ key, reason: `KTX connection ${connectionId} does not exist` });
|
||||
errors.push({ key, reason: `ktx connection ${connectionId} does not exist` });
|
||||
}
|
||||
}
|
||||
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
||||
|
|
@ -207,7 +207,7 @@ export function validateMappingPhysicalMatch(
|
|||
}
|
||||
|
||||
if (target.connection_type !== expectedType) {
|
||||
return `Metabase database engine '${engine}' does not match KTX connection type '${target.connection_type}'`;
|
||||
return `Metabase database engine '${engine}' does not match ktx connection type '${target.connection_type}'`;
|
||||
}
|
||||
|
||||
const metabaseDb = normalizeName(mapping.metabaseDbName);
|
||||
|
|
@ -215,7 +215,7 @@ export function validateMappingPhysicalMatch(
|
|||
|
||||
if (engine === 'snowflake' || engine === 'bigquery' || engine === 'bigquery-cloud-sdk') {
|
||||
if (metabaseDb && targetDb && metabaseDb !== targetDb) {
|
||||
return `Metabase database '${mapping.metabaseDbName}' does not match KTX connection database '${displayValue(
|
||||
return `Metabase database '${mapping.metabaseDbName}' does not match ktx connection database '${displayValue(
|
||||
getTargetDatabase(target),
|
||||
)}'`;
|
||||
}
|
||||
|
|
@ -227,12 +227,12 @@ export function validateMappingPhysicalMatch(
|
|||
const targetHost = normalizeHost(target.host);
|
||||
|
||||
if (metabaseHost && targetHost && metabaseHost !== targetHost) {
|
||||
return `Metabase host '${mapping.metabaseHost}' does not match KTX connection host '${displayValue(
|
||||
return `Metabase host '${mapping.metabaseHost}' does not match ktx connection host '${displayValue(
|
||||
target.host,
|
||||
)}'`;
|
||||
}
|
||||
if (metabaseDb && targetDb && metabaseDb !== targetDb) {
|
||||
return `Metabase database '${mapping.metabaseDbName}' does not match KTX connection database '${displayValue(
|
||||
return `Metabase database '${mapping.metabaseDbName}' does not match ktx connection database '${displayValue(
|
||||
getTargetDatabase(target),
|
||||
)}'`;
|
||||
}
|
||||
|
|
@ -274,7 +274,7 @@ export async function refreshMetabaseMapping(args: {
|
|||
if (!target) {
|
||||
physicalMismatches.push({
|
||||
mappingId: String(mapping.id),
|
||||
reason: `KTX connection ${mapping.ktxConnectionId} does not exist`,
|
||||
reason: `ktx connection ${mapping.ktxConnectionId} does not exist`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ async function readOptionalFile(path: string): Promise<{ exists: boolean; conten
|
|||
|
||||
function buildGateRepairSystemPrompt(): string {
|
||||
return `<role>
|
||||
You repair one KTX isolated-diff artifact gate failure inside the integration worktree.
|
||||
You repair one ktx isolated-diff artifact gate failure inside the integration worktree.
|
||||
</role>
|
||||
|
||||
<rules>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ async function readOptionalFile(path: string): Promise<{ exists: boolean; conten
|
|||
|
||||
function buildResolverSystemPrompt(): string {
|
||||
return `<role>
|
||||
You repair one failed KTX isolated-diff patch inside the integration worktree.
|
||||
You repair one failed ktx isolated-diff patch inside the integration worktree.
|
||||
</role>
|
||||
|
||||
<rules>
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ import type { SourceAdapter } from './types.js';
|
|||
|
||||
const promptsDir = fileURLToPath(new URL('../../prompts', import.meta.url));
|
||||
const skillsDir = fileURLToPath(new URL('../../skills', import.meta.url));
|
||||
const LOCAL_AUTHOR = { name: 'KTX Local', email: 'local@ktx.local' };
|
||||
const LOCAL_AUTHOR = { name: 'ktx Local', email: 'local@ktx.local' };
|
||||
const LOCAL_SHAPE_WARNING = 'Local ingest validates semantic-layer YAML shape only.';
|
||||
const INGEST_TRACE_LEVELS = new Set<IngestTraceLevel>(['error', 'info', 'debug', 'trace']);
|
||||
|
||||
|
|
@ -232,8 +232,9 @@ class LocalSlPythonPort implements SlPythonPort {
|
|||
}
|
||||
|
||||
class LocalShapeOnlySlValidator implements SlValidatorPort<SlValidationDeps> {
|
||||
private validateParsedSource(sourceName: string, parsed: Record<string, unknown>) {
|
||||
const isOverlay = parsed.table == null && parsed.sql == null;
|
||||
private validateParsedSource(sourceName: string, parsed: unknown) {
|
||||
const fields = (parsed ?? {}) as { table?: unknown; sql?: unknown };
|
||||
const isOverlay = fields.table == null && fields.sql == null;
|
||||
const result = (isOverlay ? sourceOverlaySchema : sourceDefinitionSchema).safeParse(parsed);
|
||||
return result.success
|
||||
? { errors: [], warnings: [LOCAL_SHAPE_WARNING] }
|
||||
|
|
@ -255,7 +256,7 @@ class LocalShapeOnlySlValidator implements SlValidatorPort<SlValidationDeps> {
|
|||
const { sources, loadErrors } = await deps.semanticLayerService.loadAllSources(connectionId);
|
||||
const source = sources.find((candidate) => candidate.name === sourceName);
|
||||
if (source) {
|
||||
return this.validateParsedSource(sourceName, source as unknown as Record<string, unknown>);
|
||||
return this.validateParsedSource(sourceName, source);
|
||||
}
|
||||
const detail =
|
||||
loadErrors.length > 0
|
||||
|
|
@ -279,7 +280,7 @@ class LocalShapeOnlySlValidator implements SlValidatorPort<SlValidationDeps> {
|
|||
}
|
||||
|
||||
try {
|
||||
const parsed = YAML.parse(file.content) as unknown as Record<string, unknown>;
|
||||
const parsed = YAML.parse(file.content) as Record<string, unknown>;
|
||||
return this.validateParsedSource(sourceName, parsed);
|
||||
} catch (error) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -513,7 +513,7 @@ export function buildMemoryFlowViewModel(input: MemoryFlowReplayInput): MemoryFl
|
|||
: `${input.connectionId}/${input.adapter}`;
|
||||
|
||||
return {
|
||||
title: `KTX memory flow ${titleSources} ${input.status}`,
|
||||
title: `ktx memory flow ${titleSources} ${input.status}`,
|
||||
subtitle: `Run ${input.runId} Sync ${input.syncId}`,
|
||||
status: input.status,
|
||||
activeLine: activeLine(input),
|
||||
|
|
|
|||
|
|
@ -70,10 +70,10 @@ export class DiscoverDataTool extends BaseTool<typeof discoverDataInputSchema> {
|
|||
}
|
||||
|
||||
if (input.sourceName) {
|
||||
const sl = await this.deps.slDiscoverTool.call(
|
||||
const sl = (await this.deps.slDiscoverTool.call(
|
||||
{ sourceName: input.sourceName, connectionId: input.connectionId },
|
||||
context,
|
||||
);
|
||||
)) as ToolOutput;
|
||||
return { markdown: sl.markdown, structured: { wiki: null, sl: sl.structured, raw: null } };
|
||||
}
|
||||
|
||||
|
|
@ -85,17 +85,17 @@ export class DiscoverDataTool extends BaseTool<typeof discoverDataInputSchema> {
|
|||
let raw: DiscoverDataStructured['raw'] = null;
|
||||
|
||||
if (query) {
|
||||
const wikiResult = await this.deps.wikiSearchTool.call({ query, limit }, context);
|
||||
const wikiResult = (await this.deps.wikiSearchTool.call({ query, limit }, context)) as ToolOutput;
|
||||
if (totalFound(wikiResult.structured) > 0) {
|
||||
parts.push('## Wiki Pages', '> use `wiki_read(blockKey)` for full content', wikiResult.markdown, '');
|
||||
wiki = wikiResult.structured;
|
||||
}
|
||||
}
|
||||
|
||||
const slResult = await this.deps.slDiscoverTool.call(
|
||||
const slResult = (await this.deps.slDiscoverTool.call(
|
||||
{ query: query || undefined, connectionId: input.connectionId },
|
||||
context,
|
||||
);
|
||||
)) as ToolOutput;
|
||||
if (totalSources(slResult.structured) > 0) {
|
||||
parts.push(
|
||||
'## Semantic Layer Sources',
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ export class AiSdkKtxLlmRuntime implements KtxLlmRuntimePort {
|
|||
const result = await this.generateTextWithRateLimitRetry(modelProviderName(model), input.abortSignal, () => generateText(request));
|
||||
input.onMetrics?.({ totalMs: Date.now() - startedAt, usage: toLlmTokenUsage(result.totalUsage ?? result.usage) });
|
||||
if (typeof result.text !== 'string') {
|
||||
throw new Error('KTX LLM text generation returned no text');
|
||||
throw new Error('ktx LLM text generation returned no text');
|
||||
}
|
||||
return result.text;
|
||||
}
|
||||
|
|
@ -271,12 +271,12 @@ export class AiSdkKtxLlmRuntime implements KtxLlmRuntimePort {
|
|||
}),
|
||||
}
|
||||
: {}),
|
||||
output: Output.object({ schema: input.schema as unknown as FlexibleSchema<TOutput> }),
|
||||
output: Output.object({ schema: input.schema as FlexibleSchema<TOutput> }),
|
||||
};
|
||||
const result = await this.generateTextWithRateLimitRetry(modelProviderName(model), input.abortSignal, () => generateText(request));
|
||||
input.onMetrics?.({ totalMs: Date.now() - startedAt, usage: toLlmTokenUsage(result.totalUsage ?? result.usage) });
|
||||
if (result.output == null) {
|
||||
throw new Error('KTX LLM object generation returned no output');
|
||||
throw new Error('ktx LLM object generation returned no output');
|
||||
}
|
||||
return result.output as TOutput;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ function isClaudeRateLimitResult(result: SDKResultMessage, rejectedSignal: RateL
|
|||
}
|
||||
|
||||
function claudeRateLimitSignal(message: SDKMessage): RateLimitSignal | null {
|
||||
const record = message as unknown as Record<string, unknown>;
|
||||
const record = message as Record<string, unknown>;
|
||||
if (record.type === 'rate_limit_event') {
|
||||
const info = record.rate_limit_info as Record<string, unknown> | undefined;
|
||||
if (!info) return null;
|
||||
|
|
@ -253,7 +253,7 @@ function baseOptions(input: {
|
|||
? { behavior: 'allow', toolUseID: options.toolUseID }
|
||||
: {
|
||||
behavior: 'deny',
|
||||
message: `KTX claude-code runtime only permits current KTX MCP tools; denied ${toolName}.`,
|
||||
message: `ktx claude-code runtime only permits current ktx MCP tools; denied ${toolName}.`,
|
||||
toolUseID: options.toolUseID,
|
||||
},
|
||||
permissionMode: 'dontAsk',
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ export function resolveLocalKtxEmbeddingConfig(
|
|||
batchSize: config.batchSize,
|
||||
};
|
||||
}
|
||||
throw new Error(`Unsupported KTX embedding backend: ${String((config as { backend?: string }).backend)}`);
|
||||
throw new Error(`Unsupported ktx embedding backend: ${String((config as { backend?: string }).backend)}`);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export function normalizeKtxRuntimeToolOutput(value: unknown): KtxRuntimeToolOut
|
|||
|
||||
function assertObjectSchema(name: string, schema: z.ZodType): asserts schema is z.ZodObject<z.ZodRawShape> {
|
||||
if (!(schema instanceof z.ZodObject)) {
|
||||
throw new Error(`KTX runtime tool "${name}" must use z.object input schema for claude-code`);
|
||||
throw new Error(`ktx runtime tool "${name}" must use z.object input schema for claude-code`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ export function createRuntimeToolDescriptorFromAiTool(name: string, aiSdkTool: T
|
|||
inputSchema: aiSdkTool.inputSchema as KtxRuntimeToolDescriptor['inputSchema'],
|
||||
execute: async (input) => {
|
||||
if (typeof aiSdkTool.execute !== 'function') {
|
||||
throw new Error(`KTX runtime tool "${name}" has no execute function`);
|
||||
throw new Error(`ktx runtime tool "${name}" has no execute function`);
|
||||
}
|
||||
return normalizeKtxRuntimeToolOutput(
|
||||
await aiSdkTool.execute(input as never, { toolCallId: `runtime-${name}` } as never),
|
||||
|
|
|
|||
|
|
@ -56,12 +56,12 @@ const toolAnnotations = {
|
|||
|
||||
const toolDescriptions = {
|
||||
connection_list:
|
||||
'List configured read-only data connections available to this KTX project. Use this before connection-scoped tools when the project may have multiple warehouses.',
|
||||
'List configured read-only data connections available to this ktx project. Use this before connection-scoped tools when the project may have multiple warehouses.',
|
||||
discover_data:
|
||||
'Search across KTX wiki pages, semantic-layer sources, measures, dimensions, raw tables, and columns. Example: discover_data({ query: "monthly orders by customer", connectionId: "warehouse", kinds: ["sl_source", "table"] }).',
|
||||
'Search across ktx wiki pages, semantic-layer sources, measures, dimensions, raw tables, and columns. Example: discover_data({ query: "monthly orders by customer", connectionId: "warehouse", kinds: ["sl_source", "table"] }).',
|
||||
wiki_search:
|
||||
'Search KTX wiki pages for reusable business context. Example: wiki_search({ query: "revenue recognition", limit: 5 }).',
|
||||
wiki_read: 'Read a KTX wiki page by key returned from wiki_search. Example: wiki_read({ key: "global/revenue" }).',
|
||||
'Search ktx wiki pages for reusable business context. Example: wiki_search({ query: "revenue recognition", limit: 5 }).',
|
||||
wiki_read: 'Read a ktx wiki page by key returned from wiki_search. Example: wiki_read({ key: "global/revenue" }).',
|
||||
entity_details:
|
||||
'Read table and column metadata from the latest live-database scan snapshot. Example: entity_details({ connectionId: "warehouse", entities: [{ table: { catalog: null, db: "public", name: "orders" }, columns: ["id"] }] }).',
|
||||
dictionary_search:
|
||||
|
|
@ -71,9 +71,9 @@ const toolDescriptions = {
|
|||
sl_query:
|
||||
'Execute a semantic-layer query and return headers, rows, and total row count, plus correctness notes (e.g. compile-only or fan-out) when relevant. The generated SQL and full query plan are omitted by default; request them with include: ["sql"] and/or include: ["plan"]. Example: sl_query({ connectionId: "warehouse", measures: ["orders.order_count"], dimensions: [{ field: "orders.created_at", granularity: "month" }], include: ["sql"] }).',
|
||||
sql_execution:
|
||||
'Execute one parser-validated read-only SQL query against a configured KTX connection. Example: sql_execution({ connectionId: "warehouse", sql: "select count(*) from public.orders", maxRows: 100 }).',
|
||||
'Execute one parser-validated read-only SQL query against a configured ktx connection. Example: sql_execution({ connectionId: "warehouse", sql: "select count(*) from public.orders", maxRows: 100 }).',
|
||||
memory_ingest:
|
||||
'Ingest free-form markdown knowledge into durable KTX memory. Use this for business rules, metric definitions, schema gotchas, recurring findings, or explicit user requests to remember something. Example: memory_ingest({ connectionId: "warehouse", content: "ARR is reported in cents in this warehouse." }).',
|
||||
'Ingest free-form markdown knowledge into durable ktx memory. Use this for business rules, metric definitions, schema gotchas, recurring findings, or explicit user requests to remember something. Example: memory_ingest({ connectionId: "warehouse", content: "ARR is reported in cents in this warehouse." }).',
|
||||
memory_ingest_status:
|
||||
'Read the current or final status for a memory ingest run. Example: memory_ingest_status({ runId: "memory-run-1" }).',
|
||||
} satisfies Record<string, string>;
|
||||
|
|
@ -856,7 +856,7 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void
|
|||
const ingestInput: MemoryAgentInput = {
|
||||
userId: userContext.userId,
|
||||
chatId: `mcp-${randomUUID()}`,
|
||||
userMessage: 'Ingest external knowledge into KTX memory.',
|
||||
userMessage: 'Ingest external knowledge into ktx memory.',
|
||||
assistantMessage: input.content,
|
||||
connectionId: input.connectionId,
|
||||
sourceType: 'external_ingest',
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ import type {
|
|||
|
||||
const promptsDir = fileURLToPath(new URL('../../prompts', import.meta.url));
|
||||
const skillsDir = fileURLToPath(new URL('../../skills', import.meta.url));
|
||||
const LOCAL_AUTHOR = { name: 'KTX Local', email: 'local@ktx.local' };
|
||||
const LOCAL_AUTHOR = { name: 'ktx Local', email: 'local@ktx.local' };
|
||||
const LOCAL_SHAPE_WARNING = 'Local memory ingest validates semantic-layer YAML shape only.';
|
||||
|
||||
export interface CreateLocalProjectMemoryIngestOptions {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ const llmSchema = z
|
|||
models: z
|
||||
.partialRecord(z.enum(KTX_MODEL_ROLES), z.string().min(1))
|
||||
.default({})
|
||||
.describe('Per-role model overrides keyed by KTX model role (e.g. "default", "triage"). Values are provider-specific model identifiers.'),
|
||||
.describe('Per-role model overrides keyed by ktx model role (e.g. "default", "triage"). Values are provider-specific model identifiers.'),
|
||||
promptCaching: promptCachingSchema.optional().describe('Optional prompt-caching tunables.'),
|
||||
})
|
||||
.describe('LLM provider, per-role model overrides, and prompt-caching tunables.');
|
||||
|
|
@ -243,14 +243,14 @@ const storageSchema = z
|
|||
state: z
|
||||
.enum(KTX_STORAGE_STATES)
|
||||
.default('sqlite')
|
||||
.describe('Backend for KTX state storage. "sqlite" uses .ktx/db.sqlite; "postgres" expects a configured Postgres connection.'),
|
||||
.describe('Backend for ktx state storage. "sqlite" uses .ktx/db.sqlite; "postgres" expects a configured Postgres connection.'),
|
||||
search: z
|
||||
.enum(KTX_SEARCH_BACKENDS)
|
||||
.default('sqlite-fts5')
|
||||
.describe('Backend for search indexes. "sqlite-fts5" uses SQLite FTS5; "postgres-hybrid" uses Postgres lexical + vector hybrid search.'),
|
||||
git: storageGitSchema.prefault({}).describe('Git-backed storage commit policy.'),
|
||||
})
|
||||
.describe('Storage backends and commit policy for KTX state and search indexes.');
|
||||
.describe('Storage backends and commit policy for ktx state and search indexes.');
|
||||
|
||||
const connectionSchema = connectionConfigSchema;
|
||||
|
||||
|
|
@ -282,13 +282,13 @@ const ktxProjectConfigSchema = z
|
|||
.record(z.string(), connectionSchema)
|
||||
.default({})
|
||||
.describe('Map of connection ID to connector configuration. Keys are user-chosen names referenced elsewhere in the config.'),
|
||||
storage: storageSchema.prefault({}).describe('Storage backends and commit policy for KTX state and search indexes.'),
|
||||
storage: storageSchema.prefault({}).describe('Storage backends and commit policy for ktx state and search indexes.'),
|
||||
llm: llmSchema.prefault({}).describe('LLM provider, per-role model overrides, and prompt-caching tunables.'),
|
||||
ingest: ingestSchema.prefault({}).describe('Ingest pipeline configuration.'),
|
||||
agent: agentSchema.prefault({}).describe('Agent feature configuration.'),
|
||||
scan: scanSchema.prefault({}).describe('Schema-scan configuration: enrichment and relationship discovery.'),
|
||||
})
|
||||
.describe('Configuration schema for KTX project files (ktx.yaml).');
|
||||
.describe('Configuration schema for ktx project files (ktx.yaml).');
|
||||
|
||||
export type KtxProjectConfig = z.infer<typeof ktxProjectConfigSchema>;
|
||||
export type KtxProjectLlmConfig = z.infer<typeof llmSchema>;
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ const lookerConnectionSchema = z
|
|||
.min(1)
|
||||
.optional()
|
||||
.describe('Reference to Looker OAuth client secret (e.g. env:LOOKER_CLIENT_SECRET).'),
|
||||
mappings: lookerMappingsSchema.optional().describe('Looker connection-name to KTX warehouse mappings.'),
|
||||
mappings: lookerMappingsSchema.optional().describe('Looker connection-name to ktx warehouse mappings.'),
|
||||
})
|
||||
.describe('Looker context-source connection.');
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const metabaseMappingsSchema = z
|
|||
databaseMappings: z
|
||||
.record(z.string(), stringTargetSchema)
|
||||
.default({})
|
||||
.describe('Map of Metabase database ID (positive integer string) to KTX connection ID. Use null to explicitly unmap.'),
|
||||
.describe('Map of Metabase database ID (positive integer string) to ktx connection ID. Use null to explicitly unmap.'),
|
||||
syncEnabled: z
|
||||
.record(z.string(), z.boolean())
|
||||
.default({})
|
||||
|
|
@ -38,7 +38,7 @@ export const lookerMappingsSchema = z
|
|||
connectionMappings: z
|
||||
.record(z.string().min(1), stringTargetSchema)
|
||||
.default({})
|
||||
.describe('Map of Looker connection name to KTX connection ID. Use null to explicitly unmap.'),
|
||||
.describe('Map of Looker connection name to ktx connection ID. Use null to explicitly unmap.'),
|
||||
})
|
||||
.describe('Looker connection-to-warehouse mapping configuration.');
|
||||
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ export async function initKtxProject(options: InitKtxProjectOptions): Promise<In
|
|||
|
||||
const commit = await runtime.git.commitFiles(
|
||||
['ktx.yaml', ...TRACKED_SCAFFOLD_FILES.map((file) => file.path)],
|
||||
`Initialize KTX project: ${projectName}`,
|
||||
`Initialize ktx project: ${projectName}`,
|
||||
authorName,
|
||||
authorEmail,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -569,7 +569,7 @@ export class KtxDescriptionGenerator {
|
|||
|
||||
if (!connector.sampleTable) {
|
||||
fallbackReason = 'capability_missing';
|
||||
this.logger?.warn('KTX scan connector does not support table sampling; falling back to metadata-only prompt', {
|
||||
this.logger?.warn('ktx scan connector does not support table sampling; falling back to metadata-only prompt', {
|
||||
connectorId: input.connector.id,
|
||||
table: input.table.name,
|
||||
});
|
||||
|
|
@ -690,7 +690,7 @@ export class KtxDescriptionGenerator {
|
|||
let fallbackReason: 'capability_missing' | 'sampling_failed' | 'empty_sample' | null = null;
|
||||
if (!input.connector.sampleTable) {
|
||||
fallbackReason = 'capability_missing';
|
||||
this.logger?.warn('KTX scan connector does not support table sampling; falling back to metadata-only prompt', {
|
||||
this.logger?.warn('ktx scan connector does not support table sampling; falling back to metadata-only prompt', {
|
||||
connectorId: input.connector.id,
|
||||
table: input.table.name,
|
||||
});
|
||||
|
|
@ -846,7 +846,7 @@ export class KtxDescriptionGenerator {
|
|||
}
|
||||
|
||||
if (!input.connector.sampleTable) {
|
||||
this.logger?.warn('KTX scan connector does not support table sampling for data-source description generation', {
|
||||
this.logger?.warn('ktx scan connector does not support table sampling for data-source description generation', {
|
||||
connectorId: input.connector.id,
|
||||
});
|
||||
return 'No accessible tables found in database';
|
||||
|
|
@ -927,7 +927,7 @@ export class KtxDescriptionGenerator {
|
|||
let columnValues = column.sampleValues;
|
||||
if (!columnValues || columnValues.length === 0) {
|
||||
if (!input.connector.sampleColumn) {
|
||||
this.logger?.warn('KTX scan connector does not support column sampling; using available metadata only', {
|
||||
this.logger?.warn('ktx scan connector does not support column sampling; using available metadata only', {
|
||||
connectorId: input.connector.id,
|
||||
table: input.table.name,
|
||||
column: column.name,
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ function scanReportPath(connectionId: string, syncId: string): string {
|
|||
|
||||
function assertSupportedMode(mode: KtxScanMode): void {
|
||||
if (mode !== 'structural' && mode !== 'relationships' && mode !== 'enriched') {
|
||||
throw new Error(`Unsupported KTX scan mode: ${mode}`);
|
||||
throw new Error(`Unsupported ktx scan mode: ${mode}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -544,7 +544,7 @@ export async function runLocalScan(options: RunLocalScanOptions): Promise<LocalS
|
|||
}
|
||||
report.warnings.push({
|
||||
code: 'enrichment_failed',
|
||||
message: `KTX scan enrichment failed after structural scan completed: ${message}`,
|
||||
message: `ktx scan enrichment failed after structural scan completed: ${message}`,
|
||||
recoverable: true,
|
||||
metadata: { mode, detectRelationships: options.detectRelationships ?? false },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ function parseWarning(rawWarning: unknown, path: string): KtxScanWarning {
|
|||
typeof rawWarning.message !== 'string' ||
|
||||
typeof rawWarning.recoverable !== 'boolean'
|
||||
) {
|
||||
throw new Error(`Invalid KTX schema warning artifact: ${path}`);
|
||||
throw new Error(`Invalid ktx schema warning artifact: ${path}`);
|
||||
}
|
||||
return {
|
||||
code: rawWarning.code as KtxScanWarning['code'],
|
||||
|
|
@ -73,7 +73,7 @@ async function readWarnings(input: ReadLocalScanStructuralSnapshotInput): Promis
|
|||
const warningRaw = await input.project.fileStore.readFile(path);
|
||||
const parsed = JSON.parse(warningRaw.content) as unknown;
|
||||
if (!isRecord(parsed) || !Array.isArray(parsed.warnings)) {
|
||||
throw new Error(`Invalid KTX schema warnings artifact: ${path}`);
|
||||
throw new Error(`Invalid ktx schema warnings artifact: ${path}`);
|
||||
}
|
||||
return parsed.warnings.map((warning) => parseWarning(warning, path));
|
||||
} catch (error) {
|
||||
|
|
@ -102,7 +102,7 @@ function parseColumn(rawColumn: unknown, path: string): KtxSchemaColumn {
|
|||
rawColumn.dimensionType !== 'number' &&
|
||||
rawColumn.dimensionType !== 'boolean')
|
||||
) {
|
||||
throw new Error(`Invalid KTX schema column artifact: ${path}`);
|
||||
throw new Error(`Invalid ktx schema column artifact: ${path}`);
|
||||
}
|
||||
return {
|
||||
name: rawColumn.name,
|
||||
|
|
@ -122,7 +122,7 @@ function parseForeignKey(rawForeignKey: unknown, path: string): KtxSchemaForeign
|
|||
typeof rawForeignKey.toTable !== 'string' ||
|
||||
typeof rawForeignKey.toColumn !== 'string'
|
||||
) {
|
||||
throw new Error(`Invalid KTX schema foreign key artifact: ${path}`);
|
||||
throw new Error(`Invalid ktx schema foreign key artifact: ${path}`);
|
||||
}
|
||||
return {
|
||||
fromColumn: rawForeignKey.fromColumn,
|
||||
|
|
@ -137,7 +137,7 @@ function parseForeignKey(rawForeignKey: unknown, path: string): KtxSchemaForeign
|
|||
function parseTable(raw: string, path: string): KtxSchemaTable {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!isRecord(parsed) || typeof parsed.name !== 'string' || !Array.isArray(parsed.columns)) {
|
||||
throw new Error(`Invalid KTX schema table artifact: ${path}`);
|
||||
throw new Error(`Invalid ktx schema table artifact: ${path}`);
|
||||
}
|
||||
return {
|
||||
catalog: optionalStringOrNull(parsed.catalog) ?? null,
|
||||
|
|
|
|||
|
|
@ -317,7 +317,7 @@ function compositeSkipBlocks(report: KtxRelationshipBenchmarkReport): string[] {
|
|||
|
||||
export function formatKtxRelationshipBenchmarkReportMarkdown(report: KtxRelationshipBenchmarkReport): string {
|
||||
const lines = [
|
||||
'# KTX Relationship Discovery Benchmark Evidence',
|
||||
'# ktx Relationship Discovery Benchmark Evidence',
|
||||
'',
|
||||
`Generated: ${report.generatedAt}`,
|
||||
'',
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ async function detectCompositeRelationships(input: {
|
|||
} catch (error) {
|
||||
input.warnings.push({
|
||||
code: 'relationship_validation_failed',
|
||||
message: `KTX composite relationship detection failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
message: `ktx composite relationship detection failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
recoverable: true,
|
||||
metadata: { source: 'composite_relationship_detection' },
|
||||
});
|
||||
|
|
@ -185,7 +185,7 @@ function sqlExecutor(input: DiscoverKtxRelationshipsInput): {
|
|||
warnings: [
|
||||
{
|
||||
code: 'connector_capability_missing',
|
||||
message: 'KTX scan connector cannot run read-only SQL relationship validation',
|
||||
message: 'ktx scan connector cannot run read-only SQL relationship validation',
|
||||
recoverable: true,
|
||||
metadata: { capability: 'readOnlySql' },
|
||||
},
|
||||
|
|
@ -199,7 +199,7 @@ function sqlExecutor(input: DiscoverKtxRelationshipsInput): {
|
|||
warnings: [
|
||||
{
|
||||
code: 'relationship_validation_failed',
|
||||
message: 'KTX scan connector advertises readOnlySql but does not expose executeReadOnly',
|
||||
message: 'ktx scan connector advertises readOnlySql but does not expose executeReadOnly',
|
||||
recoverable: true,
|
||||
metadata: { capability: 'readOnlySql' },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ function mapValidProposals(
|
|||
const toColumn = toTable ? findColumn(toTable, item.toColumn) : null;
|
||||
if (!fromTable || !toTable || !fromColumn || !toColumn) {
|
||||
warnings.push(
|
||||
invalidReferenceWarning('KTX relationship LLM proposal referenced a table or column that is not in the schema.', {
|
||||
invalidReferenceWarning('ktx relationship LLM proposal referenced a table or column that is not in the schema.', {
|
||||
proposal: item,
|
||||
}),
|
||||
);
|
||||
|
|
@ -218,7 +218,7 @@ function generationFailureWarning(error: unknown): KtxScanWarning {
|
|||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
code: 'relationship_llm_proposal_failed',
|
||||
message: `KTX relationship LLM proposal failed: ${message}`,
|
||||
message: `ktx relationship LLM proposal failed: ${message}`,
|
||||
recoverable: true,
|
||||
};
|
||||
}
|
||||
|
|
@ -233,7 +233,7 @@ export async function proposeKtxRelationshipCandidatesWithLlm(
|
|||
const settings = mergeSettings(input.settings);
|
||||
const evidence = buildEvidencePacket(input.schema, input.profile, settings);
|
||||
const system = [
|
||||
'You are helping KTX review possible SQL relationships before validation.',
|
||||
'You are helping ktx review possible SQL relationships before validation.',
|
||||
'Use only the compact schema evidence. Propose likely primary keys and foreign keys for later SQL validation.',
|
||||
'Return structured output only; never assume a join is accepted.',
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export class SemanticLayerService {
|
|||
async listConnectionIds(): Promise<string[]> {
|
||||
try {
|
||||
const result = await this.configService.listFiles(SL_DIR_PREFIX);
|
||||
// Directories under semantic-layer/ are connectionIds. Local KTX projects use
|
||||
// Directories under semantic-layer/ are connectionIds. Local ktx projects use
|
||||
// readable ids like "warehouse" and "dbt-main", not only UUIDs.
|
||||
return result.files
|
||||
.map((f) => f.replace(`${SL_DIR_PREFIX}/`, '').split('/')[0])
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ZodType } from 'zod';
|
||||
import type { z } from 'zod';
|
||||
import type { GitAuthorResolverPort } from '../../../context/tools/authors.js';
|
||||
import type { ToolContext, ToolOutput } from '../../../context/tools/base-tool.js';
|
||||
import { BaseTool } from '../../../context/tools/base-tool.js';
|
||||
|
|
@ -27,7 +27,9 @@ export interface BaseSemanticLayerToolDeps {
|
|||
|
||||
// ── Abstract base class ──
|
||||
|
||||
export abstract class BaseSemanticLayerTool<TInput extends ZodType = ZodType> extends BaseTool<TInput> {
|
||||
export abstract class BaseSemanticLayerTool<
|
||||
TInput extends z.ZodObject<z.ZodRawShape> = z.ZodObject<z.ZodRawShape>,
|
||||
> extends BaseTool<TInput> {
|
||||
protected readonly semanticLayerService: SemanticLayerService;
|
||||
protected readonly slSearchService: SlSearchService;
|
||||
protected readonly authorResolver: GitAuthorResolverPort;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { tool } from 'ai';
|
||||
import { z, type ZodType } from 'zod';
|
||||
import { tool, type Tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { noopLogger, type KtxLogger } from '../../context/core/config.js';
|
||||
import type { KtxRuntimeToolDescriptor } from '../llm/runtime-port.js';
|
||||
import { normalizeKtxRuntimeToolOutput } from '../llm/runtime-tools.js';
|
||||
|
|
@ -73,7 +73,7 @@ interface MethodologyEntry {
|
|||
/**
|
||||
* SECURITY: All tools require authentication. userId must always be provided in ToolContext.
|
||||
*/
|
||||
export abstract class BaseTool<TInput extends ZodType = ZodType> {
|
||||
export abstract class BaseTool<TInput extends z.ZodObject<z.ZodRawShape> = z.ZodObject<z.ZodRawShape>> {
|
||||
protected readonly logger: KtxLogger;
|
||||
|
||||
abstract readonly name: string;
|
||||
|
|
@ -86,37 +86,9 @@ export abstract class BaseTool<TInput extends ZodType = ZodType> {
|
|||
|
||||
abstract get inputSchema(): TInput;
|
||||
|
||||
abstract call(input: z.infer<TInput>, context: ToolContext): Promise<any>;
|
||||
abstract call(input: z.infer<TInput>, context: ToolContext): Promise<unknown>;
|
||||
|
||||
getParametersSchema(): {
|
||||
type: 'object';
|
||||
properties: Record<string, any>;
|
||||
required?: string[];
|
||||
} {
|
||||
const jsonSchema = z.toJSONSchema(this.inputSchema, {
|
||||
target: 'draft-7',
|
||||
});
|
||||
|
||||
return jsonSchema as any;
|
||||
}
|
||||
|
||||
toAnthropicFormat(): {
|
||||
name: string;
|
||||
description: string;
|
||||
input_schema: {
|
||||
type: 'object';
|
||||
properties: Record<string, any>;
|
||||
required?: string[];
|
||||
};
|
||||
} {
|
||||
return {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
input_schema: this.getParametersSchema(),
|
||||
};
|
||||
}
|
||||
|
||||
toAiSdkTool(context: ToolContext): any {
|
||||
toAiSdkTool(context: ToolContext): Tool {
|
||||
const toolName = this.name;
|
||||
const logger = this.logger;
|
||||
|
||||
|
|
@ -137,7 +109,7 @@ export abstract class BaseTool<TInput extends ZodType = ZodType> {
|
|||
if (!callContext.userId) {
|
||||
throw new Error('Authentication required: userId must be provided in ToolContext');
|
||||
}
|
||||
const parsedInput = this.parseInput(params as Record<string, any>);
|
||||
const parsedInput = this.parseInput(params as Record<string, unknown>);
|
||||
const result = await this.call(parsedInput, callContext);
|
||||
return result;
|
||||
} catch (error) {
|
||||
|
|
@ -171,19 +143,19 @@ export abstract class BaseTool<TInput extends ZodType = ZodType> {
|
|||
return {
|
||||
name: toolName,
|
||||
description: this.description,
|
||||
inputSchema: this.inputSchema as unknown as KtxRuntimeToolDescriptor['inputSchema'],
|
||||
inputSchema: this.inputSchema,
|
||||
execute: async (params) => {
|
||||
const callContext = { ...context };
|
||||
if (!callContext.userId) {
|
||||
throw new Error('Authentication required: userId must be provided in ToolContext');
|
||||
}
|
||||
const parsedInput = this.parseInput(params as Record<string, any>);
|
||||
const parsedInput = this.parseInput(params as Record<string, unknown>);
|
||||
return normalizeKtxRuntimeToolOutput(await this.call(parsedInput, callContext));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
parseInput(input: Record<string, any>): z.infer<TInput> {
|
||||
parseInput(input: Record<string, unknown>): z.infer<TInput> {
|
||||
return this.inputSchema.parse(input);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue