mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-04 10:52:13 +02:00
feat(cli): add ktx dev schema to emit ktx.yaml JSON Schema
Annotates the Zod config schema with .describe() text on every field and adds generateKtxProjectConfigJsonSchema() plus a ktx dev schema command that prints (or writes) a draft-07 JSON Schema for editors and LLM agents.
This commit is contained in:
parent
b3be54e3fa
commit
bd4b41f681
4 changed files with 299 additions and 110 deletions
|
|
@ -46,5 +46,23 @@ export function registerDevCommands(program: Command, context: KtxCliCommandCont
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
dev
|
||||||
|
.command('schema')
|
||||||
|
.description('Print a JSON Schema describing ktx.yaml (for editors and LLM agents)')
|
||||||
|
.option('--output <file>', 'Write the schema to a file instead of stdout')
|
||||||
|
.action(async (options: { output?: string }) => {
|
||||||
|
const { generateKtxProjectConfigJsonSchema } = await import('@ktx/context/project');
|
||||||
|
const json = `${JSON.stringify(generateKtxProjectConfigJsonSchema(), null, 2)}\n`;
|
||||||
|
if (options.output) {
|
||||||
|
const { writeFile } = await import('node:fs/promises');
|
||||||
|
const target = resolve(options.output);
|
||||||
|
await writeFile(target, json, 'utf8');
|
||||||
|
context.io.stdout.write(`Wrote ${target}\n`);
|
||||||
|
} else {
|
||||||
|
context.io.stdout.write(json);
|
||||||
|
}
|
||||||
|
context.setExitCode(0);
|
||||||
|
});
|
||||||
|
|
||||||
registerRuntimeCommands(dev, context);
|
registerRuntimeCommands(dev, context);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import {
|
import {
|
||||||
buildDefaultKtxProjectConfig,
|
buildDefaultKtxProjectConfig,
|
||||||
|
generateKtxProjectConfigJsonSchema,
|
||||||
parseKtxProjectConfig,
|
parseKtxProjectConfig,
|
||||||
serializeKtxProjectConfig,
|
serializeKtxProjectConfig,
|
||||||
validateKtxProjectConfig,
|
validateKtxProjectConfig,
|
||||||
|
|
@ -485,3 +486,49 @@ scan:
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('generateKtxProjectConfigJsonSchema', () => {
|
||||||
|
const schema = generateKtxProjectConfigJsonSchema();
|
||||||
|
|
||||||
|
it('emits draft-07 metadata', () => {
|
||||||
|
expect(schema.$schema).toBe('http://json-schema.org/draft-07/schema#');
|
||||||
|
expect(schema.$id).toBe('https://ktx.dev/schemas/ktx-project-config.json');
|
||||||
|
expect(schema.title).toBe('ktx.yaml');
|
||||||
|
expect(schema.type).toBe('object');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes every top-level ktx.yaml section under properties', () => {
|
||||||
|
const properties = schema.properties as Record<string, unknown>;
|
||||||
|
expect(Object.keys(properties).sort()).toEqual(
|
||||||
|
['agent', 'connections', 'ingest', 'llm', 'memory', 'project', 'scan', 'setup', 'storage'].sort(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks "project" as required', () => {
|
||||||
|
expect(schema.required).toEqual(expect.arrayContaining(['project']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('carries .describe() text on top-level fields', () => {
|
||||||
|
const properties = schema.properties as Record<string, { description?: string }>;
|
||||||
|
expect(properties.project?.description).toMatch(/Project identifier/);
|
||||||
|
expect(properties.llm?.description).toMatch(/LLM/);
|
||||||
|
expect(properties.scan?.description).toMatch(/Schema-scan/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('propagates enum values through to nested fields', () => {
|
||||||
|
const llm = (schema.properties as Record<string, { properties?: Record<string, unknown> }>).llm;
|
||||||
|
const provider = llm?.properties?.provider as { properties?: Record<string, unknown> };
|
||||||
|
const backend = provider?.properties?.backend as { enum?: readonly string[] };
|
||||||
|
expect(backend?.enum).toEqual(['none', 'anthropic', 'vertex', 'gateway']);
|
||||||
|
|
||||||
|
const storage = (schema.properties as Record<string, { properties?: Record<string, unknown> }>).storage;
|
||||||
|
const state = storage?.properties?.state as { enum?: readonly string[] };
|
||||||
|
expect(state?.enum).toEqual(['sqlite', 'postgres']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('carries descriptions on deeply nested leaves', () => {
|
||||||
|
const scan = (schema.properties as Record<string, { properties?: Record<string, unknown> }>).scan;
|
||||||
|
const relationships = scan?.properties?.relationships as { properties?: Record<string, { description?: string }> };
|
||||||
|
expect(relationships?.properties?.acceptThreshold?.description).toMatch(/auto-accepted/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -19,137 +19,247 @@ const DEPRECATED_KEY_HINTS: Record<string, string> = {
|
||||||
'scan.enrichment.embeddings.provider': 'use scan.enrichment.embeddings.backend',
|
'scan.enrichment.embeddings.provider': 'use scan.enrichment.embeddings.backend',
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiCredentialsSchema = z.strictObject({
|
const apiCredentialsSchema = z
|
||||||
api_key: z.string().min(1).optional(),
|
.strictObject({
|
||||||
base_url: z.string().min(1).optional(),
|
api_key: z.string().min(1).optional().describe('API key for the provider. Read from this value or the provider-specific environment variable.'),
|
||||||
});
|
base_url: z.string().min(1).optional().describe('Override the provider\'s default API base URL (e.g. a proxy or self-hosted gateway).'),
|
||||||
|
})
|
||||||
|
.describe('API credentials block: optional key and base URL for an LLM or embedding provider.');
|
||||||
|
|
||||||
const vertexProviderSchema = z.strictObject({
|
const vertexProviderSchema = z
|
||||||
project: z.string().min(1).optional(),
|
.strictObject({
|
||||||
location: z.string().default(''),
|
project: z.string().min(1).optional().describe('Google Cloud project ID hosting the Vertex AI endpoint.'),
|
||||||
});
|
location: z.string().default('').describe('Vertex AI region (e.g. "us-east5"). Empty string falls back to the SDK default.'),
|
||||||
|
})
|
||||||
|
.describe('Google Vertex AI provider configuration.');
|
||||||
|
|
||||||
const sentenceTransformersSchema = z.strictObject({
|
const sentenceTransformersSchema = z
|
||||||
base_url: z.string().default(''),
|
.strictObject({
|
||||||
pathPrefix: z.string().optional(),
|
base_url: z.string().default('').describe('Base URL of the sentence-transformers HTTP server. Empty string uses the managed local runtime.'),
|
||||||
});
|
pathPrefix: z.string().optional().describe('Optional URL path prefix prepended to embedding requests.'),
|
||||||
|
})
|
||||||
|
.describe('Sentence-transformers embedding server configuration.');
|
||||||
|
|
||||||
const llmProviderSchema = z.strictObject({
|
const llmProviderSchema = z
|
||||||
backend: z.enum(KTX_LLM_BACKENDS).default('none'),
|
.strictObject({
|
||||||
vertex: vertexProviderSchema.optional(),
|
backend: z
|
||||||
anthropic: apiCredentialsSchema.optional(),
|
.enum(KTX_LLM_BACKENDS)
|
||||||
gateway: apiCredentialsSchema.optional(),
|
.default('none')
|
||||||
});
|
.describe('LLM provider backend. "none" disables LLM features; "anthropic" / "vertex" / "gateway" require the matching nested credentials block.'),
|
||||||
|
vertex: vertexProviderSchema.optional().describe('Vertex AI credentials, used when backend is "vertex".'),
|
||||||
|
anthropic: apiCredentialsSchema.optional().describe('Anthropic API credentials, used when backend is "anthropic".'),
|
||||||
|
gateway: apiCredentialsSchema.optional().describe('AI Gateway credentials, used when backend is "gateway".'),
|
||||||
|
})
|
||||||
|
.describe('LLM provider selection and credentials.');
|
||||||
|
|
||||||
const promptCachingSchema = z.strictObject({
|
const promptCachingSchema = z
|
||||||
enabled: z.boolean().optional(),
|
.strictObject({
|
||||||
systemTtl: z.enum(KTX_PROMPT_CACHE_TTLS).optional(),
|
enabled: z.boolean().optional().describe('Master switch for Anthropic-style prompt caching. When omitted, the backend\'s default applies.'),
|
||||||
toolsTtl: z.enum(KTX_PROMPT_CACHE_TTLS).optional(),
|
systemTtl: z.enum(KTX_PROMPT_CACHE_TTLS).optional().describe('Cache TTL for the system prompt segment ("5m" or "1h").'),
|
||||||
historyTtl: z.enum(KTX_PROMPT_CACHE_TTLS).optional(),
|
toolsTtl: z.enum(KTX_PROMPT_CACHE_TTLS).optional().describe('Cache TTL for the tools/schema segment ("5m" or "1h").'),
|
||||||
vertexFallbackTo5m: z.boolean().optional(),
|
historyTtl: z.enum(KTX_PROMPT_CACHE_TTLS).optional().describe('Cache TTL for conversation-history cache breakpoints ("5m" or "1h").'),
|
||||||
});
|
vertexFallbackTo5m: z.boolean().optional().describe('When true, transparently downgrade 1h TTLs to 5m on Vertex, which does not support 1h caching.'),
|
||||||
|
})
|
||||||
|
.describe('Prompt-caching tunables for Anthropic-compatible providers.');
|
||||||
|
|
||||||
const llmSchema = z.strictObject({
|
const llmSchema = z
|
||||||
provider: llmProviderSchema.prefault({}),
|
.strictObject({
|
||||||
models: z.partialRecord(z.enum(KTX_MODEL_ROLES), z.string().min(1)).default({}),
|
provider: llmProviderSchema.prefault({}).describe('LLM provider backend and credentials.'),
|
||||||
promptCaching: promptCachingSchema.optional(),
|
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.'),
|
||||||
|
promptCaching: promptCachingSchema.optional().describe('Optional prompt-caching tunables.'),
|
||||||
|
})
|
||||||
|
.describe('LLM provider, per-role model overrides, and prompt-caching tunables.');
|
||||||
|
|
||||||
const embeddingSchema = z.strictObject({
|
const embeddingSchema = z
|
||||||
backend: z.enum(KTX_EMBEDDING_BACKENDS).default('deterministic'),
|
.strictObject({
|
||||||
model: z.string().min(1).optional(),
|
backend: z
|
||||||
dimensions: z.int().positive().default(8),
|
.enum(KTX_EMBEDDING_BACKENDS)
|
||||||
openai: apiCredentialsSchema.optional(),
|
.default('deterministic')
|
||||||
sentenceTransformers: sentenceTransformersSchema.optional(),
|
.describe('Embedding backend. "deterministic" is a built-in hash-based vector for offline use; "openai" and "sentence-transformers" call out to those providers; "none" disables embeddings.'),
|
||||||
batchSize: z.int().positive().optional(),
|
model: z.string().min(1).optional().describe('Provider-specific embedding model identifier (e.g. "text-embedding-3-small"). Ignored by the "deterministic" backend.'),
|
||||||
});
|
dimensions: z.int().positive().default(8).describe('Embedding vector dimensionality. Must match the chosen model when using a real provider.'),
|
||||||
|
openai: apiCredentialsSchema.optional().describe('OpenAI credentials, used when backend is "openai".'),
|
||||||
|
sentenceTransformers: sentenceTransformersSchema.optional().describe('Sentence-transformers server config, used when backend is "sentence-transformers".'),
|
||||||
|
batchSize: z.int().positive().optional().describe('Number of texts per embedding API call. Omit to use the backend default.'),
|
||||||
|
})
|
||||||
|
.describe('Embedding backend, model, and provider credentials.');
|
||||||
|
|
||||||
const workUnitsSchema = z.strictObject({
|
const workUnitsSchema = z
|
||||||
stepBudget: z.int().positive().default(40),
|
.strictObject({
|
||||||
maxConcurrency: z.int().positive().default(1),
|
stepBudget: z.int().positive().default(40).describe('Maximum number of agent steps allowed per work unit before it is force-terminated.'),
|
||||||
failureMode: z.enum(KTX_WORK_UNIT_FAILURE_MODES).default('continue'),
|
maxConcurrency: z.int().positive().default(1).describe('Maximum number of work units run concurrently during ingest.'),
|
||||||
});
|
failureMode: z
|
||||||
|
.enum(KTX_WORK_UNIT_FAILURE_MODES)
|
||||||
|
.default('continue')
|
||||||
|
.describe('Behavior when a work unit fails: "abort" stops the whole ingest run; "continue" records the failure and keeps going.'),
|
||||||
|
})
|
||||||
|
.describe('Concurrency and failure handling for ingest work units.');
|
||||||
|
|
||||||
const ingestSchema = z.strictObject({
|
const ingestSchema = z
|
||||||
adapters: z.array(z.string().min(1)).default([]),
|
.strictObject({
|
||||||
embeddings: embeddingSchema.prefault({ backend: 'deterministic', model: 'deterministic' }),
|
adapters: z
|
||||||
workUnits: workUnitsSchema.prefault({}),
|
.array(z.string().min(1))
|
||||||
});
|
.default([])
|
||||||
|
.describe('Ingest adapter identifiers to run (e.g. "metabase", "looker", "historic-sql"). Empty array means no adapters are run.'),
|
||||||
|
embeddings: embeddingSchema
|
||||||
|
.prefault({ backend: 'deterministic', model: 'deterministic' })
|
||||||
|
.describe('Embedding configuration used when ingest adapters need to embed documents.'),
|
||||||
|
workUnits: workUnitsSchema.prefault({}).describe('Concurrency and failure handling for ingest work units.'),
|
||||||
|
})
|
||||||
|
.describe('Ingest pipeline configuration: adapters, embeddings, and work-unit policy.');
|
||||||
|
|
||||||
const scanEnrichmentSchema = z.strictObject({
|
const scanEnrichmentSchema = z
|
||||||
mode: z.enum(KTX_ENRICHMENT_MODES).default('none'),
|
.strictObject({
|
||||||
embeddings: embeddingSchema.optional(),
|
mode: z
|
||||||
});
|
.enum(KTX_ENRICHMENT_MODES)
|
||||||
|
.default('none')
|
||||||
|
.describe('Column/table enrichment mode. "none" disables enrichment; "deterministic" uses local heuristics; "llm" calls the configured LLM provider.'),
|
||||||
|
embeddings: embeddingSchema.optional().describe('Optional embedding override for enrichment-time vectorization. Falls back to ingest.embeddings when omitted.'),
|
||||||
|
})
|
||||||
|
.describe('Schema-scan enrichment: how columns and tables are described.');
|
||||||
|
|
||||||
const scanRelationshipsSchema = z.strictObject({
|
const scanRelationshipsSchema = z
|
||||||
enabled: z.boolean().default(true),
|
.strictObject({
|
||||||
llmProposals: z.boolean().default(true),
|
enabled: z.boolean().default(true).describe('Master switch for relationship discovery during scan.'),
|
||||||
validationRequiredForManifest: z.boolean().default(true),
|
llmProposals: z.boolean().default(true).describe('When true, propose relationships using the configured LLM in addition to deterministic candidates.'),
|
||||||
acceptThreshold: z.number().min(0).max(1).default(0.85),
|
validationRequiredForManifest: z
|
||||||
reviewThreshold: z.number().min(0).max(1).default(0.55),
|
.boolean()
|
||||||
maxLlmTablesPerBatch: z.int().positive().default(40),
|
.default(true)
|
||||||
maxCandidatesPerColumn: z.int().positive().default(25),
|
.describe('When true, only relationships that pass database-side validation are written to the manifest.'),
|
||||||
profileSampleRows: z.int().positive().default(10000),
|
acceptThreshold: z
|
||||||
validationConcurrency: z.int().positive().default(4),
|
.number()
|
||||||
validationBudget: z.union([z.literal('all'), z.int().nonnegative()]).optional(),
|
.min(0)
|
||||||
});
|
.max(1)
|
||||||
|
.default(0.85)
|
||||||
|
.describe('Confidence score (0–1) at or above which an LLM-proposed relationship is auto-accepted into the manifest.'),
|
||||||
|
reviewThreshold: z
|
||||||
|
.number()
|
||||||
|
.min(0)
|
||||||
|
.max(1)
|
||||||
|
.default(0.55)
|
||||||
|
.describe('Confidence score (0–1) at or above which a proposal is surfaced for human review (but not auto-accepted).'),
|
||||||
|
maxLlmTablesPerBatch: z
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.default(40)
|
||||||
|
.describe('Maximum number of tables included in a single LLM relationship-proposal batch.'),
|
||||||
|
maxCandidatesPerColumn: z
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.default(25)
|
||||||
|
.describe('Maximum number of candidate join partners considered per column during relationship discovery.'),
|
||||||
|
profileSampleRows: z.int().positive().default(10000).describe('Number of rows sampled per table when profiling values for relationship inference.'),
|
||||||
|
validationConcurrency: z.int().positive().default(4).describe('Number of relationship validation queries run in parallel against the database.'),
|
||||||
|
validationBudget: z
|
||||||
|
.union([z.literal('all'), z.int().nonnegative()])
|
||||||
|
.optional()
|
||||||
|
.describe('Cap on validation queries per scan run. Use "all" for unlimited, an integer for a hard cap, or omit for the runtime default.'),
|
||||||
|
})
|
||||||
|
.describe('Schema-scan relationship discovery and validation tunables.');
|
||||||
|
|
||||||
const scanSchema = z.strictObject({
|
const scanSchema = z
|
||||||
enrichment: scanEnrichmentSchema.prefault({}),
|
.strictObject({
|
||||||
relationships: scanRelationshipsSchema.prefault({}),
|
enrichment: scanEnrichmentSchema.prefault({}).describe('Column/table enrichment configuration.'),
|
||||||
});
|
relationships: scanRelationshipsSchema.prefault({}).describe('Relationship discovery and validation configuration.'),
|
||||||
|
})
|
||||||
|
.describe('Schema-scan configuration: enrichment and relationship discovery.');
|
||||||
|
|
||||||
const setupSchema = z
|
const setupSchema = z
|
||||||
.strictObject({
|
.strictObject({
|
||||||
database_connection_ids: z.array(z.string().min(1)).default([]),
|
database_connection_ids: z
|
||||||
completed_steps: z.unknown().optional(),
|
.array(z.string().min(1))
|
||||||
|
.default([])
|
||||||
|
.describe('Connection IDs (keys of the top-level `connections` map) that the setup wizard treats as the project\'s primary databases.'),
|
||||||
|
completed_steps: z
|
||||||
|
.unknown()
|
||||||
|
.optional()
|
||||||
|
.describe('Deprecated. Accepted for backward compatibility but ignored; KTX no longer tracks setup progress here.'),
|
||||||
})
|
})
|
||||||
.transform(({ database_connection_ids }) => ({ database_connection_ids }));
|
.transform(({ database_connection_ids }) => ({ database_connection_ids }))
|
||||||
|
.describe('Setup-wizard state captured during `ktx setup`.');
|
||||||
|
|
||||||
const storageGitSchema = z.strictObject({
|
const storageGitSchema = z
|
||||||
auto_commit: z.boolean().default(true),
|
.strictObject({
|
||||||
author: z.string().min(1).default('ktx <ktx@example.com>'),
|
auto_commit: z.boolean().default(true).describe('When true, KTX automatically commits state changes to the local Git-backed store.'),
|
||||||
});
|
author: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.default('ktx <ktx@example.com>')
|
||||||
|
.describe('Git author identity used for auto-commits, in standard "Name <email>" form.'),
|
||||||
|
})
|
||||||
|
.describe('Git-backed storage commit policy.');
|
||||||
|
|
||||||
const storageSchema = z.strictObject({
|
const storageSchema = z
|
||||||
state: z.enum(KTX_STORAGE_STATES).default('sqlite'),
|
.strictObject({
|
||||||
search: z.enum(KTX_SEARCH_BACKENDS).default('sqlite-fts5'),
|
state: z
|
||||||
git: storageGitSchema.prefault({}),
|
.enum(KTX_STORAGE_STATES)
|
||||||
});
|
.default('sqlite')
|
||||||
|
.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.');
|
||||||
|
|
||||||
const connectionSchema = z.looseObject({
|
const connectionSchema = z
|
||||||
driver: z.string().min(1).optional(),
|
.looseObject({
|
||||||
url: z.string().optional(),
|
driver: z.string().min(1).optional().describe('Connector driver identifier (e.g. "postgres", "bigquery", "snowflake").'),
|
||||||
});
|
url: z.string().optional().describe('Connection URL or DSN. Format depends on the driver; may contain environment-variable references.'),
|
||||||
|
})
|
||||||
|
.describe('A single database/connector connection entry. Additional driver-specific fields are accepted and passed through.');
|
||||||
|
|
||||||
const agentSchema = z.strictObject({
|
const agentSchema = z
|
||||||
run_research: z
|
.strictObject({
|
||||||
.strictObject({
|
run_research: z
|
||||||
enabled: z.boolean().default(false),
|
.strictObject({
|
||||||
max_iterations: z.number().int().nonnegative().default(20),
|
enabled: z.boolean().default(false).describe('Master switch for the research agent.'),
|
||||||
default_toolset: z.array(z.string().min(1)).default(['sl_query', 'wiki_search', 'sl_read_source']),
|
max_iterations: z
|
||||||
})
|
.number()
|
||||||
.prefault({}),
|
.int()
|
||||||
});
|
.nonnegative()
|
||||||
|
.default(20)
|
||||||
|
.describe('Maximum number of tool-call iterations the research agent may take per run.'),
|
||||||
|
default_toolset: z
|
||||||
|
.array(z.string().min(1))
|
||||||
|
.default(['sl_query', 'wiki_search', 'sl_read_source'])
|
||||||
|
.describe('Default list of tool identifiers exposed to the research agent.'),
|
||||||
|
})
|
||||||
|
.prefault({})
|
||||||
|
.describe('Research-agent configuration.'),
|
||||||
|
})
|
||||||
|
.describe('Agent feature configuration.');
|
||||||
|
|
||||||
const memorySchema = z.strictObject({
|
const memorySchema = z
|
||||||
auto_commit: z.boolean().default(true),
|
.strictObject({
|
||||||
});
|
auto_commit: z.boolean().default(true).describe('When true, KTX automatically commits memory updates to the Git-backed store.'),
|
||||||
|
})
|
||||||
|
.describe('Memory subsystem configuration.');
|
||||||
|
|
||||||
const ktxProjectConfigSchema = z.strictObject({
|
const ktxProjectConfigSchema = z
|
||||||
project: z
|
.strictObject({
|
||||||
.string({ error: 'ktx.yaml field "project" is required' })
|
project: z
|
||||||
.trim()
|
.string({ error: 'ktx.yaml field "project" is required' })
|
||||||
.min(1, 'ktx.yaml field "project" is required'),
|
.trim()
|
||||||
setup: setupSchema.optional(),
|
.min(1, 'ktx.yaml field "project" is required')
|
||||||
connections: z.record(z.string(), connectionSchema).default({}),
|
.describe('Project identifier; used in logs, ktx state files, and as the default workspace name.'),
|
||||||
storage: storageSchema.prefault({}),
|
setup: setupSchema.optional().describe('Setup-wizard state. Written by `ktx setup`; may be omitted.'),
|
||||||
llm: llmSchema.prefault({}),
|
connections: z
|
||||||
ingest: ingestSchema.prefault({}),
|
.record(z.string(), connectionSchema)
|
||||||
agent: agentSchema.prefault({}),
|
.default({})
|
||||||
memory: memorySchema.prefault({}),
|
.describe('Map of connection ID to connector configuration. Keys are user-chosen names referenced elsewhere in the config.'),
|
||||||
scan: scanSchema.prefault({}),
|
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.'),
|
||||||
|
memory: memorySchema.prefault({}).describe('Memory subsystem configuration.'),
|
||||||
|
scan: scanSchema.prefault({}).describe('Schema-scan configuration: enrichment and relationship discovery.'),
|
||||||
|
})
|
||||||
|
.describe('Configuration schema for KTX project files (ktx.yaml).');
|
||||||
|
|
||||||
export type KtxProjectConfig = z.infer<typeof ktxProjectConfigSchema>;
|
export type KtxProjectConfig = z.infer<typeof ktxProjectConfigSchema>;
|
||||||
export type KtxProjectLlmConfig = z.infer<typeof llmSchema>;
|
export type KtxProjectLlmConfig = z.infer<typeof llmSchema>;
|
||||||
|
|
@ -260,6 +370,19 @@ export function validateKtxProjectConfig(raw: string): KtxConfigValidation {
|
||||||
return { ok: false, issues: collectIssues(result.error, parsed) };
|
return { ok: false, issues: collectIssues(result.error, parsed) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateKtxProjectConfigJsonSchema(): Record<string, unknown> {
|
||||||
|
const schema = z.toJSONSchema(ktxProjectConfigSchema, {
|
||||||
|
target: 'draft-7',
|
||||||
|
io: 'input',
|
||||||
|
}) as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||||
|
$id: 'https://ktx.dev/schemas/ktx-project-config.json',
|
||||||
|
title: 'ktx.yaml',
|
||||||
|
...schema,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function serializeKtxProjectConfig(config: KtxProjectConfig): string {
|
export function serializeKtxProjectConfig(config: KtxProjectConfig): string {
|
||||||
const serializedConfig =
|
const serializedConfig =
|
||||||
config.ingest.adapters.length === 0
|
config.ingest.adapters.length === 0
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export type {
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
export {
|
export {
|
||||||
buildDefaultKtxProjectConfig,
|
buildDefaultKtxProjectConfig,
|
||||||
|
generateKtxProjectConfigJsonSchema,
|
||||||
parseKtxProjectConfig,
|
parseKtxProjectConfig,
|
||||||
serializeKtxProjectConfig,
|
serializeKtxProjectConfig,
|
||||||
validateKtxProjectConfig,
|
validateKtxProjectConfig,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue