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:
Andrey Avtomonov 2026-06-11 13:49:45 +02:00 committed by GitHub
parent 005c5fc860
commit 00cdf2de90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
237 changed files with 844 additions and 974 deletions

View file

@ -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 {

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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

View file

@ -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.`,
);
}

View file

@ -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;
}

View file

@ -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>

View file

@ -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>

View file

@ -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 {

View file

@ -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),

View file

@ -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',