chore(workspace): gate dead-code with knip production mode

Turn on production-mode knip plus an autofix run in pre-commit and the
`pnpm dead-code` script, document the `/** @internal */` convention for
test-only exports in AGENTS.md, annotate test-only exports across the
CLI with that JSDoc, and drop dead exports/wrappers the new gate
surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`,
`createLocalScanEnrichmentProvidersFromConfig`,
`PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports).
Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit
production entries so cross-package barrel leaks are caught.
This commit is contained in:
Andrey Avtomonov 2026-05-21 11:35:41 +02:00
parent ac3885b652
commit b690e6988b
71 changed files with 211 additions and 279 deletions

View file

@ -3,6 +3,7 @@ import { join, relative } from 'node:path';
const YAML_EXT_RE = /\.(ya?ml)$/i;
/** @internal */
export function normalizeDbtPath(path: string): string {
return path.replaceAll('\\', '/');
}

View file

@ -1,13 +1,14 @@
import { Buffer } from 'node:buffer';
import type { StagedPatternsInput } from './types.js';
export const HISTORIC_SQL_PATTERN_WORKUNIT_DIR = 'patterns-input';
const HISTORIC_SQL_PATTERN_WORKUNIT_DIR = 'patterns-input';
/** @internal */
export const HISTORIC_SQL_PATTERN_WORKUNIT_MAX_BYTES = 110_000;
export const HISTORIC_SQL_PATTERN_WORKUNIT_PATH_RE = /^patterns-input\/part-\d{4}\.json$/;
const HISTORIC_SQL_PATTERN_WORKUNIT_PATH_RE = /^patterns-input\/part-\d{4}\.json$/;
type PatternTemplate = StagedPatternsInput['templates'][number];
export interface HistoricSqlPatternInputShard {
interface HistoricSqlPatternInputShard {
path: string;
input: StagedPatternsInput;
byteLength: number;
@ -27,10 +28,11 @@ export function isHistoricSqlPatternInputShardPath(path: string): boolean {
return HISTORIC_SQL_PATTERN_WORKUNIT_PATH_RE.test(path);
}
export function serializeStagedPatternsInput(input: StagedPatternsInput): string {
function serializeStagedPatternsInput(input: StagedPatternsInput): string {
return `${JSON.stringify(input, null, 2)}\n`;
}
/** @internal */
export function serializedStagedPatternsInputByteLength(input: StagedPatternsInput): number {
return Buffer.byteLength(serializeStagedPatternsInput(input), 'utf-8');
}

View file

@ -33,6 +33,7 @@ function tableSortKey(table: KtxTableRef): string {
return `${table.catalog ?? ''}\u0000${table.db ?? ''}\u0000${table.name}`;
}
/** @internal */
export function liveDatabaseTablePath(table: KtxTableRef): string {
return `${LIVE_DATABASE_TABLES_DIR}/${encodePathPart(table.catalog)}.${encodePathPart(table.db)}.${encodePathPart(
table.name,

View file

@ -1,11 +1,11 @@
import { mkdir, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import type { ParsedTargetTable } from '../../parsed-target-table.js';
import type { FetchContext } from '../../types.js';
import { writeLookerEvidenceDocuments } from './evidence-documents.js';
import { writeLookerFetchReport } from './fetch-report.js';
import {
type LookerPullConfig,
type ParsedTargetTable,
parseLookerPullConfig,
STAGED_FILES,
type StagedDashboardFile,

View file

@ -1,11 +1,6 @@
import type { ParsedTargetTable } from '../../parsed-target-table.js';
import type { LookerWarehouseConnectionInfo } from './client.js';
import type {
LookerPullConfig,
LookerRuntimeCursors,
ParsedTargetTable,
StagedExploreFile,
StagedLookmlModelsFile,
} from './types.js';
import type { LookerPullConfig, LookerRuntimeCursors, StagedExploreFile, StagedLookmlModelsFile } from './types.js';
export const LOOKER_DIALECT_TO_CONNECTION_TYPE = {
bigquery: 'BIGQUERY',

View file

@ -1,17 +1,5 @@
import { describe, expect, it } from 'vitest';
import { buildLookerReconcileNotes, lookerRuntimeSourceToFileAdapterSource } from './reconcile.js';
describe('lookerRuntimeSourceToFileAdapterSource', () => {
it('maps API-derived Looker source names to file-adapter source names', () => {
expect(lookerRuntimeSourceToFileAdapterSource('looker__b2b__sales_pipeline')).toBe('b2b__sales_pipeline');
expect(lookerRuntimeSourceToFileAdapterSource('looker__finance__orders')).toBe('finance__orders');
});
it('ignores non-Looker and malformed source names', () => {
expect(lookerRuntimeSourceToFileAdapterSource('b2b__sales_pipeline')).toBeNull();
expect(lookerRuntimeSourceToFileAdapterSource('looker__missing_explore')).toBeNull();
});
});
import { buildLookerReconcileNotes } from './reconcile.js';
describe('buildLookerReconcileNotes', () => {
it('instructs reconciliation to record subsumed provenance', () => {

View file

@ -1,16 +1,3 @@
export function lookerRuntimeSourceToFileAdapterSource(sourceName: string): string | null {
if (!sourceName.startsWith('looker__')) {
return null;
}
const stripped = sourceName.slice('looker__'.length);
const parts = stripped.split('__');
if (parts.length < 2 || parts.some((part) => part.length === 0)) {
return null;
}
const [model, ...exploreParts] = parts;
return `${model}__${exploreParts.join('__')}`;
}
export function buildLookerReconcileNotes(): string[] {
return [
[

View file

@ -1,7 +1,8 @@
import { tool } from 'ai';
import { z } from 'zod';
import type { ToolOutput } from '../../../../tools/index.js';
import { type ParsedTargetTable, stagedLookerQuerySchema } from '../types.js';
import type { ParsedTargetTable } from '../../../parsed-target-table.js';
import { stagedLookerQuerySchema } from '../types.js';
const lookerUsageInputSchema = z.object({
queryCount30d: z.number().int().nonnegative().default(0),

View file

@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest';
import { parsedTargetTableSchema } from '../../parsed-target-table.js';
import {
lookerPullConfigSchema,
parseLookerPullConfig,
parsedTargetTableSchema,
stagedDashboardFileSchema,
stagedExploreFileSchema,
stagedLookerFetchIssueSchema,

View file

@ -7,8 +7,6 @@ const nullableLookerIdSchema = z.union([lookerIdSchema, z.null()]).default(null)
export const lookerConnectionIdSchema = z.string().min(1).regex(/^[A-Za-z0-9_-]+$/);
export { parsedTargetTableSchema, type ParsedTargetTable } from '../../parsed-target-table.js';
export const lookerRuntimeCursorsSchema = z.object({
dashboardsLastSyncedAt: z.iso.datetime().nullable().default(null),
looksLastSyncedAt: z.iso.datetime().nullable().default(null),
@ -16,6 +14,7 @@ export const lookerRuntimeCursorsSchema = z.object({
export type LookerRuntimeCursors = z.infer<typeof lookerRuntimeCursorsSchema>;
/** @internal */
export const lookerPullConfigSchema = z.object({
lookerConnectionId: lookerConnectionIdSchema.optional(),
instanceBaseUrl: z.url().optional(),

View file

@ -4,7 +4,9 @@ import * as z from 'zod';
import type { SourceFetchReport } from '../../types.js';
import type { ParsedLookmlProject } from './parse.js';
/** @internal */
export const LOOKML_FETCH_REPORT_FILE = 'lookml-fetch-report.json';
/** @internal */
export const LOOKML_MISMATCHED_MODELS_FILE = 'lookml-mismatched-models.json';
const fetchIssueKindSchema = z.enum([

View file

@ -1,7 +1,7 @@
import * as z from 'zod';
import { parsedTargetTableSchema } from '../../parsed-target-table.js';
export const lookmlPullConfigSchema = z.object({
const lookmlPullConfigSchema = z.object({
repoUrl: z.string().url(),
branch: z.string().default('main'),
path: z.string().nullable().default(null),

View file

@ -6,6 +6,7 @@ const CARD_REF_RE = /\{\{#(\d+)\}\}/g;
* Input TemplateTag shape mirrors `MetabaseClient.getTemplateTags` output. We keep the
* shape loose only `name`, `type`, and optional `cardReference`/`default` are needed here.
*/
/** @internal */
export interface InputTemplateTag {
name: string;
type: string;
@ -13,6 +14,7 @@ export interface InputTemplateTag {
defaultValue?: string | null;
}
/** @internal */
export function extractReferencedCardIds(templateTags: InputTemplateTag[], sql: string): number[] {
const ids = new Set<number>();
for (const tag of templateTags) {
@ -34,7 +36,7 @@ export function extractReferencedCardIds(templateTags: InputTemplateTag[], sql:
* care about. The adapter reads whatever the client returns; this helper stays
* duck-typed so the client's type can evolve without churn here.
*/
export interface InputCard {
interface InputCard {
id: number;
name: string;
description?: string | null;

View file

@ -1,6 +1,6 @@
import { z } from 'zod';
export const metabaseSyncModeSchema = z.enum(['ALL', 'ONLY', 'EXCEPT']);
const metabaseSyncModeSchema = z.enum(['ALL', 'ONLY', 'EXCEPT']);
export type MetabaseSyncMode = z.infer<typeof metabaseSyncModeSchema>;
export const metabaseLocalConnectionIdSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/);
@ -25,7 +25,7 @@ export function parseMetabasePullConfig(raw: unknown): MetabasePullConfig {
}
/** A Metabase column from `card.result_metadata`. Mirrors what the LLM consumes today. */
export const stagedResultColumnSchema = z.object({
const stagedResultColumnSchema = z.object({
name: z.string(),
display_name: z.string().optional().nullable(),
base_type: z.string(),
@ -37,7 +37,7 @@ export const stagedResultColumnSchema = z.object({
export type StagedResultColumn = z.infer<typeof stagedResultColumnSchema>;
export const stagedParameterSchema = z.object({
const stagedParameterSchema = z.object({
id: z.string(),
name: z.string(),
type: z.string(),
@ -49,7 +49,7 @@ export const stagedParameterSchema = z.object({
export type StagedParameter = z.infer<typeof stagedParameterSchema>;
/** A template tag pulled from an MBQL card's `dataset_query.stages[0].template-tags`. */
export const stagedTemplateTagSchema = z.object({
const stagedTemplateTagSchema = z.object({
name: z.string(),
type: z.string(),
defaultValue: z.string().optional().nullable(),
@ -87,7 +87,7 @@ export const stagedCardFileSchema = z.object({
export type StagedCardFile = z.infer<typeof stagedCardFileSchema>;
/** A serialized collection file, `collections/<id>.json`. Minimal — path lives on the card. */
export const stagedCollectionFileSchema = z.object({
const stagedCollectionFileSchema = z.object({
metabaseId: z.union([z.number().int(), z.literal('root')]),
name: z.string(),
parentId: z.union([z.number().int(), z.literal('root')]).nullable(),
@ -96,7 +96,7 @@ export const stagedCollectionFileSchema = z.object({
export type StagedCollectionFile = z.infer<typeof stagedCollectionFileSchema>;
/** A serialized database-mapping snapshot, `databases/<id>.json`. */
export const stagedDatabaseFileSchema = z.object({
const stagedDatabaseFileSchema = z.object({
metabaseDatabaseId: z.number().int().positive(),
metabaseDatabaseName: z.string(),
metabaseEngine: z.string().nullable(),

View file

@ -2,7 +2,7 @@ import { readdir, readFile } from 'node:fs/promises';
import { join, relative } from 'node:path';
import { parse as parseYaml } from 'yaml';
export interface ParsedMetricFlowSemanticModel {
interface ParsedMetricFlowSemanticModel {
/** Path relative to stagedDir, e.g. "models/orders.yml". */
path: string;
/** `name:` on the semantic_model. */
@ -24,9 +24,9 @@ export interface ParsedMetricFlowSemanticModel {
defaultTimeDimension: string | null;
}
export type MetricFlowMetricType = 'simple' | 'derived' | 'cumulative' | 'ratio' | 'conversion';
type MetricFlowMetricType = 'simple' | 'derived' | 'cumulative' | 'ratio' | 'conversion';
export interface ParsedMetricFlowMetric {
interface ParsedMetricFlowMetric {
path: string;
name: string;
type: MetricFlowMetricType;

View file

@ -5,6 +5,7 @@ import { kmeans, pickK } from '../../clustering/kmeans.js';
import type { WorkUnit } from '../../types.js';
import { notionMetadataSchema } from './types.js';
/** @internal */
export const MIN_PAGES_TO_CLUSTER = 5;
const CLUSTER_TEXT_BODY_CHARS = 1024;
const CLUSTER_SEED = 42;

View file

@ -14,6 +14,7 @@ function richTextToMarkdown(value: unknown): string {
.trim();
}
/** @internal */
export function propertyValueToText(value: unknown): string {
if (!value || typeof value !== 'object' || !('type' in value)) {
return '';

View file

@ -89,6 +89,7 @@ function shouldRetryNotionError(error: unknown): boolean {
return code === 'rate_limited' || transientErrorCodes.has(code ?? '') || transientStatusCodes.has(status ?? 0);
}
/** @internal */
export async function retryNotionRequest<T>(operation: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
const maxAttempts = options.maxAttempts ?? 4;
const sleep = options.sleep ?? defaultSleep;

View file

@ -2,7 +2,7 @@ import type { KtxModelRole } from '../../llm/index.js';
import type { KtxEmbeddingPort } from '../core/embedding.js';
import type { GitService, KtxFileStorePort, KtxLogger, SessionOutcome } from '../core/index.js';
import type { AgentRunnerPort, KtxLlmRuntimePort, KtxRuntimeToolSet } from '../llm/index.js';
import type { CaptureSession, MemoryAction, MemoryKnowledgeSlRefsPort } from '../memory/index.js';
import type { MemoryAction, MemoryKnowledgeSlRefsPort } from '../memory/index.js';
import type { PromptService } from '../prompts/index.js';
import type { SkillsRegistryService } from '../skills/index.js';
import type {
@ -34,7 +34,7 @@ import type {
SourceAdapter,
} from './types.js';
export type JsonPrimitive = string | number | boolean | null;
type JsonPrimitive = string | number | boolean | null;
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue | undefined };
export interface IngestRunRecord {
@ -359,9 +359,4 @@ export interface IngestBundleRunnerDeps {
logger?: KtxLogger;
}
export interface IngestCaptureState {
session: CaptureSession;
actions: MemoryAction[];
}
export type IngestRunnerJob = IngestBundleJob;

View file

@ -3,6 +3,7 @@ export interface SemanticLayerTargetPolicyInput {
allowedConnectionIds: ReadonlySet<string>;
}
/** @internal */
export interface SemanticLayerTargetPolicyViolation {
path: string;
connectionId: string;

View file

@ -6,7 +6,7 @@ import type { WorkUnit } from '../types.js';
const MAX_WORK_UNIT_PROMPT_CHARS = 240_000;
export interface TouchedValidationResult {
interface TouchedValidationResult {
invalidSources: string[];
validSources: string[];
}

View file

@ -1,7 +1,7 @@
import type { MemoryAction } from '../../memory/index.js';
import type { TouchedSlSource } from '../../tools/index.js';
export interface StageIndexWorkUnit {
interface StageIndexWorkUnit {
unitKey: string;
rawFiles: string[];
status: 'success' | 'failed';

View file

@ -141,6 +141,7 @@ export interface KtxSqlExecutionResponse {
rowCount: number;
}
/** @internal */
export interface KtxSqlExecutionMcpPort {
execute(
input: { connectionId: string; sql: string; maxRows: number },

View file

@ -256,14 +256,10 @@ const ktxProjectConfigSchema = z
export type KtxProjectConfig = z.infer<typeof ktxProjectConfigSchema>;
export type KtxProjectLlmConfig = z.infer<typeof llmSchema>;
export type KtxProjectLlmProviderConfig = z.infer<typeof llmProviderSchema>;
export type KtxProjectEmbeddingConfig = z.infer<typeof embeddingSchema>;
export type KtxScanEnrichmentConfig = z.infer<typeof scanEnrichmentSchema>;
export type KtxIngestWorkUnitsConfig = z.infer<typeof workUnitsSchema>;
export type KtxScanRelationshipConfig = z.infer<typeof scanRelationshipsSchema>;
export type KtxProjectScanConfig = z.infer<typeof scanSchema>;
export type KtxProjectConnectionConfig = z.infer<typeof connectionSchema>;
export type KtxProjectSetupConfig = z.infer<typeof setupSchema>;
export type KtxStorageState = z.infer<typeof storageSchema>['state'];
export type KtxSearchBackend = z.infer<typeof storageSchema>['search'];

View file

@ -12,7 +12,6 @@ import {
runLocalScanEnrichment,
snapshotToKtxEnrichedSchema,
} from './local-enrichment.js';
import { createLocalScanEnrichmentProvidersFromConfig } from './local-scan.js';
import {
createKtxConnectorCapabilities,
type KtxQueryResult,
@ -813,46 +812,4 @@ describe('local scan enrichment', () => {
}
});
it('resolves gateway LLM providers and passes injected embedding provider through to scan enrichment', () => {
const createKtxLlmProvider = vi.fn(() => ({
getModel: vi.fn().mockReturnValue({ modelId: 'provider/language-model', provider: 'gateway' }),
}));
const embeddingProvider = {
dimensions: 1536,
maxBatchSize: 8,
embed: vi.fn(),
[['embed', 'Many'].join('')]: vi.fn(),
};
const providers = createLocalScanEnrichmentProvidersFromConfig(
{
mode: 'llm',
embeddings: {
backend: 'openai',
model: 'provider/embedding-model',
dimensions: 1536,
batchSize: 8,
openai: { api_key: 'env:OPENAI_API_KEY' }, // pragma: allowlist secret
},
},
{
provider: {
backend: 'gateway',
gateway: {},
},
models: { default: 'provider/language-model' },
},
{
createKtxLlmProvider: createKtxLlmProvider as any,
env: { OPENAI_API_KEY: 'openai-key' }, // pragma: allowlist secret
embeddingProvider: embeddingProvider as any,
},
);
expect(providers?.embedding?.dimensions).toBe(1536);
expect(providers?.embedding?.maxBatchSize).toBe(8);
expect(createKtxLlmProvider).toHaveBeenCalledWith(
expect.objectContaining({ backend: 'gateway', modelSlots: { default: 'provider/language-model' } }),
);
});
});

View file

@ -226,15 +226,6 @@ function resolveLocalScanEnrichmentProviders(
};
}
export function createLocalScanEnrichmentProvidersFromConfig(
config: KtxScanEnrichmentConfig,
llmConfig: KtxProjectLlmConfig,
deps: LocalScanEnrichmentProviderDeps = {},
): KtxLocalScanEnrichmentProviders | null {
const resolved = resolveLocalScanEnrichmentProviders(config, llmConfig, deps);
return resolved.status === 'ready' ? resolved.providers : null;
}
function createLocalScanEnrichmentStateStore(options: RunLocalScanOptions): SqliteLocalScanEnrichmentStateStore | null {
if (options.dryRun) {
return null;

View file

@ -12,7 +12,7 @@ import {
pluralizeKtxRelationshipToken,
singularizeKtxRelationshipToken,
} from './relationship-name-similarity.js';
export type { KtxRelationshipNormalizedName } from './relationship-name-similarity.js';
;
export { normalizeKtxRelationshipName } from './relationship-name-similarity.js';
import type { KtxRelationshipProfileArtifact } from './relationship-profiling.js';
import {

View file

@ -66,7 +66,7 @@ export interface KtxRelationshipDiagnosticsThresholds {
reviewThreshold: number;
}
export interface KtxRelationshipDiagnosticsPolicy {
interface KtxRelationshipDiagnosticsPolicy {
validationRequiredForManifest: boolean;
maxCandidatesPerColumn: number;
profileSampleRows: number;

View file

@ -4,8 +4,8 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Client } from 'pg';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { assertSearchBackendCapabilities, assertSearchBackendConformanceCase } from './index.js';
import { KtxPGliteOwnerProcess, PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES } from './pglite-owner-process.js';
import { assertSearchBackendConformanceCase } from './index.js';
import { KtxPGliteOwnerProcess } from './pglite-owner-process.js';
async function allocatePort(): Promise<number> {
const server = createServer();
@ -107,20 +107,6 @@ describe('KtxPGliteOwnerProcess', () => {
await rm(tempDir, { recursive: true, force: true });
});
it('declares the advanced PGlite search capabilities observed by the spike', () => {
assertSearchBackendCapabilities({
backendName: 'pglite-owner-process',
capabilities: PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES,
expected: {
fts: true,
vector: true,
fuzzy: true,
jsonSearch: true,
arraySearch: false,
},
});
});
it('starts a socket owner process and serves PostgreSQL clients', async () => {
const owner = await KtxPGliteOwnerProcess.start({
dataDir,

View file

@ -3,15 +3,6 @@ import { pg_trgm } from '@electric-sql/pglite/contrib/pg_trgm';
import { vector } from '@electric-sql/pglite/vector';
import { PGLiteSocketServer } from '@electric-sql/pglite-socket';
import { Client, type ClientConfig, type QueryResult, type QueryResultRow } from 'pg';
import type { SearchBackendCapabilities } from './types.js';
export const PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES = {
fts: true,
vector: true,
fuzzy: true,
jsonSearch: true,
arraySearch: false,
} satisfies SearchBackendCapabilities;
export interface KtxPGliteOwnerProcessOptions {
dataDir: string;

View file

@ -25,8 +25,11 @@ export interface LoadAllSourcesResult {
loadErrors: string[];
}
/** @internal */
export class UnknownColumnOverrideError extends Error {}
/** @internal */
export class ColumnNameCollisionError extends Error {}
/** @internal */
export class ConflictingExcludeAndOverrideError extends Error {}
class ComposeContractError extends Error {}

View file

@ -13,7 +13,7 @@ export interface ContextEvidenceSearchArgs {
export type ContextEvidenceSearchMatchReason = 'lexical' | 'semantic' | 'token' | (string & {});
export interface ContextEvidenceSearchLaneSummary {
interface ContextEvidenceSearchLaneSummary {
lane: string;
status: 'available' | 'skipped' | 'failed';
requestedCandidatePoolLimit: number;

View file

@ -23,6 +23,7 @@ interface WikiSearchStructured {
totalFound: number;
}
/** @internal */
export interface WikiSearchAdapterPort {
search(input: { userId: string; query: string; limit: number }): Promise<{
results: Array<{