feat(docs): visualize KTX ingestion with ReactFlow diagram

Reframe the introduction around the two user-facing ingestion outputs (wiki
and executable semantic layer) and replace the static product-mechanics card
flow with a ReactFlow diagram: sources fan into a sequential ingest pipeline,
which forks into wiki and semantic-layer outputs connected by a bidirectional
"references" edge. Drop the .ktx/raw-sources internal-implementation rows from
the intro table and update the content test to guard the new copy.
This commit is contained in:
Andrey Avtomonov 2026-05-18 16:18:01 +02:00
parent e64da5a85d
commit 4421fe1c12
5 changed files with 661 additions and 457 deletions

View file

@ -1,114 +1,348 @@
import type { ReactNode } from "react";
"use client";
type SourceInput = {
name: string;
sources: string[];
detail: string;
signal: string;
import {
Background,
BackgroundVariant,
Handle,
MarkerType,
type Node,
type NodeProps,
Position,
ReactFlow,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
type SourceNodeData = {
accent: string;
body: string;
items: string[];
title: string;
};
const sourceInputs = [
{
name: "Database structure",
sources: [
"Postgres",
"Snowflake",
"BigQuery",
"and many others",
],
detail: "tables, columns, types, constraints, row counts",
signal: "grounds definitions in live database structure",
accent: "border-fd-primary",
},
{
name: "BI and usage evidence",
sources: ["Metabase", "Looker"],
detail: "historic SQL, questions, dashboards, usage patterns",
signal: "extracts joins, filters, grain, and trusted examples",
accent: "border-orange-500",
},
{
name: "Semantic modeling",
sources: ["dbt", "MetricFlow", "LookML"],
detail: "models, metrics, dimensions, explores, joins",
signal: "maps existing modeling logic into semantic entities",
accent: "border-amber-500",
},
{
name: "Company documentation",
sources: ["Notion"],
detail: "Notion pages, policies, caveats",
signal: "links business language back to semantic references",
accent: "border-slate-500 dark:border-cyan-200",
},
] satisfies SourceInput[];
type StageNodeData = {
body: string;
index: number;
title: string;
};
const ingestSteps = [
type OutputNodeData = {
accent: string;
body: string;
path: string;
tags: string[];
title: string;
};
type SourceNode = Node<SourceNodeData, "source">;
type StageNode = Node<StageNodeData, "stage">;
type OutputNode = Node<OutputNodeData, "output">;
type FlowNode = SourceNode | StageNode | OutputNode;
const SOURCE_W = 210;
const SOURCE_H = 200;
const STAGE_W = 280;
const STAGE_H = 120;
const OUTPUT_W = 340;
const OUTPUT_H = 232;
const ROW_SOURCES_Y = 80;
const ROW_STAGE_START_Y = 360;
const STAGE_GAP = 30;
const ROW_OUTPUTS_Y = 1000;
const STAGE_CENTER_X = 460;
const STAGE_X = STAGE_CENTER_X - STAGE_W / 2;
const SOURCE_GAP_X = 24;
const SOURCES_TOTAL = SOURCE_W * 4 + SOURCE_GAP_X * 3;
const SOURCES_START_X = STAGE_CENTER_X - SOURCES_TOTAL / 2;
const OUTPUT_GAP_X = 180;
const OUTPUTS_TOTAL = OUTPUT_W * 2 + OUTPUT_GAP_X;
const OUTPUTS_START_X = STAGE_CENTER_X - OUTPUTS_TOTAL / 2;
const EDGE_STROKE = "#94a3b8";
const sourceData: SourceNodeData[] = [
{
title: "extract evidence",
body: "Pull structured facts from schemas, SQL, BI metadata, and docs.",
title: "Databases",
body: "Schemas, columns, keys, row counts, and query history.",
items: ["PostgreSQL", "Snowflake", "BigQuery", "SQLite"],
accent: "#3b82f6",
},
{
title: "reconcile entities",
body: "Merge names, measures, joins, and caveats into one project model.",
title: "BI tools",
body: "Dashboards, questions, explores, usage, and trusted examples.",
items: ["Metabase", "Looker"],
accent: "#f97316",
},
{
title: "validate references",
body: "Check semantic fields and joins against database context before agents use them.",
title: "Modeling code",
body: "Existing metrics, dimensions, models, joins, and entities.",
items: ["dbt", "LookML", "MetricFlow"],
accent: "#f59e0b",
},
{
title: "Docs and notes",
body: "Policies, caveats, team definitions, and analyst context.",
items: ["Notion", "Markdown"],
accent: "#10b981",
},
];
const artifacts = [
const stageData: Omit<StageNodeData, "index">[] = [
{
path: "semantic-layer/*.yaml",
title: "Typed query model",
body: "sources, grain, joins, dimensions, measures, filters, segments",
title: "Source adapters",
body: "Read each configured system in its native shape.",
},
{
title: "Context builder",
body: "Turn source evidence into proposed context updates.",
},
{
title: "Reconciliation",
body: "Merge new evidence with the context that already exists.",
},
{
title: "Validation",
body: "Check references and semantics before agents rely on them.",
},
];
const outputData: OutputNodeData[] = [
{
title: "Wiki",
path: "wiki/*.md",
title: "Business context",
body: "rules and caveats with sl_refs back to semantic-layer entities",
tags: ["free-form", "auto-maintained"],
body: "Definitions, caveats, policies, analyst notes, and business language that agents can search.",
accent: "#10b981",
},
{
path: "raw-sources/",
title: "Evidence trail",
body: "scan artifacts, extracted metadata, relationship evidence",
},
{
path: ".ktx/",
title: "Local indexes",
body: "embeddings and search indexes, not the source of truth",
title: "Semantic layer",
path: "semantic-layer/*.yaml",
tags: ["structured", "executable", "auto-maintained"],
body: "Metrics, joins, tables, dimensions, filters, and segments that KTX can validate and compile into SQL.",
accent: "#3b82f6",
},
];
const runtimeSteps = [
{
title: "Search wiki",
body: "Find business rules, caveats, synonyms, and sl_refs.",
},
{
title: "Resolve semantic refs",
body: "Map measure and dimension names to approved entities.",
},
{
title: "Validate fields",
body: "Check source, columns, joins, grain, filters, and segments.",
},
{
title: "Build query plan",
body: "Create a semantic query plan before SQL is generated.",
},
{
title: "Compile dialect SQL",
body: "Generate warehouse-shaped SQL instead of copying examples.",
},
{
title: "Execute with bounds",
body: "Optionally run with bounded rows and return provenance.",
},
const nodes: FlowNode[] = [
...sourceData.map<SourceNode>((source, index) => ({
id: `source-${index}`,
type: "source",
position: {
x: SOURCES_START_X + index * (SOURCE_W + SOURCE_GAP_X),
y: ROW_SOURCES_Y,
},
data: source,
draggable: false,
selectable: false,
})),
...stageData.map<StageNode>((stage, index) => ({
id: `stage-${index}`,
type: "stage",
position: {
x: STAGE_X,
y: ROW_STAGE_START_Y + index * (STAGE_H + STAGE_GAP),
},
data: { ...stage, index: index + 1 },
draggable: false,
selectable: false,
})),
...outputData.map<OutputNode>((output, index) => ({
id: `output-${index}`,
type: "output",
position: {
x: OUTPUTS_START_X + index * (OUTPUT_W + OUTPUT_GAP_X),
y: ROW_OUTPUTS_Y,
},
data: output,
draggable: false,
selectable: false,
})),
];
const REF_EDGE_STROKE = "#64748b";
const flowEdges = [
...sourceData.map((_, index) => ({
id: `e-source-${index}-stage-0`,
source: `source-${index}`,
target: "stage-0",
})),
...stageData.slice(0, -1).map((_, index) => ({
id: `e-stage-${index}-stage-${index + 1}`,
source: `stage-${index}`,
target: `stage-${index + 1}`,
})),
...outputData.map((_, index) => ({
id: `e-stage-3-output-${index}`,
source: "stage-3",
target: `output-${index}`,
})),
].map((edge) => ({
...edge,
type: "smoothstep" as const,
style: { stroke: EDGE_STROKE, strokeWidth: 1.5 },
markerEnd: {
type: MarkerType.ArrowClosed,
color: EDGE_STROKE,
width: 16,
height: 16,
},
}));
const refsEdge = {
id: "e-output-refs",
source: "output-0",
sourceHandle: "right",
target: "output-1",
targetHandle: "left",
type: "straight" as const,
label: "references",
labelBgPadding: [6, 3] as [number, number],
labelBgBorderRadius: 4,
labelStyle: {
fontSize: 13,
fontWeight: 500,
fill: "var(--color-fd-muted-foreground)",
},
labelBgStyle: {
fill: "var(--color-fd-background)",
stroke: "var(--color-fd-border)",
strokeWidth: 1,
},
style: {
stroke: REF_EDGE_STROKE,
strokeWidth: 1.25,
strokeDasharray: "4 4",
},
markerStart: {
type: MarkerType.ArrowClosed,
color: REF_EDGE_STROKE,
width: 14,
height: 14,
},
markerEnd: {
type: MarkerType.ArrowClosed,
color: REF_EDGE_STROKE,
width: 14,
height: 14,
},
};
const edges = [...flowEdges, refsEdge];
function SourceNodeView({ data }: NodeProps<SourceNode>) {
return (
<div
style={{
width: SOURCE_W,
height: SOURCE_H,
borderTop: `3px solid ${data.accent}`,
}}
className="overflow-hidden rounded-md border border-fd-border bg-fd-card px-3.5 py-3 shadow-sm"
>
<Handle type="target" position={Position.Top} className="!opacity-0" />
<p className="text-[16px] font-semibold leading-6 text-fd-foreground">
{data.title}
</p>
<p className="mt-1 line-clamp-3 text-[13px] leading-5 text-fd-muted-foreground">
{data.body}
</p>
<div className="mt-2 flex flex-wrap gap-1.5">
{data.items.map((item) => (
<span
key={item}
className="rounded border border-fd-border bg-fd-background px-1.5 py-0.5 text-[12px] leading-5 text-fd-muted-foreground"
>
{item}
</span>
))}
</div>
<Handle type="source" position={Position.Bottom} className="!opacity-0" />
</div>
);
}
function StageNodeView({ data }: NodeProps<StageNode>) {
return (
<div
style={{ width: STAGE_W, height: STAGE_H }}
className="flex items-center gap-3.5 rounded-md border border-cyan-200/20 bg-[#0f1f23] px-4 py-3.5 text-white shadow-sm dark:bg-[#0b181b]"
>
<Handle type="target" position={Position.Top} className="!opacity-0" />
<span className="flex h-8 w-8 flex-none items-center justify-center rounded-full bg-cyan-300/95 font-mono text-sm font-semibold text-[#0b1c20]">
{data.index}
</span>
<div className="min-w-0">
<p className="text-[16px] font-semibold leading-6 text-white">
{data.title}
</p>
<p className="mt-1 line-clamp-3 text-[13px] leading-5 text-cyan-50/75">
{data.body}
</p>
</div>
<Handle type="source" position={Position.Bottom} className="!opacity-0" />
</div>
);
}
function OutputNodeView({ data }: NodeProps<OutputNode>) {
return (
<div
style={{
width: OUTPUT_W,
height: OUTPUT_H,
borderTop: `3px solid ${data.accent}`,
}}
className="overflow-hidden rounded-md border border-fd-border bg-fd-card px-4 py-3.5 shadow-sm"
>
<Handle type="target" position={Position.Top} className="!opacity-0" />
<Handle
id="left"
type="target"
position={Position.Left}
className="!opacity-0"
/>
<Handle
id="right"
type="source"
position={Position.Right}
className="!opacity-0"
/>
<p
className="font-mono text-[13px] font-semibold tracking-tight"
style={{ color: data.accent }}
>
{data.path}
</p>
<p className="mt-1.5 text-[16px] font-semibold leading-6 text-fd-foreground">
{data.title}
</p>
<div className="mt-1.5 flex flex-nowrap gap-1">
{data.tags.map((tag) => (
<span
key={tag}
className="whitespace-nowrap rounded border border-fd-border bg-fd-background px-1.5 py-0.5 text-[12px] leading-5 text-fd-muted-foreground"
>
{tag}
</span>
))}
</div>
<p className="mt-2 line-clamp-3 text-[13px] leading-5 text-fd-muted-foreground">
{data.body}
</p>
</div>
);
}
const nodeTypes = {
source: SourceNodeView,
stage: StageNodeView,
output: OutputNodeView,
};
export function ProductMechanics() {
return (
<section
@ -121,339 +355,111 @@ export function ProductMechanics() {
className="text-xl font-semibold tracking-normal text-fd-foreground sm:text-2xl"
style={{ fontFamily: "var(--font-display)" }}
>
How KTX works
How ingestion works
</h2>
<p className="mt-3 text-sm leading-6 text-fd-muted-foreground">
KTX reads source evidence, writes local context files, and gives
agents semantic search, validation, SQL, and provenance.
KTX ingests source evidence, reconciles it with your existing project,
and produces durable context that agents can search, review, and
execute.
</p>
</div>
<div className="space-y-4">
<IngestionDiagram />
<RuntimeDiagram />
</div>
<article
className="max-w-full min-w-0 overflow-hidden rounded-lg border border-fd-border bg-fd-card shadow-sm"
aria-label="KTX ingestion flow from source systems to durable context outputs"
>
<div className="border-b border-fd-border bg-fd-muted/35 px-5 py-4">
<p className="text-xs font-semibold uppercase tracking-wide text-fd-primary">
Ingestion flow
</p>
<h3
className="mt-1 text-base font-semibold tracking-normal text-fd-foreground sm:text-lg"
style={{ fontFamily: "var(--font-display)" }}
>
From scattered source systems to agent-ready context
</h3>
<p className="mt-2 max-w-3xl text-xs leading-5 text-fd-muted-foreground">
The inputs can be structured systems or loose team knowledge. The
outputs are the two files agents need: a readable wiki and an
executable semantic layer.
</p>
</div>
<div
className="mechanics-canvas bg-fd-background"
style={{
height: "min(1180px, 165vw)",
minHeight: 680,
}}
>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
fitView
fitViewOptions={{ padding: 0.04 }}
nodesDraggable={false}
nodesConnectable={false}
nodesFocusable={false}
edgesFocusable={false}
elementsSelectable={false}
panOnDrag={false}
panOnScroll={false}
zoomOnScroll={false}
zoomOnPinch={false}
zoomOnDoubleClick={false}
preventScrolling={false}
minZoom={0.2}
maxZoom={1.5}
proOptions={{ hideAttribution: true }}
>
<Background
variant={BackgroundVariant.Dots}
gap={18}
size={1}
color="var(--color-fd-border)"
/>
</ReactFlow>
</div>
</article>
<style>{`
.mechanics-canvas .react-flow__node {
background: transparent;
border: 0;
box-shadow: none;
padding: 0;
border-radius: 0;
width: auto;
text-align: left;
user-select: text;
-webkit-user-select: text;
cursor: auto;
pointer-events: all !important;
}
.mechanics-canvas .react-flow__node > * {
pointer-events: auto;
user-select: text;
-webkit-user-select: text;
}
.mechanics-canvas .react-flow__node.selected,
.mechanics-canvas .react-flow__node:focus,
.mechanics-canvas .react-flow__node:focus-visible {
outline: none;
box-shadow: none;
}
.mechanics-canvas .react-flow__pane {
cursor: default;
}
.mechanics-canvas .react-flow__handle {
width: 1px;
height: 1px;
min-width: 0;
min-height: 0;
background: transparent;
border: 0;
pointer-events: none;
}
`}</style>
</section>
);
}
function IngestionDiagram() {
return (
<article
className="max-w-full min-w-0 overflow-hidden rounded-lg border border-fd-border bg-fd-card shadow-sm"
aria-labelledby="ingestion-diagram-title"
>
<DiagramHeader
eyebrow="Ingestion"
id="ingestion-diagram-title"
title="Build context from source evidence"
body="KTX reconciles loose metadata, SQL, BI usage, and documentation into files agents can validate and edit."
/>
<div className="grid gap-0 lg:grid-cols-[minmax(0,0.94fr)_minmax(0,1.06fr)]">
<section className="flex flex-col border-b border-fd-border p-4 lg:border-r lg:border-b-0">
<ColumnLabel>Inputs KTX reads</ColumnLabel>
<div className="grid flex-1 auto-rows-fr gap-2">
{sourceInputs.map((source) => (
<div
key={source.name}
className={`grid min-h-0 gap-2 border-l-2 bg-fd-background px-3 py-2 sm:grid-cols-[minmax(0,1fr)_minmax(6.5rem,0.42fr)] sm:items-center ${source.accent}`}
>
<div className="min-w-0">
<p className="text-sm font-semibold text-fd-foreground">
{source.name}
</p>
<p className="mt-0.5 text-xs leading-4 text-fd-muted-foreground">
{source.detail}
</p>
<p className="mt-1 text-xs leading-4 text-fd-primary">
{source.signal}
</p>
</div>
<SourceList sources={source.sources} />
</div>
))}
</div>
</section>
<section className="flex flex-col bg-fd-muted/35 p-4">
<ColumnLabel>KTX transforms evidence</ColumnLabel>
<div className="flex flex-col gap-3">
<div className="rounded-md border border-fd-border bg-[#102226] p-3 text-white dark:bg-[#0b181b]">
<p className="mb-3 text-[11px] font-semibold uppercase tracking-wide text-cyan-200">
KTX builds the model
</p>
<ol className="grid gap-3">
{ingestSteps.map((step, index) => (
<PipelineStep
key={step.title}
index={index + 1}
title={step.title}
body={step.body}
dark
/>
))}
</ol>
</div>
<div className="border-t border-fd-border/80 pt-3">
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-fd-muted-foreground">
Outputs KTX writes
</p>
<div className="grid gap-2 sm:grid-cols-2">
{artifacts.map((artifact) => (
<Artifact key={artifact.path} {...artifact} />
))}
</div>
</div>
</div>
</section>
</div>
</article>
);
}
function RuntimeDiagram() {
return (
<article
className="max-w-full min-w-0 overflow-hidden rounded-lg border border-fd-border bg-fd-card shadow-sm"
aria-labelledby="runtime-diagram-title"
>
<DiagramHeader
eyebrow="Runtime"
id="runtime-diagram-title"
title="Run agent requests through the model"
body="Agents send business intent. KTX resolves fields, checks joins and grain, compiles SQL, and can execute with row limits."
/>
<div className="grid gap-0 lg:grid-cols-[minmax(0,0.82fr)_minmax(0,1.18fr)]">
<section className="border-b border-fd-border p-4 lg:border-r lg:border-b-0">
<ColumnLabel>Agent sends</ColumnLabel>
<CodeBox>
<div>connection: warehouse</div>
<div>measure: orders.total_revenue</div>
<div>dimension: customers.segment</div>
<div>filter: orders.created_date &gt;= '2024-01-01'</div>
</CodeBox>
<p className="mt-3 text-xs leading-5 text-fd-muted-foreground">
This is the API surface agents should use: compact semantic intent,
not hand-written warehouse SQL.
</p>
</section>
<section className="bg-fd-muted/35 p-4">
<ColumnLabel>KTX planning and execution</ColumnLabel>
<ol className="grid gap-2 sm:grid-cols-2">
{runtimeSteps.map((step, index) => (
<PipelineStep
key={step.title}
index={index + 1}
title={step.title}
body={step.body}
/>
))}
</ol>
</section>
</div>
<div className="grid gap-0 border-t border-fd-border lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<section className="border-b border-fd-border p-4 lg:border-r lg:border-b-0">
<ColumnLabel>Semantic query plan</ColumnLabel>
<div className="rounded-md border border-fd-border bg-fd-card p-3 text-xs leading-5 text-fd-muted-foreground">
<p>
<strong className="text-fd-foreground">source:</strong>{" "}
orders joined to customers as many_to_one
</p>
<p>
<strong className="text-fd-foreground">measure:</strong>{" "}
total_revenue = sum(amount) with refund filter
</p>
<p>
<strong className="text-fd-foreground">grain:</strong> segment
group-by with date predicate
</p>
<p>
<strong className="text-fd-foreground">result:</strong> dialect
SQL, bounded rows, and provenance
</p>
</div>
</section>
<section className="p-4">
<ColumnLabel>KTX returns</ColumnLabel>
<CodeBox>
<div>select</div>
<div className="pl-3">customers.segment,</div>
<div className="pl-3">sum(orders.amount) as total_revenue</div>
<div>from analytics.orders</div>
<div>join analytics.customers</div>
<div className="pl-3">on orders.customer_id = customers.id</div>
<div>where orders.status != 'refunded'</div>
<div className="pl-3">and orders.created_date &gt;= '2024-01-01'</div>
<div>group by 1</div>
</CodeBox>
<p className="mt-3 text-xs leading-5 text-fd-muted-foreground">
The output can be SQL-only or executed results with provenance, so
the agent can show where the answer came from.
</p>
</section>
</div>
</article>
);
}
function DiagramHeader({
body,
eyebrow,
id,
title,
}: {
body: string;
eyebrow: string;
id: string;
title: string;
}) {
return (
<div className="border-b border-fd-border bg-fd-muted/35 px-5 py-4">
<p className="text-xs font-semibold uppercase tracking-wide text-fd-primary">
{eyebrow}
</p>
<h3
id={id}
className="mt-1 text-base font-semibold tracking-normal text-fd-foreground sm:text-lg"
style={{ fontFamily: "var(--font-display)" }}
>
{title}
</h3>
<p className="mt-2 max-w-3xl text-xs leading-5 text-fd-muted-foreground">
{body}
</p>
</div>
);
}
function Artifact({
body,
path,
title,
}: {
body: string;
path: string;
title: string;
}) {
return (
<div className="rounded-md border border-fd-border bg-fd-card px-3 py-2">
<p className="font-mono text-xs font-semibold text-fd-foreground">
{path}
</p>
<p className="mt-1 text-sm font-semibold text-fd-foreground">{title}</p>
<p className="mt-0.5 text-xs leading-5 text-fd-muted-foreground">
{body}
</p>
</div>
);
}
function PipelineStep({
body,
dark = false,
index,
title,
}: {
body: string;
dark?: boolean;
index: number;
title: string;
}) {
return (
<li
className={
dark
? "flex gap-3 text-sm"
: "flex gap-3 rounded-md border border-fd-border bg-fd-card px-3 py-2"
}
>
<span
className={
dark
? "flex h-5 w-5 flex-none items-center justify-center rounded-full bg-cyan-200 text-[11px] font-semibold text-[#102226]"
: "flex h-5 w-5 flex-none items-center justify-center rounded-full bg-fd-primary text-[11px] font-semibold text-fd-primary-foreground"
}
>
{index}
</span>
<span className="min-w-0">
<span
className={
dark
? "block text-sm font-semibold text-white"
: "block text-xs font-semibold text-fd-foreground"
}
>
{title}
</span>
<span
className={
dark
? "mt-0.5 block break-words text-xs leading-5 text-cyan-50/75"
: "mt-0.5 block break-words text-xs leading-5 text-fd-muted-foreground"
}
>
{body}
</span>
</span>
</li>
);
}
function ColumnLabel({ children }: { children: ReactNode }) {
return (
<p className="mb-3 text-[11px] font-semibold uppercase tracking-wide text-fd-muted-foreground">
{children}
</p>
);
}
function SourceList({
sources,
}: {
sources: string[];
}) {
return (
<div
className="min-w-0 border-t border-fd-border/70 pt-2 sm:border-t-0 sm:border-l sm:pl-3 sm:pt-0"
aria-label="Sources"
>
<p className="mb-1.5 text-[10px] font-semibold uppercase tracking-wide text-fd-muted-foreground/80">
Sources
</p>
<div className="flex flex-wrap gap-1.5">
{sources.map((source) =>
source === "and many others" ? (
<span
key={source}
className="px-0.5 py-0.5 text-[10px] font-medium leading-4 text-fd-muted-foreground/85"
>
{source}
</span>
) : (
<span
key={source}
className="rounded border border-fd-border bg-fd-card/75 px-1.5 py-0.5 text-[10px] font-medium leading-4 text-fd-muted-foreground shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]"
>
{source}
</span>
),
)}
</div>
</div>
);
}
function CodeBox({ children }: { children: ReactNode }) {
return (
<div className="max-w-full min-w-0 overflow-x-auto rounded-md border border-fd-border bg-[#0c1417] p-3 font-mono text-[11px] leading-5 text-cyan-50 shadow-sm">
<div className="[overflow-wrap:anywhere]">{children}</div>
</div>
);
}

View file

@ -23,7 +23,7 @@ import { ProductMechanics } from "@/components/product-mechanics";
Make analytics context usable by agents
</h1>
<p className="mt-4 max-w-2xl text-lg text-fd-muted-foreground" style={{ lineHeight: '1.7' }}>
{'KTX turns warehouse metadata, semantic definitions, BI usage, and team knowledge into local files and runtime tools that database agents can trust.'}
{'KTX turns warehouse metadata, semantic definitions, BI usage, and team knowledge into a wiki and executable semantic layer that database agents can trust.'}
</p>
</div>
</div>
@ -36,12 +36,13 @@ import { ProductMechanics } from "@/components/product-mechanics";
## What KTX creates
KTX ingestion turns source evidence into durable context files that agents can
search, review, and execute.
| Path | What it gives agents |
|------|----------------------|
| `semantic-layer/` | Measures, dimensions, joins, grain, filters, segments |
| `semantic-layer/` | Executable measures, dimensions, joins, grain, filters, and segments |
| `wiki/` | Business definitions, caveats, policies, analyst notes |
| `raw-sources/` | Extracted metadata, scan output, relationship evidence |
| `.ktx/` | Local indexes, embeddings, setup state, runtime data |
<ProductMechanics />

View file

@ -10,6 +10,7 @@
"test": "node --test tests/*.test.mjs"
},
"dependencies": {
"@xyflow/react": "^12.10.2",
"fumadocs-core": "16.8.10",
"fumadocs-mdx": "15.0.4",
"fumadocs-ui": "16.8.10",
@ -18,11 +19,11 @@
"react-dom": "19.2.6"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^25.7.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^6.0",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4"
"tailwindcss": "^4",
"typescript": "^6.0"
}
}

View file

@ -49,49 +49,44 @@ test("docs introduction frames the concept before showing product mechanics", as
assert.doesNotMatch(heroSource, /The Context Layer/);
assert.doesNotMatch(heroSource, /Building Context/);
assert.doesNotMatch(heroSource, /flex flex-wrap gap-3/);
assert.doesNotMatch(introduction, /raw-sources/);
assert.doesNotMatch(introduction, /\.ktx/);
});
test("product mechanics component covers source-specific context and SQL expansion", async () => {
test("product mechanics component explains ingestion outputs", async () => {
const component = await readDocsFile("components/product-mechanics.tsx");
for (const expectedText of [
"How KTX works",
"Build context from source evidence",
"Run agent requests through the model",
"Ingestion",
"Runtime",
"wiki/",
"semantic-layer/",
"raw-sources/",
".ktx/",
"sl_refs",
"Database structure",
"BI and usage evidence",
"Semantic modeling",
"Company documentation",
"Notion pages",
"Sources",
"KTX transforms evidence",
"KTX builds the model",
"Outputs KTX writes",
"Postgres",
"How ingestion works",
"Ingestion flow",
"From scattered source systems to agent-ready context",
"wiki/*.md",
"semantic-layer/*.yaml",
"Wiki",
"Semantic layer",
"Databases",
"BI tools",
"Modeling code",
"Docs and notes",
"Source adapters",
"Context builder",
"Reconciliation",
"Validation",
"PostgreSQL",
"Snowflake",
"BigQuery",
"and many others",
"Metabase",
"Looker",
"dbt",
"MetricFlow",
"LookML",
"extract evidence",
"reconcile entities",
"validate references",
"semantic query plan",
"dialect SQL",
"bounded rows",
"provenance",
"measure: orders.total_revenue",
"dimension: customers.segment",
"select",
"Notion",
"Markdown",
"compile into SQL",
'"use client"',
"@xyflow/react",
"<ReactFlow",
"smoothstep",
]) {
assert.ok(
component.includes(expectedText),
@ -99,7 +94,27 @@ test("product mechanics component covers source-specific context and SQL expansi
);
}
assert.match(
component,
/nodesDraggable=\{false\}/,
"ReactFlow canvas should disable node dragging",
);
assert.match(
component,
/panOnDrag=\{false\}/,
"ReactFlow canvas should disable panning",
);
assert.match(
component,
/zoomOnScroll=\{false\}/,
"ReactFlow canvas should disable scroll zoom",
);
assert.doesNotMatch(component, /raw-sources/);
assert.doesNotMatch(component, /\.ktx/);
assert.doesNotMatch(component, /Product mechanics/);
assert.doesNotMatch(component, /How KTX works/);
assert.doesNotMatch(component, /Runtime/);
assert.doesNotMatch(component, /A semantic compiler for analytics agents/);
assert.doesNotMatch(component, /KTX does more than retrieve Markdown/);
assert.doesNotMatch(component, /Plain Markdown \+ RAG/);
@ -109,12 +124,9 @@ test("product mechanics component covers source-specific context and SQL expansi
assert.doesNotMatch(component, /KTX works in two moments/);
assert.doesNotMatch(component, /name: "Metabase and query history"/);
assert.doesNotMatch(component, /name: "dbt, MetricFlow, LookML"/);
assert.doesNotMatch(component, /query history/);
assert.doesNotMatch(component, /analyst notes/);
assert.doesNotMatch(component, /ClickHouse/);
assert.doesNotMatch(component, /MySQL/);
assert.doesNotMatch(component, /SQL Server/);
assert.doesNotMatch(component, /SQLite/);
assert.doesNotMatch(
component,
/\/ktx\/brand\/(?:postgresql|snowflake|bigquery|clickhouse|mysql|sqlserver|sqlite|metabase|dbt|looker|notion)\.svg/,

184
pnpm-lock.yaml generated
View file

@ -57,6 +57,9 @@ importers:
docs-site:
dependencies:
'@xyflow/react':
specifier: ^12.10.2
version: 12.10.2(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
fumadocs-core:
specifier: 16.8.10
version: 16.8.10(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.6))(next@16.2.6(@opentelemetry/api@1.9.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3)
@ -2748,6 +2751,24 @@ packages:
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
'@types/d3-color@3.1.3':
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
'@types/d3-drag@3.0.7':
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
'@types/d3-interpolate@3.0.4':
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
'@types/d3-selection@3.0.11':
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
'@types/d3-transition@3.0.9':
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
'@types/d3-zoom@3.0.8':
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
'@types/debug@4.1.13':
resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
@ -2853,6 +2874,15 @@ packages:
'@vitest/utils@4.1.6':
resolution: {integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==}
'@xyflow/react@12.10.2':
resolution: {integrity: sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@xyflow/system@0.0.76':
resolution: {integrity: sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==}
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
@ -3139,6 +3169,9 @@ packages:
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
classcat@5.0.5:
resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
clean-stack@2.2.0:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
engines: {node: '>=6'}
@ -3325,6 +3358,44 @@ packages:
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
d3-dispatch@3.0.1:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'}
d3-drag@3.0.0:
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
engines: {node: '>=12'}
d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
d3-transition@3.0.1:
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
engines: {node: '>=12'}
peerDependencies:
d3-selection: 2 - 3
d3-zoom@3.0.0:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
@ -5937,6 +6008,11 @@ packages:
'@types/react':
optional: true
use-sync-external-store@1.6.0:
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -6158,6 +6234,21 @@ packages:
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
zustand@4.5.7:
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
engines: {node: '>=12.7.0'}
peerDependencies:
'@types/react': '>=16.8'
immer: '>=9.0.6'
react: '>=16.8'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@ -9061,6 +9152,27 @@ snapshots:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
'@types/d3-color@3.1.3': {}
'@types/d3-drag@3.0.7':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-interpolate@3.0.4':
dependencies:
'@types/d3-color': 3.1.3
'@types/d3-selection@3.0.11': {}
'@types/d3-transition@3.0.9':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-zoom@3.0.8':
dependencies:
'@types/d3-interpolate': 3.0.4
'@types/d3-selection': 3.0.11
'@types/debug@4.1.13':
dependencies:
'@types/ms': 2.1.0
@ -9191,6 +9303,29 @@ snapshots:
convert-source-map: 2.0.0
tinyrainbow: 3.1.0
'@xyflow/react@12.10.2(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@xyflow/system': 0.0.76
classcat: 5.0.5
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
zustand: 4.5.7(@types/react@19.2.14)(react@19.2.6)
transitivePeerDependencies:
- '@types/react'
- immer
'@xyflow/system@0.0.76':
dependencies:
'@types/d3-drag': 3.0.7
'@types/d3-interpolate': 3.0.4
'@types/d3-selection': 3.0.11
'@types/d3-transition': 3.0.9
'@types/d3-zoom': 3.0.8
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-zoom: 3.0.0
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
@ -9459,6 +9594,8 @@ snapshots:
dependencies:
clsx: 2.1.1
classcat@5.0.5: {}
clean-stack@2.2.0: {}
clean-stack@5.3.0:
@ -9631,6 +9768,42 @@ snapshots:
csstype@3.2.3: {}
d3-color@3.1.0: {}
d3-dispatch@3.0.1: {}
d3-drag@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-selection: 3.0.0
d3-ease@3.0.1: {}
d3-interpolate@3.0.1:
dependencies:
d3-color: 3.1.0
d3-selection@3.0.0: {}
d3-timer@3.0.1: {}
d3-transition@3.0.1(d3-selection@3.0.0):
dependencies:
d3-color: 3.1.0
d3-dispatch: 3.0.1
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-timer: 3.0.1
d3-zoom@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
data-uri-to-buffer@4.0.1: {}
debug@4.4.3:
@ -12730,6 +12903,10 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
use-sync-external-store@1.6.0(react@19.2.6):
dependencies:
react: 19.2.6
util-deprecate@1.0.2: {}
validate-npm-package-license@3.0.4:
@ -12907,4 +13084,11 @@ snapshots:
zod@4.4.3: {}
zustand@4.5.7(@types/react@19.2.14)(react@19.2.6):
dependencies:
use-sync-external-store: 1.6.0(react@19.2.6)
optionalDependencies:
'@types/react': 19.2.14
react: 19.2.6
zwitch@2.0.4: {}