diff --git a/docs-site/app/api/search/route.ts b/docs-site/app/api/search/route.ts new file mode 100644 index 00000000..d86bfc5b --- /dev/null +++ b/docs-site/app/api/search/route.ts @@ -0,0 +1,4 @@ +import { source } from "@/lib/source"; +import { createFromSource } from "fumadocs-core/search/server"; + +export const { GET } = createFromSource(source); diff --git a/docs-site/app/global.css b/docs-site/app/global.css index bc4ed8a4..e7e2c5b2 100644 --- a/docs-site/app/global.css +++ b/docs-site/app/global.css @@ -69,7 +69,11 @@ --color-fd-muted-foreground: #7a8d96; } -html, body { +/* Keep html overflow at the default `visible` so body's overflow + propagates to the viewport (per CSS Overflow spec). That lets + `react-remove-scroll-bar` lock viewport scroll via body alone while + leaving the sticky sidebar placeholder anchored to the viewport. */ +body { overflow-x: clip; } @@ -161,6 +165,17 @@ pre { line-height: 1.7 !important; } +/* Disable monospace ligatures so `--flag` keeps a visible space and double + dashes don't fuse into an em-dash glyph. */ +code, +pre, +pre code, +.ktx-code, +.ktx-code code { + font-variant-ligatures: none !important; + font-feature-settings: "liga" 0, "calt" 0 !important; +} + .dark pre { background: transparent !important; } @@ -216,57 +231,10 @@ figure[data-rehype-pretty-code-figure]:has(.ktx-code) { margin: 0; } -/* ── Mode A: Terminal ─────────────────────── */ -.ktx-code-terminal { - background: #0c1417; - border: 1px solid rgba(255, 255, 255, 0.08); - color: #c8c3bc; - box-shadow: - 0 1px 2px rgba(0, 0, 0, 0.1), - 0 12px 32px -16px rgba(0, 0, 0, 0.3); -} - -.ktx-code-terminal:hover { - border-color: rgba(34, 211, 238, 0.2); - box-shadow: - 0 1px 2px rgba(0, 0, 0, 0.1), - 0 14px 32px -12px rgba(34, 211, 238, 0.18); -} - -.ktx-code-terminal-head { - display: flex; - align-items: center; - gap: 6px; - padding: 10px 12px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent); -} - -.ktx-tl-dot { - width: 11px; - height: 11px; - border-radius: 999px; - flex-shrink: 0; -} - -.ktx-code-terminal-label { - margin-left: 8px; - font-size: 11px; - font-weight: 500; - letter-spacing: 0.02em; - color: rgba(255, 255, 255, 0.4); -} - -.ktx-code-body-terminal { - background: transparent !important; - color: #c8c3bc !important; -} - /* ── Mode D: Output preview (wizard prompts, status output) ── */ .ktx-code-output { background: var(--color-fd-muted); border: 1px solid var(--color-fd-border); - border-left: 3px solid color-mix(in oklch, var(--color-fd-primary) 50%, var(--color-fd-border)); position: relative; box-shadow: 0 1px 2px rgba(27, 27, 24, 0.02); } @@ -274,17 +242,14 @@ figure[data-rehype-pretty-code-figure]:has(.ktx-code) { .dark .ktx-code-output { background: #111a1e; border-color: rgba(255, 255, 255, 0.05); - border-left-color: rgba(34, 211, 238, 0.25); } .ktx-code-output:hover { border-color: color-mix(in oklch, var(--color-fd-primary) 25%, var(--color-fd-border)); - border-left-color: var(--color-fd-primary); } .dark .ktx-code-output:hover { border-color: rgba(255, 255, 255, 0.08); - border-left-color: rgba(34, 211, 238, 0.45); } .ktx-code-output-label { @@ -304,8 +269,8 @@ figure[data-rehype-pretty-code-figure]:has(.ktx-code) { .ktx-code-output-copy { position: absolute !important; - top: 6px !important; - right: 6px !important; + top: 7px !important; + right: 8px !important; opacity: 0; transform: translateY(-4px); transition: opacity 0.2s var(--ktx-ease), transform 0.2s var(--ktx-ease); @@ -445,30 +410,10 @@ figure[data-rehype-pretty-code-figure]:has(.ktx-code) { border-color: rgba(34, 211, 238, 0.2); } -.ktx-code-minimal-lang { - position: absolute; - top: 8px; - left: 14px; - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-fd-muted-foreground); - font-family: var(--font-display), var(--font-sans), sans-serif; - opacity: 0; - transition: opacity 0.2s var(--ktx-ease); - pointer-events: none; - z-index: 1; -} - -.ktx-code-minimal:hover .ktx-code-minimal-lang { - opacity: 0.5; -} - .ktx-code-minimal-copy { position: absolute !important; - top: 6px !important; - right: 6px !important; + top: 7px !important; + right: 8px !important; opacity: 0; transform: translateY(-4px); transition: opacity 0.2s var(--ktx-ease), transform 0.2s var(--ktx-ease); @@ -778,8 +723,8 @@ body::after { mix-blend-mode: overlay; } -/* Make sure content stays above background */ -body > * { +/* Make sure page content stays above the decorative background. */ +.ktx-site-shell { position: relative; z-index: 2; } @@ -1058,8 +1003,7 @@ body > * { .pill-badge .pill-dot { animation: none; } .card-lift { transition: none; } .ktx-code, - .ktx-code-minimal-copy, - .ktx-code-minimal-lang { + .ktx-code-minimal-copy { transition: none; } #nd-sidebar div[data-state]:not([class]) > button[data-state] svg { diff --git a/docs-site/app/layout.tsx b/docs-site/app/layout.tsx index 48e12a3f..7c808130 100644 --- a/docs-site/app/layout.tsx +++ b/docs-site/app/layout.tsx @@ -41,7 +41,9 @@ export default function RootLayout({ children }: { children: ReactNode }) { suppressHydrationWarning > - {children} + +
{children}
+
); diff --git a/docs-site/components/code-block.tsx b/docs-site/components/code-block.tsx index 7d6a22af..9c9d71ec 100644 --- a/docs-site/components/code-block.tsx +++ b/docs-site/components/code-block.tsx @@ -13,7 +13,7 @@ type Props = ComponentPropsWithoutRef<"pre"> & { "data-language"?: string; }; -const TERMINAL_LANGS = new Set(["bash", "sh", "shell", "zsh"]); +const OUTPUT_LANGS = new Set(["text", "plain", "plaintext", "console", "output"]); const WIZARD_GLYPHS = /^\s*[◆◇◯◐○●]/; function extractText(node: ReactNode): string { @@ -27,6 +27,33 @@ function extractText(node: ReactNode): string { return ""; } +function findLanguageInNode(node: ReactNode): string | null { + if (!isValidElement(node)) return null; + const props = (node as ReactElement<{ + className?: string; + "data-language"?: string; + children?: ReactNode; + }>).props; + + const dataLang = props["data-language"]; + if (typeof dataLang === "string" && dataLang) return dataLang; + + const className = typeof props.className === "string" ? props.className : ""; + const m = className.match(/language-([\w-]+)/); + if (m) return m[1]; + + const children = props.children; + if (Array.isArray(children)) { + for (const child of children) { + const found = findLanguageInNode(child); + if (found) return found; + } + } else if (children) { + return findLanguageInNode(children); + } + return null; +} + function detectLanguage(props: Props, children: ReactNode): string | null { const dataLang = props["data-language"]; if (typeof dataLang === "string" && dataLang) return dataLang; @@ -35,14 +62,7 @@ function detectLanguage(props: Props, children: ReactNode): string | null { const m = className.match(/language-([\w-]+)/); if (m) return m[1]; - if (isValidElement(children)) { - const childProps = (children as ReactElement<{ className?: string }>).props; - const childClass = typeof childProps.className === "string" ? childProps.className : ""; - const cm = childClass.match(/language-([\w-]+)/); - if (cm) return cm[1]; - } - - return null; + return findLanguageInNode(children); } export function CodeBlock(props: Props) { @@ -50,32 +70,11 @@ export function CodeBlock(props: Props) { const language = detectLanguage(props, children); const codeText = extractText(children); - const isTerminal = language !== null && TERMINAL_LANGS.has(language); - const isOutput = !isTerminal && WIZARD_GLYPHS.test(codeText); const hasTitle = typeof title === "string" && title.length > 0; - - // Mode A - Terminal (commands the user types) - if (isTerminal) { - return ( -
-
- - - - - {hasTitle ? title : "zsh"} - - -
-
-          {children}
-        
-
- ); - } + const isOutput = + !hasTitle && + (WIZARD_GLYPHS.test(codeText) || + (language !== null && OUTPUT_LANGS.has(language))); // Mode D - Output preview (wizard prompts, terminal output) if (isOutput) { @@ -110,7 +109,6 @@ export function CodeBlock(props: Props) { // Mode C - Minimal default return (
- {language && {language}}
         {children}
diff --git a/docs-site/components/copy-button.tsx b/docs-site/components/copy-button.tsx
index 876f5de0..c0dd1f31 100644
--- a/docs-site/components/copy-button.tsx
+++ b/docs-site/components/copy-button.tsx
@@ -25,12 +25,12 @@ export function CopyButton({ text, className = "" }: Props) {
       type="button"
       onClick={onClick}
       aria-label={copied ? "Copied" : "Copy code"}
-      className={`inline-flex items-center justify-center w-7 h-7 rounded-md transition-all hover:bg-white/5 ${className}`}
+      className={`inline-flex items-center justify-center w-9 h-9 rounded-md transition-all hover:bg-fd-muted ${className}`}
     >
       {copied ? (
         
       ) : (
          setCopied(false), 1500);
     } catch {
-      // Clipboard denied — fail silently
+      // Clipboard denied - fail silently
     }
   };
 
diff --git a/docs-site/components/product-mechanics.tsx b/docs-site/components/product-mechanics.tsx
index 1c10e9aa..45baaa31 100644
--- a/docs-site/components/product-mechanics.tsx
+++ b/docs-site/components/product-mechanics.tsx
@@ -1,115 +1,502 @@
-import type { ReactNode } from "react";
+"use client";
 
-type SourceInput = {
-  name: string;
-  sources: string[];
-  detail: string;
-  signal: string;
+import {
+  Background,
+  BackgroundVariant,
+  type Edge,
+  type EdgeProps,
+  getSmoothStepPath,
+  Handle,
+  MarkerType,
+  type Node,
+  type NodeProps,
+  Position,
+  ReactFlow,
+} from "@xyflow/react";
+import "@xyflow/react/dist/style.css";
+import { useEffect, useMemo, useState } from "react";
+
+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;
+type StageNode = Node;
+type OutputNode = Node;
+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", "Any text"],
+    accent: "#10b981",
   },
 ];
 
-const artifacts = [
+const stageData: Omit[] = [
   {
-    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((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((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((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,
+  },
+};
+
+
+function SourceNodeView({ data }: NodeProps) {
+  return (
+    
+ +

+ {data.title} +

+

+ {data.body} +

+
+ {data.items.map((item) => ( + + {item} + + ))} +
+ +
+ ); +} + +function StageNodeView({ data }: NodeProps) { + return ( +
+ + + {data.index} + +
+

+ {data.title} +

+

+ {data.body} +

+
+ +
+ ); +} + +function OutputNodeView({ data }: NodeProps) { + return ( +
+ + + +

+ {data.path} +

+

+ {data.title} +

+
+ {data.tags.map((tag) => ( + + {tag} + + ))} +
+

+ {data.body} +

+
+ ); +} + +const PARTICLES_PER_SOURCE = 2; +const PARTICLE_SPEED_PX_PER_SEC = 130; +const PARTICLE_MIN_DURATION_SEC = 4; + +const stageTopY = (i: number) => ROW_STAGE_START_Y + i * (STAGE_H + STAGE_GAP); +const stageBottomY = (i: number) => stageTopY(i) + STAGE_H; + +function buildParticlePath( + sourceIndex: number, + outputIndex: number, +): { d: string; length: number } { + const sourceCenterX = + SOURCES_START_X + sourceIndex * (SOURCE_W + SOURCE_GAP_X) + SOURCE_W / 2; + const sourceBottomYVal = ROW_SOURCES_Y + SOURCE_H; + const outputCenterX = + OUTPUTS_START_X + outputIndex * (OUTPUT_W + OUTPUT_GAP_X) + OUTPUT_W / 2; + + const legs: Array<[number, number, number, number]> = [ + [sourceCenterX, sourceBottomYVal, STAGE_CENTER_X, stageTopY(0)], + [STAGE_CENTER_X, stageBottomY(0), STAGE_CENTER_X, stageTopY(1)], + [STAGE_CENTER_X, stageBottomY(1), STAGE_CENTER_X, stageTopY(2)], + [STAGE_CENTER_X, stageBottomY(2), STAGE_CENTER_X, stageTopY(3)], + [STAGE_CENTER_X, stageBottomY(3), outputCenterX, ROW_OUTPUTS_Y], + ]; + + const segments = legs.map(([sx, sy, tx, ty]) => { + const [segment] = getSmoothStepPath({ + sourceX: sx, + sourceY: sy, + sourcePosition: Position.Bottom, + targetX: tx, + targetY: ty, + targetPosition: Position.Top, + }); + return segment; + }); + + let d = segments[0]; + for (let i = 1; i < segments.length; i += 1) { + d += ` ${segments[i].replace(/^M/, "L")}`; + } + + const length = legs.reduce( + (sum, [sx, sy, tx, ty]) => sum + Math.abs(tx - sx) + Math.abs(ty - sy), + 0, + ); + + return { d, length }; +} + +type ParticleEdgeData = { + d: string; + duration: number; + beginOffset: number; + color: string; +}; + +type ParticleEdge = Edge; + +function ParticleEdgeView({ id, data }: EdgeProps) { + if (!data) return null; + const pathId = `mechanics-particle-path-${id}`; + return ( + <> + + + + + + + + + + + ); +} + +const nodeTypes = { + source: SourceNodeView, + stage: StageNodeView, + output: OutputNodeView, +}; + +const edgeTypes = { + particle: ParticleEdgeView, +}; + +const staticEdges = [...flowEdges, refsEdge]; + +type ParticleSpec = { + id: string; + sourceIndex: number; + outputIndex: number; +}; + +function makeRandomParticles(perSource: number): ParticleSpec[] { + const specs: ParticleSpec[] = []; + for (let sourceIndex = 0; sourceIndex < sourceData.length; sourceIndex += 1) { + for (let n = 0; n < perSource; n += 1) { + specs.push({ + id: `particle-${sourceIndex}-${n}`, + sourceIndex, + outputIndex: Math.floor(Math.random() * outputData.length), + }); + } + } + return specs; +} + +function specToEdge(spec: ParticleSpec): { + id: string; + source: string; + target: string; + type: "particle"; + data: ParticleEdgeData; +} { + const { d, length } = buildParticlePath(spec.sourceIndex, spec.outputIndex); + const duration = Math.max( + PARTICLE_MIN_DURATION_SEC, + length / PARTICLE_SPEED_PX_PER_SEC, + ); + return { + id: spec.id, + source: `source-${spec.sourceIndex}`, + target: `output-${spec.outputIndex}`, + type: "particle", + data: { + d, + duration, + beginOffset: Math.random() * duration, + color: sourceData[spec.sourceIndex].accent, + }, + }; +} + export function ProductMechanics() { + const [particles, setParticles] = useState([]); + + useEffect(() => { + setParticles(makeRandomParticles(PARTICLES_PER_SOURCE)); + }, []); + + const edges = useMemo( + () => [...staticEdges, ...particles.map(specToEdge)], + [particles], + ); + return (
- How KTX works + How ingestion works

- 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.

-
- - -
+
+
+

+ Ingestion flow +

+

+ From scattered source systems to agent-ready context +

+

+ 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. +

+
+ +
+ + + +
+
+ ); } - -function IngestionDiagram() { - return ( -
- - -
-
- Inputs KTX reads -
- {sourceInputs.map((source) => ( -
-
-

- {source.name} -

-

- {source.detail} -

-

- {source.signal} -

-
- -
- ))} -
-
- -
- KTX transforms evidence -
-
-

- KTX builds the model -

-
    - {ingestSteps.map((step, index) => ( - - ))} -
-
- -
-

- Outputs KTX writes -

-
- {artifacts.map((artifact) => ( - - ))} -
-
-
-
-
-
- ); -} - -function RuntimeDiagram() { - return ( -
- - -
-
- Agent sends - -
connection: warehouse
-
measure: orders.total_revenue
-
dimension: customers.segment
-
filter: orders.created_date >= '2024-01-01'
-
-

- This is the API surface agents should use: compact semantic intent, - not hand-written warehouse SQL. -

-
- -
- KTX planning and execution -
    - {runtimeSteps.map((step, index) => ( - - ))} -
-
-
- -
-
- Semantic query plan -
-

- source:{" "} - orders joined to customers as many_to_one -

-

- measure:{" "} - total_revenue = sum(amount) with refund filter -

-

- grain: segment - group-by with date predicate -

-

- result: dialect - SQL, bounded rows, and provenance -

-
-
- -
- KTX returns - -
select
-
customers.segment,
-
sum(orders.amount) as total_revenue
-
from analytics.orders
-
join analytics.customers
-
on orders.customer_id = customers.id
-
where orders.status != 'refunded'
-
and orders.created_date >= '2024-01-01'
-
group by 1
-
-

- The output can be SQL-only or executed results with provenance, so - the agent can show where the answer came from. -

-
-
-
- ); -} - -function DiagramHeader({ - body, - eyebrow, - id, - title, -}: { - body: string; - eyebrow: string; - id: string; - title: string; -}) { - return ( -
-

- {eyebrow} -

-

- {title} -

-

- {body} -

-
- ); -} - -function Artifact({ - body, - path, - title, -}: { - body: string; - path: string; - title: string; -}) { - return ( -
-

- {path} -

-

{title}

-

- {body} -

-
- ); -} - -function PipelineStep({ - body, - dark = false, - index, - title, -}: { - body: string; - dark?: boolean; - index: number; - title: string; -}) { - return ( -
  • - - {index} - - - - {title} - - - {body} - - -
  • - ); -} - -function ColumnLabel({ children }: { children: ReactNode }) { - return ( -

    - {children} -

    - ); -} - -function SourceList({ - sources, -}: { - sources: string[]; -}) { - return ( -
    -

    - Sources -

    -
    - {sources.map((source) => - source === "and many others" ? ( - - {source} - - ) : ( - - {source} - - ), - )} -
    -
    - ); -} - -function CodeBox({ children }: { children: ReactNode }) { - return ( -
    -
    {children}
    -
    - ); -} diff --git a/docs-site/content/docs/cli-reference/ktx-dev.mdx b/docs-site/content/docs/cli-reference/ktx-dev.mdx index 50d4ea77..efa3d74b 100644 --- a/docs-site/content/docs/cli-reference/ktx-dev.mdx +++ b/docs-site/content/docs/cli-reference/ktx-dev.mdx @@ -36,7 +36,7 @@ directory. Use it from any directory to generate editor or agent schema files. | Flag | Description | Default | |------|-------------|---------| -| `--output ` | Write the schema to a file instead of stdout | — | +| `--output ` | Write the schema to a file instead of stdout | - | ## `dev runtime` Subcommands diff --git a/docs-site/content/docs/cli-reference/ktx-ingest.mdx b/docs-site/content/docs/cli-reference/ktx-ingest.mdx index 9d94cd88..49485d10 100644 --- a/docs-site/content/docs/cli-reference/ktx-ingest.mdx +++ b/docs-site/content/docs/cli-reference/ktx-ingest.mdx @@ -33,7 +33,7 @@ connections when you use `--all`. | `--plain` | Print plain text output | `true` | | `--json` | Print JSON output | `false` | | `--yes` | Install required managed runtime features without prompting | `false` | -| `--no-input` | Disable interactive terminal input | — | +| `--no-input` | Disable interactive terminal input | - | `--fast` and `--deep` are mutually exclusive. Depth flags apply only to database connections. Query-history flags apply only to database connections @@ -60,7 +60,7 @@ read one item from stdin. | Flag | Description | Default | |------|-------------|---------| | `--text ` | Text content to ingest; repeat for a batch | `[]` | -| `--connection-id ` | Optional KTX connection id for semantic-layer capture | — | +| `--connection-id ` | Optional KTX connection id for semantic-layer capture | - | | `--user-id ` | Memory user id for capture attribution | `local-cli` | | `--json` | Print JSON output | `false` | | `--fail-fast` | Stop after the first failed text item | `false` | diff --git a/docs-site/content/docs/getting-started/introduction.mdx b/docs-site/content/docs/getting-started/introduction.mdx index 499d25a0..d0ee126d 100644 --- a/docs-site/content/docs/getting-started/introduction.mdx +++ b/docs-site/content/docs/getting-started/introduction.mdx @@ -1,6 +1,6 @@ --- title: Introduction -description: What KTX is, how it works, and where to start. +description: KTX is an open-source, self-improving context layer for data agents. --- import { ProductMechanics } from "@/components/product-mechanics"; @@ -23,46 +23,67 @@ import { ProductMechanics } from "@/components/product-mechanics"; Make analytics context usable by agents

    - {'KTX turns warehouse metadata, semantic definitions, BI usage, and team knowledge into local files and runtime tools that database agents can trust.'} + {'KTX is an open-source context layer for database agents. It turns warehouse metadata, BI models, query history, docs, and approved metric definitions into reviewable files agents can search and execute.'}

    -## Why KTX +## Why KTX helps -- Schemas show columns, not business rules. -- Agents need trusted metrics, joins, filters, caveats, and provenance. -- KTX captures that context before agents write SQL, docs, or semantic edits. +KTX gives agents a shared context workspace before they write SQL, answer a +question, or update analytics definitions. -## What KTX creates +- **Context as code.** KTX writes wiki pages and semantic-layer definitions as + git-based files you can review, diff, and merge. +- **Self-improving ingest.** KTX reads warehouses, BI tools, modeling code, + query history, and notes, then reconciles new evidence with accepted context. +- **Executable semantics.** Agents can use approved measures, joins, filters, + dimensions, and segments instead of rebuilding canonical SQL from scratch. +- **Agent-native access.** CLI and MCP tools let agents search context, compile + semantic queries, run read-only SQL, and propose updates. -| Path | What it gives agents | -|------|----------------------| -| `semantic-layer/` | Measures, dimensions, joins, grain, filters, segments | -| `wiki/` | Business definitions, caveats, policies, analyst notes | -| `raw-sources/` | Extracted metadata, scan output, relationship evidence | -| `.ktx/` | Local indexes, embeddings, setup state, runtime data | +KTX complements existing semantic layers by pairing metric definitions with the +surrounding business knowledge, caveats, provenance, and review workflow agents +need for data work. + +## How KTX works + +KTX has two connected sides: it builds and maintains the context layer, then +serves that context to agents at runtime. + +| Side | What KTX does | +|------|---------------| +| **Ingest and auto-maintain knowledge** | Reads your data stack and company knowledge, reconciles new evidence with accepted context, and keeps changes to `semantic-layer/` plus `wiki/` as version-controlled diffs automatically. | +| **Serve agents at runtime** | Helps agents find the right wiki pages and semantic-layer entities, then compile or execute semantic queries through CLI and MCP tools. | ## Use it for -- **Generate SQL** from approved measures, dimensions, joins, and filters -- **Explain provenance** with wiki context and warehouse evidence -- **Repair context** through reviewable YAML and Markdown diffs -- **Work alongside** dbt, LookML, MetricFlow, Looker, Metabase, and warehouses +Use KTX when agents need more than raw database access. Agents can search wiki +context, find semantic-layer entities, compile trusted semantic queries, run +read-only SQL, and use the same tools through MCP. -Databases: SQLite, PostgreSQL, Snowflake, BigQuery, ClickHouse, MySQL, SQL -Server. +- Generate SQL from approved metrics, joins, filters, and dimensions. +- Explain metric provenance with wiki context and source evidence. +- Repair context through reviewable YAML and Markdown diffs. +- Work alongside dbt, MetricFlow, LookML, Looker, Metabase, Notion, and + supported databases. ## Start here +Choose the route that matches what you want to do next. The quickstart is the +best first step for users; contributor setup lives in the community docs. + - Set up KTX and build your first context in under 10 minutes. + Install KTX, run setup, build context, and connect an agent. - - Hands-on workflows for scanning, ingesting, writing, and serving. + + Understand why agents need more than schema access and raw SQL. + + + Refresh context from databases, BI tools, query history, and documents. Edit semantic-layer YAML and wiki Markdown safely. diff --git a/docs-site/content/docs/getting-started/quickstart.mdx b/docs-site/content/docs/getting-started/quickstart.mdx index 0118522c..23c77827 100644 --- a/docs-site/content/docs/getting-started/quickstart.mdx +++ b/docs-site/content/docs/getting-started/quickstart.mdx @@ -1,135 +1,93 @@ --- title: Quickstart -description: Set up KTX, build local context, and connect your coding agent. +description: Install KTX, run setup, and connect your coding agent. --- -This guide gets a local analytics project ready for KTX. You will install the -CLI, run the setup wizard, connect a database, build context, and install agent -rules that teach your coding assistant which KTX commands to run. +This guide takes a local analytics project from empty to agent-ready. You'll +install the CLI, run one guided setup command, and hand the context to a +coding assistant. -If you are a coding assistant choosing a docs route, start with the -[Agent Quickstart](/docs/ai-resources/agent-quickstart). This page is the -human setup walkthrough. +If you're a coding assistant choosing a docs route, start with the +[Agent Quickstart](/docs/ai-resources/agent-quickstart) instead. -## What setup does - -`ktx setup` is the main project workflow. It can create or resume `ktx.yaml`, -configure model and embedding providers, add database connections, add optional -context sources, build the first context artifacts, and install agent -integration. - -When you run bare `ktx` in an interactive terminal outside a KTX project, the -CLI opens the same setup experience. Inside an existing project, `ktx setup` -resumes incomplete work or opens a menu for changing setup, connecting an -agent, checking status, or exploring a demo project. +
    +
    + No warehouse handy? +
    +
    + Try KTX against a real data stack - Postgres, dbt, Metabase, and Notion + pre-loaded with the Orbit demo corpus. The page lists demo credentials + you can paste straight into `ktx setup`. +
    + + Get demo credentials at kaelio.com/start → + +
    ## Install the CLI -Install the published `@kaelio/ktx` package: +Install the published package globally: ```bash npm install -g @kaelio/ktx ``` -Then run setup from the analytics project directory: +KTX is open source. If you'd like to hack on it or run from a local checkout, +the source lives at [github.com/kaelio/ktx](https://github.com/kaelio/ktx) - +see [Contributing](/docs/community/contributing) to get set up. + +## Run setup + +From your project directory, run: ```bash ktx setup ``` -The local checkout workflow is only for KTX contributors. See -[Contributing](/docs/community/contributing) for that path. +The wizard walks you through everything KTX needs in one pass: -## Step 1: Choose the project +1. **Project** - creates or resumes `ktx.yaml` in the current directory. +2. **LLM** - picks a Claude backend. The default uses your local Claude Code + session, so no API key is required. You can also use an Anthropic API key + or Vertex AI. +3. **Embeddings** - picks an embeddings backend. Choose OpenAI for hosted + embeddings or `sentence-transformers` to run locally without an API key. +4. **Database** - adds at least one primary connection. Supported drivers: + SQLite, PostgreSQL, MySQL, ClickHouse, SQL Server, BigQuery, and Snowflake. +5. **Context sources** - optionally adds dbt, MetricFlow, LookML, Looker, + Metabase, or Notion. You can skip and add them later. +6. **Build** - runs the first ingest so semantic-layer sources and wiki pages + are ready for agents. +7. **Agent integration** - installs project-local rules for Claude Code, + Codex, Cursor, OpenCode, or universal `.agents`. -In an interactive terminal, setup can create a new KTX project or resume the -nearest existing project. The main project file is `ktx.yaml`. - -For scripted setup, pass the project directory explicitly: - -```bash -ktx setup --project-dir ./analytics -``` - -If setup exits early, rerun `ktx setup` in the same directory. KTX keeps local -setup progress under `.ktx/setup/` and resumes from the remaining work. - -## Step 2: Configure the LLM - -KTX uses a Claude model for ingest agents that turn schemas, SQL, BI metadata, -and documents into semantic-layer sources and wiki context. - -Setup supports three LLM provider paths: - -| Provider | Use when | Credential model | -|----------|----------|------------------| -| Claude subscription (Pro/Max) | You want KTX to use your local Claude Code session | Claude Code local authentication | -| Anthropic API key | You have an Anthropic API key | `ANTHROPIC_API_KEY` or a local `file:` secret | -| Google Vertex AI for Anthropic Claude | Your organization runs Claude through Google Cloud | Application Default Credentials plus Vertex project and location | - -For Anthropic API, setup can read the key from the environment or save a pasted -key to `.ktx/secrets/anthropic-api-key`. `ktx.yaml` stores an `env:` or `file:` -reference, not the raw key. - -For Vertex AI, setup uses Google Application Default Credentials. It can read -your active `gcloud` project, list visible projects, or accept explicit -`--vertex-project` and `--vertex-location` values. - -To use your local Claude Code session instead of an API key, set: - -```yaml -llm: - provider: - backend: claude-code - models: - default: sonnet - triage: haiku - candidateExtraction: sonnet - curator: sonnet - reconcile: sonnet - repair: sonnet -``` - -`claude-code` uses the Claude Code authentication already configured on your -machine. It doesn't use `ANTHROPIC_API_KEY`, Vertex credentials, AI Gateway -tokens, or Bedrock credentials. In non-interactive setup, pass -`--llm-model opus`, `--llm-model sonnet`, `--llm-model haiku`, or a full Claude -model ID to select the Claude Code model. - -Setup checks the selected model before saving. Anthropic API setup fetches live -Claude model choices when possible and falls back to bundled defaults if model -discovery is unavailable. - -## Step 3: Configure embeddings - -KTX uses embeddings for semantic search over semantic-layer sources, wiki -context, schema metadata, and relationship evidence. - -| Backend | Default model | Notes | -|---------|---------------|-------| -| OpenAI | `text-embedding-3-small` | Recommended for hosted embeddings. Requires an OpenAI API key. | -| Local sentence-transformers | `all-MiniLM-L6-v2` | Runs through the KTX-managed Python runtime. No hosted embedding key is required. | - -OpenAI setup reads `OPENAI_API_KEY` or saves a local secret file. Local -sentence-transformers setup can install and start the managed runtime during -setup. To prepare that runtime before setup, run: +If you choose local `sentence-transformers` embeddings, KTX uses the managed +Python runtime. To prepare it before setup, run: ```bash ktx dev runtime install --feature local-embeddings --yes ktx dev runtime start --feature local-embeddings ``` -## Step 4: Add a database - -KTX needs at least one primary database connection before it can build database -context. The wizard supports SQLite, PostgreSQL, MySQL, ClickHouse, SQL Server, -BigQuery, and Snowflake. - -You can usually enter connection fields interactively or provide a URL. Secret -URLs can be stored as local files under `.ktx/secrets/` or referenced with -`env:NAME` in `ktx.yaml`. - -After saving a connection, setup tests it and builds fast schema context: +During the database step, setup tests the saved connection and builds initial +schema context: ```text Testing warehouse @@ -137,114 +95,24 @@ Testing warehouse Building schema context for warehouse Running fast database ingest - -Database ready - warehouse - PostgreSQL - schema context complete ``` -PostgreSQL, BigQuery, and Snowflake can also enable query-history ingest. Query -history helps KTX learn common query patterns, joins, service-account filters, -and warehouse-specific usage. BigQuery and Snowflake support a lookback window; -Postgres reads the current `pg_stat_statements` aggregate data instead. +If setup exits early, rerun `ktx setup` in the same directory. KTX keeps +progress under `.ktx/setup/` and resumes from the remaining work. -## Step 5: Add context sources +> **Note:** Running bare `ktx` in an interactive terminal outside a KTX +> project opens the same wizard. Inside a project, it opens a menu for +> resuming setup, connecting an agent, checking status, or exploring a +> pre-built demo project. -Context sources are optional, but they make the first context layer much richer. -Setup can add: +## Verify -| Source | Typical input | What KTX learns | -|--------|---------------|-----------------| -| dbt | Local project or Git repo | Models, columns, tests, descriptions, tags | -| MetricFlow | Local project or Git repo | Semantic models, metrics, dimensions, entities | -| LookML | Local files or Git repo | Views, explores, dimensions, measures, joins | -| Looker | API URL and credentials | Explores, looks, dashboards, model metadata | -| Metabase | API URL and key | Questions, dashboards, BI database mappings | -| Notion | Integration token and crawl settings | Business docs and knowledge pages | - -Setup maps BI and source metadata back to your primary warehouse connection so -generated context points at the right tables. - -You can skip this step and add sources later by rerunning `ktx setup`. - -## Step 6: Build context - -The context build turns configured databases and sources into local artifacts -agents can read. It runs database ingest first, then source ingest and memory -updates. - -Fast database ingest records deterministic schema grounding. Deep ingest adds -AI-enriched descriptions, embeddings, relationship evidence, and query-history -context when configured. - -When the build finishes, setup verifies that agent-ready context exists: - -```text -KTX context is ready for agents. - -Databases: - warehouse: deep context complete - -Context sources: - dbt_main: memory update complete - -Verification: - Agent context: ready - Semantic search: ready -``` - -If a foreground build is interrupted, rerun `ktx setup` or build the same target -with `ktx ingest `. - -## Step 7: Install agent integration - -The final setup step installs project-local rules for your coding assistant. -Supported targets are Claude Code, Codex, Cursor, OpenCode, and universal -`.agents`. - -You can also run this step later: - -```bash -ktx setup --agents --target codex -``` - -Claude Code and Codex also support global installs: - -```bash -ktx setup --agents --target codex --global -``` - -Agent rules are CLI-based. They point agents at the KTX CLI path that created -the file, so agents do not need a separate `ktx` binary in `PATH`. If the CLI -path changes after reinstalling or moving a checkout, rerun `ktx setup --agents`. - -## Generated files - -KTX writes plain files so people and agents can inspect changes in git. - -| Path | Purpose | -|------|---------| -| `ktx.yaml` | Project configuration for LLMs, embeddings, connections, context sources, and query-history settings | -| `.ktx/secrets/*` | Local secret files referenced from `ktx.yaml`; do not commit these | -| `.ktx/setup/*` | Local setup and context-build state | -| `.ktx/agents/install-manifest.json` | Manifest used to manage installed agent files | -| `semantic-layer//*.yaml` | Semantic source definitions used for SQL generation | -| `wiki/global/*.md` | Shared business context and metric definitions | -| `wiki/user//*.md` | User-scoped notes and local context | -| `.claude/skills/ktx/SKILL.md` | Claude Code project skill | -| `.agents/skills/ktx/SKILL.md` | Codex or universal project skill | -| `.cursor/rules/ktx.mdc` | Cursor project rule | -| `.opencode/commands/ktx.md` | OpenCode project command | - -## Verify setup - -Run: +When setup finishes, check readiness: ```bash ktx status ``` -Example output: - ```text KTX project: /home/user/analytics Project ready: yes @@ -256,15 +124,49 @@ KTX context built: yes Agent integration ready: yes (codex:project) ``` -Use JSON when an agent or script needs a structured readiness check: +For a structured check inside scripts, use `ktx status --json`. -```bash -ktx status --json +When setup builds deep context, its final context check looks like: + +```text +KTX context is ready for agents. + +Databases: + warehouse: deep context complete + +Context sources: + dbt_main: memory update complete ``` -## Scripted setup example +## Connect a coding agent -Use non-interactive setup when creating repeatable fixtures or automation: +The setup wizard installs project-local agent rules in the last step. To +install or change targets later: + +```bash +ktx setup --agents +``` + +Claude Code and Codex also support global installs with `--global`. Agent +rules point at the KTX CLI path that created them, so agents don't need a +separate `ktx` binary on `PATH`. If the CLI path changes, rerun +`ktx setup --agents`. + +## What setup writes + +KTX writes plain files so people and agents can review changes in git. + +| Path | Purpose | +|------|---------| +| `ktx.yaml` | Project configuration | +| `.ktx/secrets/*` | Local secret files referenced from `ktx.yaml` - do not commit | +| `semantic-layer//*.yaml` | Semantic sources for SQL generation | +| `wiki/global/*.md` | Shared business context and metric definitions | +| `.claude/skills/ktx/`, `.agents/skills/ktx/`, `.cursor/rules/ktx.mdc`, `.opencode/commands/ktx.md` | Installed agent rules | + +## Scripted setup + +For repeatable fixtures and automation, skip prompts with flags: ```bash ktx setup \ @@ -287,23 +189,21 @@ ktx ingest warehouse --fast See [ktx setup](/docs/cli-reference/ktx-setup) for the full automation flag surface. -## Common errors +## Common issues -| Symptom | Likely cause | Recovery | -|---------|--------------|----------| -| `ktx: command not found` | The global package is not installed or your shell cannot find it | Reinstall `@kaelio/ktx` and open a new shell | -| Setup resumes the wrong project | `KTX_PROJECT_DIR` or the nearest `ktx.yaml` points somewhere else | Pass `--project-dir ` | -| Anthropic health check fails | API key, model id, or access is invalid | Fix `ANTHROPIC_API_KEY` or rerun setup with a different key or model | -| Vertex AI health check fails | Vertex API, Claude access, project, location, or IAM permissions are missing | Check the project, location, Application Default Credentials, and Vertex AI permissions | -| OpenAI embeddings fail | `OPENAI_API_KEY` is missing or invalid | Export the key or choose local sentence-transformers embeddings | -| Local embeddings fail | Managed Python runtime cannot install or start | Run `ktx dev runtime status`, then install the local embeddings runtime | -| Database test fails | Credentials, network access, database, warehouse, or schema is wrong | Test the same values with the database's native client, then rerun setup | -| Context is not built | Setup saved configuration but skipped or interrupted the build | Run `ktx setup` or `ktx ingest --all` | -| Agent integration is incomplete | Setup skipped the agents step or installed a different target | Run `ktx setup --agents --target ` | +| Symptom | Fix | +|---------|-----| +| `ktx: command not found` | Reinstall `@kaelio/ktx` and open a new shell | +| Setup resumes the wrong project | Pass `--project-dir ` | +| LLM or embeddings health check fails | Rerun setup and pick a different credential, model, or backend | +| Database test fails | Verify the same connection with the database's native client, then rerun setup | +| Agent integration is incomplete | Run `ktx setup --agents --target ` | ## Next steps -- Build and refresh context with [Building Context](/docs/guides/building-context). -- Edit semantic sources and wiki pages with [Writing Context](/docs/guides/writing-context). +- Refresh context with [Building Context](/docs/guides/building-context). +- Edit semantic sources and wiki pages with + [Writing Context](/docs/guides/writing-context). - Connect more tools with [Agent Clients](/docs/integrations/agent-clients). -- Read [The Context Layer](/docs/concepts/the-context-layer) to understand the architecture. +- Read [The Context Layer](/docs/concepts/the-context-layer) to understand + the architecture. diff --git a/docs-site/package.json b/docs-site/package.json index 229af1e3..4cf896ff 100644 --- a/docs-site/package.json +++ b/docs-site/package.json @@ -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" } } diff --git a/docs-site/source.config.ts b/docs-site/source.config.ts index 71c0f614..5af0fa2b 100644 --- a/docs-site/source.config.ts +++ b/docs-site/source.config.ts @@ -5,5 +5,13 @@ export const docs = defineDocs({ }); export default defineConfig({ - mdxOptions: {}, + mdxOptions: { + rehypeCodeOptions: { + addLanguageClass: true, + themes: { + light: "min-light", + dark: "github-dark", + }, + }, + }, }); diff --git a/docs-site/tests/docs-index-route.test.mjs b/docs-site/tests/docs-index-route.test.mjs index 7d1c62c0..721813ec 100644 --- a/docs-site/tests/docs-index-route.test.mjs +++ b/docs-site/tests/docs-index-route.test.mjs @@ -111,3 +111,21 @@ test("/ktx/docs redirects to the docs introduction", async () => { `${docsBasePath}/docs/getting-started/introduction`, ); }); + +test("/ktx/api/search returns docs search results", async () => { + const response = await fetch( + `${docsSiteUrl}${docsBasePath}/api/search?query=setup`, + ); + + assert.equal(response.status, 200); + + const results = await response.json(); + assert.ok(Array.isArray(results), "search response should be an array"); + assert.ok( + results.some( + (result) => + typeof result.url === "string" && result.url.startsWith("/docs/"), + ), + "search should return at least one docs result", + ); +}); diff --git a/docs-site/tests/docs-search-behavior.test.mjs b/docs-site/tests/docs-search-behavior.test.mjs new file mode 100644 index 00000000..0a96482b --- /dev/null +++ b/docs-site/tests/docs-search-behavior.test.mjs @@ -0,0 +1,53 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +const docsSiteDir = join(dirname(fileURLToPath(import.meta.url)), ".."); + +async function readDocsFile(path) { + return readFile(join(docsSiteDir, path), "utf8"); +} + +test("root provider uses the base-path-aware search API", async () => { + const layout = await readDocsFile("app/layout.tsx"); + + assert.match(layout, /search=\{\{/); + assert.match(layout, /api:\s*"\/ktx\/api\/search"/); +}); + +test("site background stacking does not target every body child", async () => { + const css = await readDocsFile("app/global.css"); + + assert.doesNotMatch(css, /body\s*>\s*\*\s*\{[^}]*z-index/s); + assert.match(css, /\.ktx-site-shell\s*\{[^}]*z-index:\s*2/s); +}); + +test("search lock relies on body overflow propagation, not html or sidebar overrides", async () => { + const css = await readDocsFile("app/global.css"); + + // Body still clips horizontal overflow defensively. + assert.match(css, /(^|\s)body\s*\{[^}]*overflow-x:\s*clip/s); + + // html must keep its default `visible` overflow so body's lock + // (`overflow: hidden` from react-remove-scroll-bar) propagates to the + // viewport. Locking html directly breaks `position: sticky` on the + // sidebar placeholder. + assert.doesNotMatch(css, /(^|\s)html\s*,?\s*\{[^}]*overflow(-y|\s*:)\s*(hidden|clip)/s); + assert.doesNotMatch( + css, + /html:has\(body\[data-scroll-locked\]\)[^{]*\{[^}]*overflow:\s*(hidden|clip)/s, + ); + + // No site-specific overrides to body's data-scroll-locked overflow or + // to the sidebar placeholder when locked. + assert.doesNotMatch( + css, + /html\s+body\[data-scroll-locked\][^{]*\{[^}]*overflow:/s, + ); + assert.doesNotMatch( + css, + /body\[data-scroll-locked\]\s+\[data-sidebar-placeholder\][^{]*\{[^}]*position:\s*fixed/s, + ); +}); diff --git a/docs-site/tests/product-mechanics-content.test.mjs b/docs-site/tests/product-mechanics-content.test.mjs index bf03abcc..81d716d3 100644 --- a/docs-site/tests/product-mechanics-content.test.mjs +++ b/docs-site/tests/product-mechanics-content.test.mjs @@ -23,7 +23,7 @@ test("docs introduction frames the concept before showing product mechanics", as const heroIndex = introduction.indexOf("Make analytics context"); const whyIndex = introduction.indexOf("## Why KTX"); - const createsIndex = introduction.indexOf("## What KTX creates"); + const worksIndex = introduction.indexOf("## How KTX works"); const mechanicsIndex = introduction.indexOf(""); const useCaseIndex = introduction.indexOf("## Use it for"); const heroSource = introduction.slice(0, mechanicsIndex); @@ -34,12 +34,12 @@ test("docs introduction frames the concept before showing product mechanics", as "problem framing should appear after the hero", ); assert.ok( - createsIndex > whyIndex, - "artifact summary should appear after problem framing", + worksIndex > whyIndex, + "mechanics bridge should appear after problem framing", ); assert.ok( - mechanicsIndex > createsIndex, - "mechanics component should appear after the artifact summary", + mechanicsIndex > worksIndex, + "mechanics component should appear after the mechanics bridge", ); assert.ok( mechanicsIndex < useCaseIndex, @@ -49,49 +49,47 @@ 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", + "Any text", + "compile into SQL", + '"use client"', + "@xyflow/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: {}