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',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue