mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
docs: rewrite context-as-code as reviewing-context guide
Move the page from Concepts to Guides and rebuild around an interactive review-loop diagram. Extract pan/zoom + fit-view controls into a shared FlowCanvas wrapper and adopt it across all three docs diagrams.
This commit is contained in:
parent
a1cfb03d73
commit
bb455ed5ce
13 changed files with 1059 additions and 285 deletions
|
|
@ -1035,6 +1035,49 @@ body::after {
|
|||
51%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Flow canvas (shared ReactFlow diagram styles)
|
||||
═══════════════════════════════════════════ */
|
||||
.flow-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: default;
|
||||
pointer-events: all !important;
|
||||
}
|
||||
.flow-canvas .react-flow__node > * {
|
||||
pointer-events: auto;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
.flow-canvas .react-flow__node.selected,
|
||||
.flow-canvas .react-flow__node:focus,
|
||||
.flow-canvas .react-flow__node:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.flow-canvas .react-flow__pane {
|
||||
cursor: grab;
|
||||
}
|
||||
.flow-canvas .react-flow__pane:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.flow-canvas .react-flow__handle {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Reduced motion
|
||||
═══════════════════════════════════════════ */
|
||||
|
|
|
|||
704
docs-site/components/context-review-loop.tsx
Normal file
704
docs-site/components/context-review-loop.tsx
Normal file
|
|
@ -0,0 +1,704 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Handle,
|
||||
MarkerType,
|
||||
type Node,
|
||||
type NodeProps,
|
||||
Position,
|
||||
} from "@xyflow/react";
|
||||
|
||||
import { FlowCanvas } from "./flow-canvas";
|
||||
|
||||
type SourcesNodeData = {
|
||||
variant: "sources";
|
||||
badge: string;
|
||||
title: string;
|
||||
caption: string;
|
||||
items: Array<{ label: string; color: string }>;
|
||||
};
|
||||
|
||||
type IngestNodeData = {
|
||||
variant: "ingest";
|
||||
badge: string;
|
||||
title: string;
|
||||
command: string;
|
||||
caption: string;
|
||||
};
|
||||
|
||||
type DiffFileLine = { kind: "add" | "del" | "ctx" | "hunk"; text: string };
|
||||
type DiffFile = {
|
||||
path: string;
|
||||
accent: string;
|
||||
added: number;
|
||||
removed: number;
|
||||
lines: DiffFileLine[];
|
||||
};
|
||||
|
||||
type DiffNodeData = {
|
||||
variant: "diff";
|
||||
badge: string;
|
||||
title: string;
|
||||
caption: string;
|
||||
branch: string;
|
||||
files: DiffFile[];
|
||||
};
|
||||
|
||||
type ReviewNodeData = {
|
||||
variant: "review";
|
||||
badge: string;
|
||||
title: string;
|
||||
caption: string;
|
||||
checks: string[];
|
||||
};
|
||||
|
||||
type MergedNodeData = {
|
||||
variant: "merged";
|
||||
badge: string;
|
||||
title: string;
|
||||
caption: string;
|
||||
paths: Array<{ label: string; color: string }>;
|
||||
};
|
||||
|
||||
type SourcesNode = Node<SourcesNodeData, "sources">;
|
||||
type IngestNode = Node<IngestNodeData, "ingest">;
|
||||
type DiffNode = Node<DiffNodeData, "diff">;
|
||||
type ReviewNode = Node<ReviewNodeData, "review">;
|
||||
type MergedNode = Node<MergedNodeData, "merged">;
|
||||
type FlowNode = SourcesNode | IngestNode | DiffNode | ReviewNode | MergedNode;
|
||||
|
||||
const NODE_W = 420;
|
||||
const STD_H = 196;
|
||||
const DIFF_H = 472;
|
||||
|
||||
const CENTER_X = 220;
|
||||
const GAP = 72;
|
||||
|
||||
const SOURCES_Y = 16;
|
||||
const INGEST_Y = SOURCES_Y + STD_H + GAP;
|
||||
const DIFF_Y = INGEST_Y + STD_H + GAP;
|
||||
const REVIEW_Y = DIFF_Y + DIFF_H + GAP;
|
||||
const MERGED_Y = REVIEW_Y + STD_H + GAP;
|
||||
const CANVAS_BOTTOM = MERGED_Y + STD_H + 16;
|
||||
|
||||
const FORWARD_STROKE = "#0891b2";
|
||||
const FEEDBACK_STROKE = "#94a3b8";
|
||||
|
||||
const sourcesNode: SourcesNode = {
|
||||
id: "sources",
|
||||
type: "sources",
|
||||
position: { x: CENTER_X, y: SOURCES_Y },
|
||||
data: {
|
||||
variant: "sources",
|
||||
badge: "1 · evidence",
|
||||
title: "Data stack",
|
||||
caption: "Connectors scan warehouses, modeling code, BI tools, and notes.",
|
||||
items: [
|
||||
{ label: "warehouse", color: "#3b82f6" },
|
||||
{ label: "dbt", color: "#f59e0b" },
|
||||
{ label: "Metabase", color: "#f97316" },
|
||||
{ label: "Notion", color: "#10b981" },
|
||||
],
|
||||
},
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
const ingestNode: IngestNode = {
|
||||
id: "ingest",
|
||||
type: "ingest",
|
||||
position: { x: CENTER_X, y: INGEST_Y },
|
||||
data: {
|
||||
variant: "ingest",
|
||||
badge: "2 · run",
|
||||
title: "ktx ingest",
|
||||
command: "ktx ingest --all",
|
||||
caption:
|
||||
"Reconciles new evidence with the accepted YAML and Markdown already on disk.",
|
||||
},
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
const diffNode: DiffNode = {
|
||||
id: "diff",
|
||||
type: "diff",
|
||||
position: { x: CENTER_X, y: DIFF_Y },
|
||||
data: {
|
||||
variant: "diff",
|
||||
badge: "3 · diff",
|
||||
title: "Branch diff",
|
||||
caption: "Every decision lands as a YAML or Markdown line.",
|
||||
branch: "ingest/nightly",
|
||||
files: [
|
||||
{
|
||||
path: "semantic-layer/warehouse/orders.yaml",
|
||||
accent: "#3b82f6",
|
||||
added: 4,
|
||||
removed: 1,
|
||||
lines: [
|
||||
{ kind: "hunk", text: "@@ measures @@" },
|
||||
{ kind: "ctx", text: " - name: revenue" },
|
||||
{ kind: "del", text: " expr: sum(amount)" },
|
||||
{ kind: "add", text: " expr: sum(amount - refund_amount)" },
|
||||
{ kind: "add", text: " - name: net_orders" },
|
||||
{ kind: "add", text: " expr: count(distinct id)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "wiki/global/revenue.md",
|
||||
accent: "#10b981",
|
||||
added: 2,
|
||||
removed: 0,
|
||||
lines: [
|
||||
{ kind: "hunk", text: "@@ Net revenue @@" },
|
||||
{ kind: "add", text: "Excludes refunds and test accounts." },
|
||||
{ kind: "add", text: "sl_refs: [warehouse.orders]" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
const reviewNode: ReviewNode = {
|
||||
id: "review",
|
||||
type: "review",
|
||||
position: { x: CENTER_X, y: REVIEW_Y },
|
||||
data: {
|
||||
variant: "review",
|
||||
badge: "4 · review",
|
||||
title: "PR review",
|
||||
caption: "Analysts approve, edit, or reject like any pull request.",
|
||||
checks: ["joins are safe", "measures match policy", "wiki cites evidence"],
|
||||
},
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
const mergedNode: MergedNode = {
|
||||
id: "merged",
|
||||
type: "merged",
|
||||
position: { x: CENTER_X, y: MERGED_Y },
|
||||
data: {
|
||||
variant: "merged",
|
||||
badge: "5 · merged",
|
||||
title: "Accepted context",
|
||||
caption: "Merged files become the trusted layer agents read at runtime.",
|
||||
paths: [
|
||||
{ label: "semantic-layer/", color: "#3b82f6" },
|
||||
{ label: "wiki/", color: "#10b981" },
|
||||
],
|
||||
},
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
const nodes: FlowNode[] = [
|
||||
sourcesNode,
|
||||
ingestNode,
|
||||
diffNode,
|
||||
reviewNode,
|
||||
mergedNode,
|
||||
];
|
||||
|
||||
const arrowMarker = (color: string) => ({
|
||||
type: MarkerType.ArrowClosed,
|
||||
color,
|
||||
width: 14,
|
||||
height: 14,
|
||||
});
|
||||
|
||||
const forwardLabelStyle = {
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
fill: "var(--color-fd-muted-foreground)",
|
||||
letterSpacing: "0.02em",
|
||||
} as const;
|
||||
|
||||
const forwardLabelBg = {
|
||||
fill: "var(--color-fd-background)",
|
||||
stroke: "var(--color-fd-border)",
|
||||
strokeWidth: 1,
|
||||
} as const;
|
||||
|
||||
const edges = [
|
||||
{
|
||||
id: "sources-ingest",
|
||||
source: "sources",
|
||||
sourceHandle: "bottom",
|
||||
target: "ingest",
|
||||
targetHandle: "top",
|
||||
type: "straight" as const,
|
||||
label: "scan",
|
||||
labelBgPadding: [6, 3] as [number, number],
|
||||
labelBgBorderRadius: 4,
|
||||
labelStyle: forwardLabelStyle,
|
||||
labelBgStyle: forwardLabelBg,
|
||||
style: { stroke: FORWARD_STROKE, strokeWidth: 1.75 },
|
||||
markerEnd: arrowMarker(FORWARD_STROKE),
|
||||
},
|
||||
{
|
||||
id: "ingest-diff",
|
||||
source: "ingest",
|
||||
sourceHandle: "bottom",
|
||||
target: "diff",
|
||||
targetHandle: "top",
|
||||
type: "straight" as const,
|
||||
label: "propose files",
|
||||
labelBgPadding: [6, 3] as [number, number],
|
||||
labelBgBorderRadius: 4,
|
||||
labelStyle: forwardLabelStyle,
|
||||
labelBgStyle: forwardLabelBg,
|
||||
style: { stroke: FORWARD_STROKE, strokeWidth: 1.75 },
|
||||
markerEnd: arrowMarker(FORWARD_STROKE),
|
||||
},
|
||||
{
|
||||
id: "diff-review",
|
||||
source: "diff",
|
||||
sourceHandle: "bottom",
|
||||
target: "review",
|
||||
targetHandle: "top",
|
||||
type: "straight" as const,
|
||||
label: "open PR",
|
||||
labelBgPadding: [6, 3] as [number, number],
|
||||
labelBgBorderRadius: 4,
|
||||
labelStyle: forwardLabelStyle,
|
||||
labelBgStyle: forwardLabelBg,
|
||||
style: { stroke: FORWARD_STROKE, strokeWidth: 1.75 },
|
||||
markerEnd: arrowMarker(FORWARD_STROKE),
|
||||
},
|
||||
{
|
||||
id: "review-merged",
|
||||
source: "review",
|
||||
sourceHandle: "bottom",
|
||||
target: "merged",
|
||||
targetHandle: "top",
|
||||
type: "straight" as const,
|
||||
label: "merge",
|
||||
labelBgPadding: [6, 3] as [number, number],
|
||||
labelBgBorderRadius: 4,
|
||||
labelStyle: forwardLabelStyle,
|
||||
labelBgStyle: forwardLabelBg,
|
||||
style: { stroke: FORWARD_STROKE, strokeWidth: 1.75 },
|
||||
markerEnd: arrowMarker(FORWARD_STROKE),
|
||||
},
|
||||
{
|
||||
id: "merged-sources",
|
||||
source: "merged",
|
||||
sourceHandle: "right",
|
||||
target: "sources",
|
||||
targetHandle: "right",
|
||||
type: "smoothstep" as const,
|
||||
pathOptions: { offset: 64, borderRadius: 18 },
|
||||
style: {
|
||||
stroke: FEEDBACK_STROKE,
|
||||
strokeWidth: 1.5,
|
||||
strokeDasharray: "5 5",
|
||||
},
|
||||
markerEnd: arrowMarker(FEEDBACK_STROKE),
|
||||
},
|
||||
];
|
||||
|
||||
function BadgePill({
|
||||
tone,
|
||||
children,
|
||||
}: {
|
||||
tone: "neutral" | "primary" | "review" | "merged";
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const cls =
|
||||
tone === "primary"
|
||||
? "border-cyan-300/70 bg-cyan-50 text-cyan-800 dark:border-cyan-400/40 dark:bg-cyan-400/15 dark:text-cyan-100"
|
||||
: tone === "review"
|
||||
? "border-fuchsia-300/70 bg-fuchsia-50 text-fuchsia-800 dark:border-fuchsia-400/40 dark:bg-fuchsia-400/15 dark:text-fuchsia-100"
|
||||
: tone === "merged"
|
||||
? "border-emerald-300/70 bg-emerald-50 text-emerald-800 dark:border-emerald-400/40 dark:bg-emerald-400/15 dark:text-emerald-100"
|
||||
: "border-slate-300 bg-slate-50 text-slate-700 dark:border-slate-600/60 dark:bg-slate-700/40 dark:text-slate-200";
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 rounded-sm border px-2 font-mono text-[13px] font-semibold uppercase tracking-[0.06em] ${cls}`}
|
||||
style={{ lineHeight: "20px", paddingBlock: 0 }}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function FlowHandles({ noTop = false }: { noTop?: boolean }) {
|
||||
return (
|
||||
<>
|
||||
{!noTop ? (
|
||||
<Handle
|
||||
id="top"
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="!opacity-0"
|
||||
/>
|
||||
) : null}
|
||||
<Handle
|
||||
id="bottom"
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
className="!opacity-0"
|
||||
/>
|
||||
<Handle
|
||||
id="right"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!opacity-0"
|
||||
/>
|
||||
<Handle
|
||||
id="right-target"
|
||||
type="target"
|
||||
position={Position.Right}
|
||||
className="!opacity-0"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SourcesHandles() {
|
||||
return (
|
||||
<>
|
||||
<Handle
|
||||
id="bottom"
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
className="!opacity-0"
|
||||
/>
|
||||
<Handle
|
||||
id="right"
|
||||
type="target"
|
||||
position={Position.Right}
|
||||
className="!opacity-0"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SourcesNodeView({ data }: NodeProps<SourcesNode>) {
|
||||
return (
|
||||
<div
|
||||
style={{ width: NODE_W, height: STD_H }}
|
||||
className="flex flex-col rounded-md border border-fd-border bg-fd-card px-4 py-3.5 shadow-sm"
|
||||
>
|
||||
<SourcesHandles />
|
||||
<BadgePill tone="neutral">{data.badge}</BadgePill>
|
||||
<p className="mt-2 text-[19px] font-semibold leading-7 text-fd-foreground">
|
||||
{data.title}
|
||||
</p>
|
||||
<p className="mt-1.5 text-[15px] leading-6 text-fd-muted-foreground">
|
||||
{data.caption}
|
||||
</p>
|
||||
<div className="mt-auto flex flex-wrap gap-1.5 pt-2.5">
|
||||
{data.items.map((item) => (
|
||||
<span
|
||||
key={item.label}
|
||||
className="inline-flex items-center gap-1.5 rounded border border-fd-border bg-fd-background px-2 py-0.5 text-[13px] leading-5 text-fd-muted-foreground"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-1.5 w-1.5 flex-none rounded-full"
|
||||
style={{ background: item.color }}
|
||||
/>
|
||||
{item.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IngestNodeView({ data }: NodeProps<IngestNode>) {
|
||||
return (
|
||||
<div
|
||||
style={{ width: NODE_W, height: STD_H }}
|
||||
className="relative flex flex-col rounded-md border border-cyan-300/40 bg-[#0f1f23] px-4 py-3.5 text-white shadow-sm dark:bg-[#0b181b]"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-y-0 left-0 w-[3px] rounded-l-md"
|
||||
style={{ background: FORWARD_STROKE }}
|
||||
/>
|
||||
<FlowHandles />
|
||||
<div className="flex items-center gap-2.5">
|
||||
<BadgePill tone="primary">{data.badge}</BadgePill>
|
||||
<p className="text-[19px] font-semibold leading-7 text-white">
|
||||
{data.title}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className="mt-2 inline-flex w-fit items-center whitespace-pre rounded-sm border border-cyan-100/15 bg-white/[0.08] px-2 py-0.5 font-mono text-[14px] leading-5 text-cyan-100"
|
||||
style={{
|
||||
fontVariantLigatures: "none",
|
||||
fontFeatureSettings: '"liga" 0, "calt" 0',
|
||||
}}
|
||||
>
|
||||
{`$ ${data.command}`}
|
||||
</span>
|
||||
<p className="mt-2 text-[15px] leading-6 text-cyan-50/80">
|
||||
{data.caption}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiffLine({ line }: { line: DiffFileLine }) {
|
||||
if (line.kind === "hunk") {
|
||||
return (
|
||||
<div className="bg-fd-muted/40 px-2.5 py-1 font-mono text-[12px] uppercase tracking-[0.06em] text-fd-muted-foreground">
|
||||
{line.text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const symbol = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
|
||||
const cls =
|
||||
line.kind === "add"
|
||||
? "bg-emerald-500/8 text-emerald-700 dark:text-emerald-300"
|
||||
: line.kind === "del"
|
||||
? "bg-rose-500/8 text-rose-700 dark:text-rose-300"
|
||||
: "text-fd-muted-foreground";
|
||||
return (
|
||||
<div
|
||||
className={`flex gap-1.5 px-2.5 py-px font-mono text-[13px] leading-5 ${cls}`}
|
||||
>
|
||||
<span aria-hidden="true" className="w-2.5 flex-none text-center opacity-70">
|
||||
{symbol}
|
||||
</span>
|
||||
<span className="min-w-0 truncate">{line.text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiffFileBlock({ file }: { file: DiffFile }) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-sm border border-fd-border bg-fd-background">
|
||||
<div
|
||||
className="flex items-center gap-2 border-b border-fd-border px-2.5 py-1.5"
|
||||
style={{ borderTop: `2px solid ${file.accent}` }}
|
||||
>
|
||||
<span
|
||||
className="truncate font-mono text-[14px] font-semibold tracking-tight"
|
||||
style={{ color: file.accent }}
|
||||
>
|
||||
{file.path}
|
||||
</span>
|
||||
<span className="ml-auto flex-none font-mono text-[12px] tabular-nums text-emerald-600 dark:text-emerald-400">
|
||||
+{file.added}
|
||||
</span>
|
||||
{file.removed > 0 ? (
|
||||
<span className="flex-none font-mono text-[12px] tabular-nums text-rose-600 dark:text-rose-400">
|
||||
-{file.removed}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="py-1">
|
||||
{file.lines.map((line, idx) => (
|
||||
<DiffLine key={idx} line={line} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiffNodeView({ data }: NodeProps<DiffNode>) {
|
||||
return (
|
||||
<div
|
||||
style={{ width: NODE_W, height: DIFF_H }}
|
||||
className="flex flex-col rounded-md border-2 border-fd-primary/45 bg-fd-card px-4 py-3.5 shadow-md"
|
||||
>
|
||||
<FlowHandles />
|
||||
<div className="flex items-center gap-2.5">
|
||||
<BadgePill tone="primary">{data.badge}</BadgePill>
|
||||
<p className="text-[19px] font-semibold leading-7 text-fd-foreground">
|
||||
{data.title}
|
||||
</p>
|
||||
<span
|
||||
className="ml-auto inline-flex w-fit items-center gap-1.5 whitespace-nowrap rounded border border-fd-border bg-fd-background px-2 py-0.5 font-mono text-[13px] leading-5 text-fd-muted-foreground"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="6" cy="6" r="2.25" />
|
||||
<circle cx="6" cy="18" r="2.25" />
|
||||
<circle cx="18" cy="6" r="2.25" />
|
||||
<path d="M6 8.5v7" />
|
||||
<path d="M6 18c0-6 12-6 12-9.5" />
|
||||
</svg>
|
||||
{data.branch}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1.5 text-[15px] leading-6 text-fd-muted-foreground">
|
||||
{data.caption}
|
||||
</p>
|
||||
<div className="mt-2.5 flex min-h-0 flex-1 flex-col gap-2">
|
||||
{data.files.map((file) => (
|
||||
<DiffFileBlock key={file.path} file={file} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReviewNodeView({ data }: NodeProps<ReviewNode>) {
|
||||
return (
|
||||
<div
|
||||
style={{ width: NODE_W, height: STD_H }}
|
||||
className="flex flex-col rounded-md border border-fd-border bg-fd-card px-4 py-3.5 shadow-sm"
|
||||
>
|
||||
<FlowHandles />
|
||||
<div className="flex items-center gap-2.5">
|
||||
<BadgePill tone="review">{data.badge}</BadgePill>
|
||||
<p className="text-[19px] font-semibold leading-7 text-fd-foreground">
|
||||
{data.title}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-1.5 text-[15px] leading-6 text-fd-muted-foreground">
|
||||
{data.caption}
|
||||
</p>
|
||||
<ul className="mt-auto flex flex-wrap gap-x-4 gap-y-1.5 pt-2.5">
|
||||
{data.checks.map((check) => (
|
||||
<li
|
||||
key={check}
|
||||
className="inline-flex items-center gap-1.5 text-[13px] leading-5 text-fd-muted-foreground"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-3.5 w-3.5 flex-none text-fuchsia-500"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="4 12 10 18 20 6" />
|
||||
</svg>
|
||||
<span>{check}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MergedNodeView({ data }: NodeProps<MergedNode>) {
|
||||
return (
|
||||
<div
|
||||
style={{ width: NODE_W, height: STD_H }}
|
||||
className="flex flex-col rounded-md border border-emerald-300/55 bg-fd-card px-4 py-3.5 shadow-sm"
|
||||
>
|
||||
<FlowHandles />
|
||||
<div className="flex items-center gap-2.5">
|
||||
<BadgePill tone="merged">{data.badge}</BadgePill>
|
||||
<p className="text-[19px] font-semibold leading-7 text-fd-foreground">
|
||||
{data.title}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-1.5 text-[15px] leading-6 text-fd-muted-foreground">
|
||||
{data.caption}
|
||||
</p>
|
||||
<div className="mt-auto flex flex-wrap gap-4 pt-2.5">
|
||||
{data.paths.map((path) => (
|
||||
<span
|
||||
key={path.label}
|
||||
className="inline-flex items-center gap-1.5 font-mono text-[15px] font-semibold tracking-tight"
|
||||
style={{ color: path.color }}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-2 w-2 flex-none rounded-full"
|
||||
style={{ background: path.color }}
|
||||
/>
|
||||
{path.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const nodeTypes = {
|
||||
sources: SourcesNodeView,
|
||||
ingest: IngestNodeView,
|
||||
diff: DiffNodeView,
|
||||
review: ReviewNodeView,
|
||||
merged: MergedNodeView,
|
||||
};
|
||||
|
||||
export function ContextReviewLoop() {
|
||||
return (
|
||||
<section
|
||||
id="review-loop"
|
||||
className="not-prose my-10 w-full max-w-full min-w-0 scroll-mt-24"
|
||||
aria-labelledby="review-loop-title"
|
||||
>
|
||||
<article
|
||||
className="max-w-full min-w-0 overflow-hidden rounded-lg border border-fd-border bg-fd-card shadow-sm"
|
||||
aria-label="The ktx context review loop"
|
||||
>
|
||||
<div className="border-b border-fd-border bg-fd-muted/35 px-5 py-4">
|
||||
<a
|
||||
href="#review-loop"
|
||||
className="group/anchor inline-flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-[0.08em] text-fd-primary transition-colors hover:text-fd-primary/80"
|
||||
>
|
||||
The review loop
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="opacity-0 transition-opacity duration-150 group-hover/anchor:opacity-100 group-focus-visible/anchor:opacity-100"
|
||||
>
|
||||
#
|
||||
</span>
|
||||
</a>
|
||||
<h3
|
||||
id="review-loop-title"
|
||||
className="mt-1 text-base font-semibold tracking-normal text-fd-foreground sm:text-lg"
|
||||
style={{ fontFamily: "var(--font-display)" }}
|
||||
>
|
||||
Every ingest is a diff you can refuse
|
||||
</h3>
|
||||
<p className="mt-2 max-w-3xl text-xs leading-5 text-fd-muted-foreground">
|
||||
Evidence becomes file changes. File changes become a PR. The PR
|
||||
merges into the layer agents will read tomorrow, and what you
|
||||
merged today becomes the baseline for the next run.
|
||||
</p>
|
||||
<p className="mt-2 inline-flex items-center gap-1.5 text-[10.5px] font-medium uppercase tracking-[0.06em] text-fd-muted-foreground">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="inline-block h-px w-6"
|
||||
style={{
|
||||
borderTop: `1.5px dashed ${FEEDBACK_STROKE}`,
|
||||
}}
|
||||
/>
|
||||
dashed line: merged files feed the next ingest
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FlowCanvas
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
canvasStyle={{
|
||||
height: "min(1080px, 160vw)",
|
||||
minHeight: 760,
|
||||
}}
|
||||
translateExtent={[
|
||||
[-160, -120],
|
||||
[CENTER_X + NODE_W + 320, CANVAS_BOTTOM + 120],
|
||||
]}
|
||||
ariaLabel="ktx context review loop diagram"
|
||||
/>
|
||||
</article>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
118
docs-site/components/flow-canvas.tsx
Normal file
118
docs-site/components/flow-canvas.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Controls,
|
||||
type Edge,
|
||||
type EdgeTypes,
|
||||
type FitViewOptions,
|
||||
type Node,
|
||||
type NodeTypes,
|
||||
type OnInit,
|
||||
ReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
|
||||
type FlowCanvasProps<TNode extends Node, TEdge extends Edge> = {
|
||||
nodes: TNode[];
|
||||
edges: TEdge[];
|
||||
nodeTypes?: NodeTypes;
|
||||
edgeTypes?: EdgeTypes;
|
||||
/** Inline style for the canvas wrapper (height, minHeight, etc.). */
|
||||
canvasStyle: React.CSSProperties;
|
||||
/** Extra class on the canvas wrapper (the `flow-canvas` class is always
|
||||
* applied). Use it to scope per-diagram styles. */
|
||||
className?: string;
|
||||
fitViewOptions?: FitViewOptions<TNode>;
|
||||
maxZoom?: number;
|
||||
translateExtent?: [[number, number], [number, number]];
|
||||
ariaLabel?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_FIT_VIEW = { padding: 0.05 } satisfies FitViewOptions;
|
||||
|
||||
/**
|
||||
* Shared ReactFlow wrapper for docs diagrams.
|
||||
*
|
||||
* Behavior:
|
||||
* - Drag-to-pan, pinch-to-zoom, double-click-to-zoom.
|
||||
* - Scroll wheel passes through to the page (zoomOnScroll/panOnScroll off).
|
||||
* - On mount, the view is fitted and `minZoom` is locked to the fitted zoom
|
||||
* so the user can zoom in but not out beyond the initial framing.
|
||||
* - Nodes are non-draggable, non-selectable, non-focusable — the diagram is
|
||||
* a static read-only artifact.
|
||||
* - Common CSS lives in `global.css` under the `.flow-canvas` selector; the
|
||||
* per-diagram `className` adds anything else specific to that diagram.
|
||||
*/
|
||||
export function FlowCanvas<TNode extends Node, TEdge extends Edge>({
|
||||
nodes,
|
||||
edges,
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
canvasStyle,
|
||||
className,
|
||||
fitViewOptions = DEFAULT_FIT_VIEW,
|
||||
maxZoom = 1.5,
|
||||
translateExtent,
|
||||
ariaLabel,
|
||||
}: FlowCanvasProps<TNode, TEdge>) {
|
||||
const [minZoom, setMinZoom] = useState(0.15);
|
||||
const handleInit = useCallback<OnInit<TNode, TEdge>>(
|
||||
(instance) => {
|
||||
requestAnimationFrame(() => {
|
||||
void instance.fitView(fitViewOptions).then(() => {
|
||||
setMinZoom(instance.getZoom());
|
||||
});
|
||||
});
|
||||
},
|
||||
[fitViewOptions],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flow-canvas relative bg-fd-background ${className ?? ""}`}
|
||||
style={canvasStyle}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className="pointer-events-none absolute right-2.5 top-2.5 z-10 rounded border border-fd-border/50 bg-white/30 px-1.5 py-px font-mono text-[9.5px] font-medium uppercase tracking-[0.06em] text-fd-muted-foreground shadow-sm backdrop-blur-sm dark:bg-white/10">
|
||||
Drag to pan · ⌘/Ctrl + scroll to zoom
|
||||
</div>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onInit={handleInit}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
nodesFocusable={false}
|
||||
edgesFocusable={false}
|
||||
elementsSelectable={false}
|
||||
panOnDrag
|
||||
panOnScroll={false}
|
||||
zoomOnScroll={false}
|
||||
zoomOnPinch
|
||||
zoomOnDoubleClick
|
||||
preventScrolling={false}
|
||||
minZoom={minZoom}
|
||||
maxZoom={maxZoom}
|
||||
translateExtent={translateExtent}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background
|
||||
variant={BackgroundVariant.Dots}
|
||||
gap={18}
|
||||
size={1}
|
||||
color="var(--color-fd-border)"
|
||||
/>
|
||||
<Controls
|
||||
showInteractive={false}
|
||||
position="bottom-right"
|
||||
aria-label="Zoom and fit-view controls"
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
type Edge,
|
||||
type EdgeProps,
|
||||
getSmoothStepPath,
|
||||
|
|
@ -11,11 +9,11 @@ import {
|
|||
type Node,
|
||||
type NodeProps,
|
||||
Position,
|
||||
ReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { FlowCanvas } from "./flow-canvas";
|
||||
|
||||
type SourceNodeData = {
|
||||
accent: string;
|
||||
body: string;
|
||||
|
|
@ -538,81 +536,21 @@ export function ProductMechanics() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mechanics-canvas bg-fd-background"
|
||||
style={{
|
||||
<FlowCanvas
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
canvasStyle={{
|
||||
height: "min(1240px, 170vw)",
|
||||
minHeight: 720,
|
||||
}}
|
||||
>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
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>
|
||||
className="mechanics-canvas"
|
||||
fitViewOptions={{ padding: 0.04 }}
|
||||
ariaLabel="ktx ingestion flow diagram"
|
||||
/>
|
||||
</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;
|
||||
}
|
||||
.mechanics-canvas .mechanics-particle {
|
||||
pointer-events: none;
|
||||
filter: drop-shadow(0 0 6px currentColor);
|
||||
|
|
|
|||
|
|
@ -2,17 +2,14 @@
|
|||
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Handle,
|
||||
MarkerType,
|
||||
type Node,
|
||||
type NodeProps,
|
||||
type OnInit,
|
||||
Position,
|
||||
ReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
|
||||
import { FlowCanvas } from "./flow-canvas";
|
||||
|
||||
type LaneVariant = "manual" | "ktx";
|
||||
|
||||
|
|
@ -472,8 +469,6 @@ const edges = [
|
|||
},
|
||||
];
|
||||
|
||||
type FlowEdge = (typeof edges)[number];
|
||||
|
||||
function AgentNodeView({ data }: NodeProps<AgentNode>) {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -980,15 +975,6 @@ const nodeTypes = {
|
|||
};
|
||||
|
||||
export function SemanticLayerFlow() {
|
||||
const [minZoom, setMinZoom] = useState(0.2);
|
||||
const handleFlowInit = useCallback<OnInit<FlowNode, FlowEdge>>((instance) => {
|
||||
requestAnimationFrame(() => {
|
||||
void instance.fitView(FIT_VIEW_OPTIONS).then(() => {
|
||||
setMinZoom(instance.getZoom());
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="imperative-vs-declarative"
|
||||
|
|
@ -1027,85 +1013,20 @@ export function SemanticLayerFlow() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="sl-flow-canvas relative bg-fd-background"
|
||||
style={{
|
||||
<FlowCanvas
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
canvasStyle={{
|
||||
height: "min(2340px, 290vw)",
|
||||
minHeight: 1780,
|
||||
}}
|
||||
>
|
||||
<div className="pointer-events-none absolute right-2.5 top-2.5 z-10 rounded border border-fd-border/50 bg-white/30 px-1.5 py-px font-mono text-[9.5px] font-medium uppercase tracking-[0.06em] text-fd-muted-foreground shadow-sm backdrop-blur-sm dark:bg-white/10">
|
||||
Drag to pan • ⌘/Ctrl + scroll to zoom
|
||||
</div>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
onInit={handleFlowInit}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
nodesFocusable={false}
|
||||
edgesFocusable={false}
|
||||
elementsSelectable={false}
|
||||
panOnDrag
|
||||
panOnScroll={false}
|
||||
zoomOnScroll={false}
|
||||
zoomOnPinch
|
||||
zoomOnDoubleClick
|
||||
preventScrolling={false}
|
||||
minZoom={minZoom}
|
||||
maxZoom={1.5}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background
|
||||
variant={BackgroundVariant.Dots}
|
||||
gap={18}
|
||||
size={1}
|
||||
color="var(--color-fd-border)"
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
className="sl-flow-canvas"
|
||||
fitViewOptions={FIT_VIEW_OPTIONS}
|
||||
ariaLabel="Semantic query to SQL flow diagram"
|
||||
/>
|
||||
</article>
|
||||
<style>{`
|
||||
.sl-flow-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;
|
||||
}
|
||||
.sl-flow-canvas .react-flow__node > * {
|
||||
pointer-events: auto;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
.sl-flow-canvas .react-flow__node.selected,
|
||||
.sl-flow-canvas .react-flow__node:focus,
|
||||
.sl-flow-canvas .react-flow__node:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.sl-flow-canvas .react-flow__pane {
|
||||
cursor: grab;
|
||||
}
|
||||
.sl-flow-canvas .react-flow__pane:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.sl-flow-canvas .react-flow__handle {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.sl-flow-canvas pre {
|
||||
font-size: 11.5px !important;
|
||||
line-height: 17.5px !important;
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
---
|
||||
title: Context as Code
|
||||
description: Treat analytics context like code - version it, review it, merge it.
|
||||
---
|
||||
|
||||
## The idea
|
||||
|
||||
dbt moved analytics transformations into git. **ktx** applies the same pattern to
|
||||
analytics context: metric definitions, joins, business rules, wiki pages, and
|
||||
ingest decisions become files that can be reviewed, merged, and audited.
|
||||
|
||||
| Before | With **ktx** |
|
||||
|--------|----------|
|
||||
| Context scattered across BI tools, chats, docs, and analyst memory | Context lives in YAML and Markdown |
|
||||
| Agent changes are hard to inspect | Agent changes are git diffs |
|
||||
| Imports overwrite local judgment | Ingest reconciles with existing files |
|
||||
| History depends on tool logs | History lives in commits and transcripts |
|
||||
|
||||
## Auto-ingestion
|
||||
|
||||
Most context already exists in dbt manifests, LookML, MetricFlow, Metabase,
|
||||
Notion, warehouse metadata, and analyst notes. **ktx** reads those inputs through
|
||||
connectors, then reconciles them into local files.
|
||||
|
||||
```text
|
||||
context sources -> connectors -> reconciliation agent -> YAML + Markdown diffs
|
||||
```
|
||||
|
||||
| Step | What happens | Output |
|
||||
|------|--------------|--------|
|
||||
| **Extract** | Connectors read models, metrics, questions, schemas, and docs | Structured metadata |
|
||||
| **Reconcile** | The agent compares incoming facts with existing context | Create, update, skip, or flag |
|
||||
| **Write** | **ktx** saves changed semantic sources and wiki pages | Reviewable project files |
|
||||
|
||||
Reconciliation is the key difference from a sync. **ktx** preserves accepted local
|
||||
edits, fills gaps, and surfaces conflicts instead of blindly overwriting files.
|
||||
|
||||
## The git workflow
|
||||
|
||||
Run ingestion on a branch, review the changed YAML and Markdown, then merge the
|
||||
accepted context the same way you merge dbt or application code.
|
||||
|
||||
```text
|
||||
dbt / BI / docs / warehouse
|
||||
|
|
||||
v
|
||||
ktx ingest --all
|
||||
|
|
||||
v
|
||||
branch: ingest/nightly
|
||||
|
|
||||
v
|
||||
semantic diff in PR
|
||||
|
|
||||
v
|
||||
approve and merge
|
||||
|
|
||||
v
|
||||
agents read updated files
|
||||
```
|
||||
|
||||
Typical review checklist:
|
||||
|
||||
- new sources match the warehouse and source-tool evidence;
|
||||
- joins have the right relationship direction;
|
||||
- generated measures match business definitions;
|
||||
- wiki pages capture caveats without duplicating YAML;
|
||||
- `.ktx/` runtime state stays out of git unless your team intentionally reviews
|
||||
a report or transcript.
|
||||
|
||||
Teams often run ingestion on demand during setup, then schedule
|
||||
`ktx ingest --all --no-input` on an ingest branch once the source is stable.
|
||||
|
||||
## Feedback loops
|
||||
|
||||
Context improves when human corrections and agent signals flow back into the
|
||||
same reviewed files.
|
||||
|
||||
| Signal | Example | Where it lands |
|
||||
|--------|---------|----------------|
|
||||
| Analyst correction | A measure excludes test accounts | `semantic-layer/**/*.yaml` |
|
||||
| Business clarification | ARR changed definition this quarter | `wiki/**/*.md` |
|
||||
| Agent query issue | A filter returns no rows unexpectedly | Wiki caveat or tighter source filter |
|
||||
| Join problem | A path duplicates order-level measures | Relationship metadata or grain fix |
|
||||
|
||||
Accepted corrections become input to the next ingest run. That makes the
|
||||
context layer converge toward the team's current source of truth.
|
||||
|
||||
## Deterministic replay
|
||||
|
||||
Every ingestion session records the connector inputs, tool calls, LLM responses,
|
||||
write decisions, and reasoning behind each change.
|
||||
|
||||
| Use case | What replay gives you |
|
||||
|----------|-----------------------|
|
||||
| **Debugging** | Trace a bad source, join, or measure back to the input that produced it |
|
||||
| **Trust** | Show where a definition came from and who reviewed the resulting diff |
|
||||
| **Reproducibility** | Compare old and new ingest behavior after config or model changes |
|
||||
|
||||
Commit the YAML and Markdown changes. Commit reports or transcripts only when
|
||||
they are part of your team's review workflow.
|
||||
|
||||
## Agent usage notes
|
||||
|
||||
Use this page when an agent needs to explain review workflows, ingestion diffs,
|
||||
replayability, or why **ktx** writes YAML and Markdown instead of hiding context in
|
||||
a hosted service.
|
||||
|
||||
| Agent task | Relevant section | Next page |
|
||||
|------------|------------------|-----------|
|
||||
| Explain how generated context should be reviewed | The git workflow | [Building Context](/docs/guides/building-context) |
|
||||
| Diagnose why ingestion changed a semantic source | Auto-ingestion / Deterministic replay | [ktx ingest](/docs/cli-reference/ktx-ingest) |
|
||||
| Explain how context improves over time | Feedback loops | [Building Context](/docs/guides/building-context) |
|
||||
| Tell a user what to commit | The git workflow | [Writing Context](/docs/guides/writing-context) |
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"title": "Concepts",
|
||||
"defaultOpen": true,
|
||||
"pages": ["the-context-layer", "semantic-layer-internals", "wiki-retrieval", "context-as-code"]
|
||||
"pages": ["the-context-layer", "semantic-layer-internals", "wiki-retrieval"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -337,4 +337,4 @@ different from what the agent first proposed.
|
|||
| Describe what the planner does between query and SQL | What the planner does | [ktx sl](/docs/cli-reference/ktx-sl) |
|
||||
| Explain why **ktx** asks for grain and relationship types | The join graph | [Writing context](/docs/guides/writing-context) |
|
||||
| Diagnose duplicated measures after a join | Fan-out and aggregate locality | [ktx sl](/docs/cli-reference/ktx-sl) |
|
||||
| Describe how semantic context stays current | Building and maintaining the graph | [Context as code](/docs/concepts/context-as-code) |
|
||||
| Describe how semantic context stays current | Building and maintaining the graph | [Reviewing Context](/docs/guides/reviewing-context) |
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ caveat stays anchored to the definition it explains.
|
|||
<span className="font-medium text-fd-foreground">{"Behind the scenes. "}</span>
|
||||
<strong className="font-medium text-fd-foreground">{"ktx"}</strong>
|
||||
{" also keeps scan snapshots and a per-run event log locally so every committed change is traceable to its evidence. You don't read or edit these files yourself - see "}
|
||||
<a href="/docs/concepts/context-as-code" className="font-medium underline">{"Context as Code"}</a>
|
||||
<a href="/docs/guides/reviewing-context" className="font-medium underline">{"Reviewing Context"}</a>
|
||||
{" for how that audit trail flows into review."}
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
|
@ -282,4 +282,4 @@ layers.
|
|||
| Explain why a data agent wrote a plausible but wrong query | Database access isn't enough | [Writing Context](/docs/guides/writing-context) |
|
||||
| Decide whether a fact belongs in YAML or Markdown | Semantic sources / Wiki pages | [Writing Context](/docs/guides/writing-context) |
|
||||
| Compare **ktx** to another semantic layer | How ktx compares | [Primary Sources](/docs/integrations/primary-sources) |
|
||||
| Explain reviewability and source of truth | A ktx project on disk | [Context as Code](/docs/concepts/context-as-code) |
|
||||
| Explain reviewability and source of truth | A ktx project on disk | [Reviewing Context](/docs/guides/reviewing-context) |
|
||||
|
|
|
|||
|
|
@ -277,4 +277,4 @@ stays in step with the semantic layer.
|
|||
| Decide whether to add a `refs` or `sl_refs` entry | The page graph | [Writing Context](/docs/guides/writing-context) |
|
||||
| Repair a wiki write rejected for missing references | Keeping the graph live | [Writing Context](/docs/guides/writing-context) |
|
||||
| Describe how historic SQL becomes a wiki page | Where the pages come from | [Building Context](/docs/guides/building-context) |
|
||||
| Explain raw-source provenance comments | Where the pages come from | [Context as Code](/docs/concepts/context-as-code) |
|
||||
| Explain raw-source provenance comments | Where the pages come from | [Reviewing Context](/docs/guides/reviewing-context) |
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"title": "Guides",
|
||||
"defaultOpen": true,
|
||||
"pages": ["building-context", "writing-context", "serving-agents", "llm-configuration"]
|
||||
"pages": ["building-context", "writing-context", "reviewing-context", "serving-agents", "llm-configuration"]
|
||||
}
|
||||
|
|
|
|||
164
docs-site/content/docs/guides/reviewing-context.mdx
Normal file
164
docs-site/content/docs/guides/reviewing-context.mdx
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
---
|
||||
title: Reviewing Context
|
||||
description: Treat ktx changes like code - review what each ingest writes, fix what's wrong, and merge the rest.
|
||||
---
|
||||
|
||||
import { ContextReviewLoop } from "@/components/context-review-loop";
|
||||
|
||||
When dbt put analytics transformations into git, it gave teams a way to argue
|
||||
about SQL before it ran in production. **ktx** does the same thing for the layer
|
||||
above transformations: metric definitions, joins, business rules, wiki pages,
|
||||
and the decisions an ingest agent makes all land as files you can read, diff,
|
||||
and merge.
|
||||
|
||||
This page covers the workflow:
|
||||
|
||||
- What `ktx ingest` writes to disk, and what it leaves alone.
|
||||
- The branch-and-PR loop you use to ship those changes.
|
||||
- The kinds of decisions you'll see in a diff.
|
||||
- How analyst fixes flow back into the next ingest.
|
||||
- How replay and provenance keep changes traceable.
|
||||
|
||||
## Why context belongs in git
|
||||
|
||||
A context layer that hides in a hosted UI is hard to audit. Agents write
|
||||
plausible YAML; analysts write quiet overrides; nobody can tell what changed
|
||||
between Tuesday and Wednesday. The fix is to put context where engineering
|
||||
teams already argue about code.
|
||||
|
||||
| Without context as code | With **ktx** |
|
||||
|--------|----------|
|
||||
| Context lives in BI tools, chats, docs, and analyst memory | Context lives in YAML and Markdown next to the warehouse code |
|
||||
| Agent changes appear without explanation | Agent changes appear as git diffs with provenance |
|
||||
| Imports overwrite analyst judgment | Ingest reconciles new evidence with accepted files |
|
||||
| History depends on tool logs | History lives in commits and ingest transcripts |
|
||||
|
||||
<ContextReviewLoop />
|
||||
|
||||
The loop closes on itself: every accepted edit becomes evidence the next ingest
|
||||
must respect. That's what makes **ktx** different from a one-way sync - it
|
||||
reads the layer before it writes to it.
|
||||
|
||||
## What's committed, what stays local
|
||||
|
||||
A **ktx** project keeps two surfaces under version control and one on disk for
|
||||
runtime use. The split matters at review time: only the first two belong in a
|
||||
PR, and the third is what you reach for when something looks off.
|
||||
|
||||
| Path | In git? | Purpose |
|
||||
|------|---------|---------|
|
||||
| `semantic-layer/<connection-id>/*.yaml` | Yes | Sources, joins, grain, measures, dimensions, and segments the compiler reads |
|
||||
| `wiki/global/*.md` | Yes | Definitions, policies, caveats, and metric provenance agents search |
|
||||
| `wiki/user/<user-id>/*.md` | Yes | Per-user scratch context that shadows global pages |
|
||||
| `.ktx/ingest-transcripts/<job>/` | No - local | Tool calls, LLM responses, and write decisions for one run |
|
||||
| `.ktx/ingest-evidence/<source>/<run>/` | No - local | Raw evidence snapshots used during reconciliation |
|
||||
| `.ktx/ingest-report.json` | No - local | Per-run summary with work units, diff stats, and the head commit |
|
||||
|
||||
Commit only the YAML and Markdown. The `.ktx/` runtime state is for debugging
|
||||
and replay; it belongs in `.gitignore`. If your team wants a record of *why* a
|
||||
change happened, link the transcript path in the PR description rather than
|
||||
committing the file.
|
||||
|
||||
## A typical review session
|
||||
|
||||
The loop above describes the shape. In practice, one review session looks like
|
||||
this:
|
||||
|
||||
```bash
|
||||
# 1. Run ingest on a branch
|
||||
git checkout -b ingest/2026-05-21
|
||||
ktx ingest --all
|
||||
|
||||
# 2. See what changed
|
||||
git status --short
|
||||
git diff -- semantic-layer wiki
|
||||
|
||||
# 3. Validate the semantic-layer changes against the warehouse
|
||||
ktx sl validate orders --connection-id warehouse
|
||||
|
||||
# 4. Compile a representative query before agents do
|
||||
ktx sl query \
|
||||
--connection-id warehouse \
|
||||
--measure orders.net_revenue \
|
||||
--dimension orders.month \
|
||||
--format sql
|
||||
|
||||
# 5. Open a PR, request review, merge when approved
|
||||
```
|
||||
|
||||
Teams typically run interactive ingest during setup, then schedule
|
||||
`ktx ingest --all --no-input` on a dedicated ingest branch once the
|
||||
sources are stable. The PR template tends to mirror what you actually
|
||||
look at in a diff:
|
||||
|
||||
- New sources match the warehouse, and their grain looks right.
|
||||
- Joins have the correct relationship direction.
|
||||
- Generated measures match business definitions.
|
||||
- Wiki pages cite evidence and don't duplicate YAML.
|
||||
- Nothing in `.ktx/` snuck into the commit.
|
||||
|
||||
## What changes ktx makes in a diff
|
||||
|
||||
Every line in a ktx diff is one of seven actions. The action is recorded in
|
||||
`.ktx/ingest-report.json` and shows up in the agent's reasoning, so you can
|
||||
trace any change back to the decision that produced it.
|
||||
|
||||
| Action | What it means | Where you see it in the diff |
|
||||
|--------|---------------|------------------------------|
|
||||
| `source_created` | A new table got a semantic source | New YAML file under `semantic-layer/<connection>/` |
|
||||
| `measure_added` | A new measure on an existing source | New entry under `measures:` in an existing YAML |
|
||||
| `join_added` | A new relationship between two sources | New entry under `joins:` |
|
||||
| `merged` | Multiple candidates were reconciled into one | Updated YAML or wiki page with combined fields |
|
||||
| `subsumed` | A duplicate was absorbed into an existing definition | One file removed; another updated |
|
||||
| `wiki_written` | Business context got captured | New or updated `.md` file under `wiki/` |
|
||||
| `skipped` | The candidate was already covered or out of scope | No file change; appears only in the report |
|
||||
|
||||
If a diff line surprises you, the action label is the fastest way to figure
|
||||
out what the ingest agent thought it was doing.
|
||||
|
||||
## Feedback loops
|
||||
|
||||
The accepted state of `semantic-layer/` and `wiki/` is input to the next
|
||||
ingest, not output. That makes corrections compound: a fix you ship today
|
||||
becomes the baseline tomorrow.
|
||||
|
||||
| Signal | Example | Where it lands |
|
||||
|--------|---------|----------------|
|
||||
| Analyst correction | "Net revenue excludes test accounts" | `semantic-layer/**/*.yaml` |
|
||||
| Business clarification | "ARR definition changed this quarter" | `wiki/**/*.md` |
|
||||
| Agent query issue | A filter returns no rows unexpectedly | Wiki caveat or tighter source filter |
|
||||
| Join problem | A path duplicates order-level measures | Updated `relationship` or `grain` metadata |
|
||||
| Mid-stream note | "Onboarding fees don't count toward ARR" | `ktx ingest --text "..."` writes to `wiki/global/` |
|
||||
|
||||
Capture context as soon as it's said. The next ingest will treat it as
|
||||
accepted truth.
|
||||
|
||||
## Replay and provenance
|
||||
|
||||
Every ingest writes a transcript next to the report. Together, they let you
|
||||
walk back through any decision after the fact - useful both for debugging a
|
||||
bad measure and for showing a stakeholder where a definition came from.
|
||||
|
||||
| Use case | What replay gives you |
|
||||
|----------|-----------------------|
|
||||
| Debugging | Trace a wrong source, join, or measure back to the evidence and tool calls that produced it |
|
||||
| Trust | Show which YAML and Markdown lines came from which dbt model, dashboard, or query history sample |
|
||||
| Reproducibility | Re-run the same evidence against a new model or config and compare diffs |
|
||||
|
||||
The artifacts live under `.ktx/ingest-transcripts/<jobId>/` and
|
||||
`.ktx/ingest-evidence/<source>/<runId>/`. Don't commit them - link to them
|
||||
from a PR or copy a span into a review comment when it explains a change.
|
||||
|
||||
## Agent usage notes
|
||||
|
||||
Use this page when an agent needs to explain review workflows, ingestion
|
||||
diffs, how corrections feed back into the layer, or why **ktx** writes YAML and
|
||||
Markdown instead of hiding context in a hosted service.
|
||||
|
||||
| Agent task | Relevant section | Next page |
|
||||
|------------|------------------|-----------|
|
||||
| Explain how generated context should be reviewed | A typical review session | [Building Context](/docs/guides/building-context) |
|
||||
| Explain what a specific diff line means | What changes ktx makes in a diff | [Writing Context](/docs/guides/writing-context) |
|
||||
| Diagnose why ingestion changed a semantic source | Replay and provenance | [ktx ingest](/docs/cli-reference/ktx-ingest) |
|
||||
| Describe how context improves over time | Feedback loops | [Building Context](/docs/guides/building-context) |
|
||||
| Tell a user what to commit | What's committed, what stays local | [Writing Context](/docs/guides/writing-context) |
|
||||
2
docs-site/next-env.d.ts
vendored
2
docs-site/next-env.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue