From f7245073dfcd4dbf87e17eca29edb68fb33b8d6f Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Wed, 13 May 2026 00:29:17 +0200 Subject: [PATCH] fix(context): report structured entity detail misses --- .../entity-details.tool.test.ts | 30 +++++++++++ .../entity-details.tool.ts | 51 ++++++++++++++----- 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/packages/context/src/ingest/tools/warehouse-verification/entity-details.tool.test.ts b/packages/context/src/ingest/tools/warehouse-verification/entity-details.tool.test.ts index 4d58f58c..9188bc68 100644 --- a/packages/context/src/ingest/tools/warehouse-verification/entity-details.tool.test.ts +++ b/packages/context/src/ingest/tools/warehouse-verification/entity-details.tool.test.ts @@ -143,6 +143,36 @@ describe('EntityDetailsTool', () => { expect(result.structured.missing).toHaveLength(1); }); + it('reports missing structured table targets in model-visible markdown', async () => { + const result = await tool.call( + { + connectionName: 'warehouse', + targets: [{ catalog: null, db: 'public', name: 'orderz' }], + }, + context, + ); + + expect(result.markdown).toContain('Not found in scan: public.orderz'); + expect(result.markdown).toContain('Closest matches: orders'); + expect(result.structured.resolved).toHaveLength(0); + expect(result.structured.missing).toHaveLength(1); + }); + + it('reports missing structured column targets in model-visible markdown', async () => { + const result = await tool.call( + { + connectionName: 'warehouse', + targets: [{ catalog: null, db: 'public', name: 'orders', column: 'plan_tier' }], + }, + context, + ); + + expect(result.markdown).toContain('Column not found in scan: public.orders.plan_tier'); + expect(result.markdown).toContain('Available columns: id, status'); + expect(result.structured.resolved).toHaveLength(0); + expect(result.structured.missing).toHaveLength(1); + }); + it('returns a no-scan state distinct from not found', async () => { const result = await tool.call( { connectionName: 'empty', targets: [{ display: 'public.orders' }] }, diff --git a/packages/context/src/ingest/tools/warehouse-verification/entity-details.tool.ts b/packages/context/src/ingest/tools/warehouse-verification/entity-details.tool.ts index 7337a884..27cf55a0 100644 --- a/packages/context/src/ingest/tools/warehouse-verification/entity-details.tool.ts +++ b/packages/context/src/ingest/tools/warehouse-verification/entity-details.tool.ts @@ -19,6 +19,7 @@ const entityDetailsInputSchema = z.object({ }); type EntityDetailsInput = z.infer; +type EntityDetailsTarget = EntityDetailsInput['targets'][number]; export interface EntityDetailsStructured { resolved: TableDetail[]; @@ -30,6 +31,41 @@ function allowedConnectionNames(context: ToolContext): ReadonlySet | nul return context.session?.allowedConnectionNames ?? null; } +function targetLabel(target: EntityDetailsTarget): string { + if ('display' in target) { + return target.display; + } + return [target.catalog, target.db, target.name, target.column].filter((part): part is string => !!part).join('.'); +} + +function appendMissingTargetMarkdown(parts: string[], target: EntityDetailsTarget, candidates: KtxTableRef[]): void { + parts.push(`Not found in scan: ${targetLabel(target)}`); + if (candidates.length > 0) { + parts.push(`Closest matches: ${candidates.map((candidate) => candidate.name).join(', ')}`); + } +} + +async function resolveTarget( + catalog: WarehouseCatalogService, + connectionName: string, + target: EntityDetailsTarget, +): Promise<{ resolved: (KtxTableRef & { column?: string }) | null; candidates: KtxTableRef[] }> { + if ('display' in target) { + return catalog.resolveDisplayTarget(connectionName, target.display); + } + + const candidateResolution = await catalog.resolveDisplayTarget(connectionName, targetLabel(target)); + return { + resolved: { + catalog: target.catalog, + db: target.db, + name: target.name, + column: target.column, + }, + candidates: candidateResolution.candidates, + }; +} + function sampleText(values: string[]): string { return values.length > 0 ? ` - sample: ${JSON.stringify(values.slice(0, 10))}` : ''; } @@ -92,25 +128,16 @@ export class EntityDetailsTool extends BaseTool const missing: EntityDetailsStructured['missing'] = []; for (const target of input.targets) { - const resolution = - 'display' in target - ? await catalog.resolveDisplayTarget(input.connectionName, target.display) - : { - resolved: { catalog: target.catalog, db: target.db, name: target.name, column: target.column }, - candidates: [], - dialect: '', - }; + const resolution = await resolveTarget(catalog, input.connectionName, target); if (!resolution.resolved) { missing.push({ target, candidates: resolution.candidates }); - parts.push(`Not found in scan: ${'display' in target ? target.display : target.name}`); - if (resolution.candidates.length > 0) { - parts.push(`Closest matches: ${resolution.candidates.map((candidate) => candidate.name).join(', ')}`); - } + appendMissingTargetMarkdown(parts, target, resolution.candidates); continue; } const detail = await catalog.getTable({ connectionName: input.connectionName, ...resolution.resolved }); if (!detail) { missing.push({ target, candidates: resolution.candidates }); + appendMissingTargetMarkdown(parts, target, resolution.candidates); continue; } const requestedColumn = resolution.resolved.column;