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 (#201)
* 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. * test: point examples-docs assertion at reviewing-context Update the doc smoke test that read context-as-code.mdx to read the new guides/reviewing-context.mdx path. The `ktx ingest --all --no-input` assertion still holds; the rename was the only break.
This commit is contained in:
parent
5211a0317e
commit
4d4296f397
14 changed files with 1062 additions and 288 deletions
|
|
@ -1035,6 +1035,49 @@ body::after {
|
||||||
51%, 100% { opacity: 0; }
|
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
|
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";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Background,
|
|
||||||
BackgroundVariant,
|
|
||||||
type Edge,
|
type Edge,
|
||||||
type EdgeProps,
|
type EdgeProps,
|
||||||
getSmoothStepPath,
|
getSmoothStepPath,
|
||||||
|
|
@ -11,11 +9,11 @@ import {
|
||||||
type Node,
|
type Node,
|
||||||
type NodeProps,
|
type NodeProps,
|
||||||
Position,
|
Position,
|
||||||
ReactFlow,
|
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { FlowCanvas } from "./flow-canvas";
|
||||||
|
|
||||||
type SourceNodeData = {
|
type SourceNodeData = {
|
||||||
accent: string;
|
accent: string;
|
||||||
body: string;
|
body: string;
|
||||||
|
|
@ -538,81 +536,21 @@ export function ProductMechanics() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<FlowCanvas
|
||||||
className="mechanics-canvas bg-fd-background"
|
nodes={nodes}
|
||||||
style={{
|
edges={edges}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
edgeTypes={edgeTypes}
|
||||||
|
canvasStyle={{
|
||||||
height: "min(1240px, 170vw)",
|
height: "min(1240px, 170vw)",
|
||||||
minHeight: 720,
|
minHeight: 720,
|
||||||
}}
|
}}
|
||||||
>
|
className="mechanics-canvas"
|
||||||
<ReactFlow
|
fitViewOptions={{ padding: 0.04 }}
|
||||||
nodes={nodes}
|
ariaLabel="ktx ingestion flow diagram"
|
||||||
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>
|
|
||||||
</article>
|
</article>
|
||||||
<style>{`
|
<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 {
|
.mechanics-canvas .mechanics-particle {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
filter: drop-shadow(0 0 6px currentColor);
|
filter: drop-shadow(0 0 6px currentColor);
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,14 @@
|
||||||
|
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Background,
|
|
||||||
BackgroundVariant,
|
|
||||||
Handle,
|
Handle,
|
||||||
MarkerType,
|
MarkerType,
|
||||||
type Node,
|
type Node,
|
||||||
type NodeProps,
|
type NodeProps,
|
||||||
type OnInit,
|
|
||||||
Position,
|
Position,
|
||||||
ReactFlow,
|
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
|
||||||
|
import { FlowCanvas } from "./flow-canvas";
|
||||||
|
|
||||||
type LaneVariant = "manual" | "ktx";
|
type LaneVariant = "manual" | "ktx";
|
||||||
|
|
||||||
|
|
@ -472,8 +469,6 @@ const edges = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
type FlowEdge = (typeof edges)[number];
|
|
||||||
|
|
||||||
function AgentNodeView({ data }: NodeProps<AgentNode>) {
|
function AgentNodeView({ data }: NodeProps<AgentNode>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -980,15 +975,6 @@ const nodeTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SemanticLayerFlow() {
|
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 (
|
return (
|
||||||
<section
|
<section
|
||||||
id="imperative-vs-declarative"
|
id="imperative-vs-declarative"
|
||||||
|
|
@ -1027,85 +1013,20 @@ export function SemanticLayerFlow() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<FlowCanvas
|
||||||
className="sl-flow-canvas relative bg-fd-background"
|
nodes={nodes}
|
||||||
style={{
|
edges={edges}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
canvasStyle={{
|
||||||
height: "min(2340px, 290vw)",
|
height: "min(2340px, 290vw)",
|
||||||
minHeight: 1780,
|
minHeight: 1780,
|
||||||
}}
|
}}
|
||||||
>
|
className="sl-flow-canvas"
|
||||||
<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">
|
fitViewOptions={FIT_VIEW_OPTIONS}
|
||||||
Drag to pan • ⌘/Ctrl + scroll to zoom
|
ariaLabel="Semantic query to SQL flow diagram"
|
||||||
</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>
|
|
||||||
</article>
|
</article>
|
||||||
<style>{`
|
<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 {
|
.sl-flow-canvas pre {
|
||||||
font-size: 11.5px !important;
|
font-size: 11.5px !important;
|
||||||
line-height: 17.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",
|
"title": "Concepts",
|
||||||
"defaultOpen": true,
|
"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) |
|
| 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) |
|
| 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) |
|
| 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>
|
<span className="font-medium text-fd-foreground">{"Behind the scenes. "}</span>
|
||||||
<strong className="font-medium text-fd-foreground">{"ktx"}</strong>
|
<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 "}
|
{" 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."}
|
{" for how that audit trail flows into review."}
|
||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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",
|
"title": "Guides",
|
||||||
"defaultOpen": true,
|
"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" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <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
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,7 @@ describe('standalone example docs', () => {
|
||||||
const setupReference = await readText('docs-site/content/docs/cli-reference/ktx-setup.mdx');
|
const setupReference = await readText('docs-site/content/docs/cli-reference/ktx-setup.mdx');
|
||||||
const buildingContext = await readText('docs-site/content/docs/guides/building-context.mdx');
|
const buildingContext = await readText('docs-site/content/docs/guides/building-context.mdx');
|
||||||
const contextSources = await readText('docs-site/content/docs/integrations/context-sources.mdx');
|
const contextSources = await readText('docs-site/content/docs/integrations/context-sources.mdx');
|
||||||
const contextAsCode = await readText('docs-site/content/docs/concepts/context-as-code.mdx');
|
const reviewingContext = await readText('docs-site/content/docs/guides/reviewing-context.mdx');
|
||||||
const quickstart = await readText('docs-site/content/docs/getting-started/quickstart.mdx');
|
const quickstart = await readText('docs-site/content/docs/getting-started/quickstart.mdx');
|
||||||
const primarySources = await readText('docs-site/content/docs/integrations/primary-sources.mdx');
|
const primarySources = await readText('docs-site/content/docs/integrations/primary-sources.mdx');
|
||||||
const examplesIndex = await readText('examples/README.md');
|
const examplesIndex = await readText('examples/README.md');
|
||||||
|
|
@ -252,7 +252,7 @@ describe('standalone example docs', () => {
|
||||||
assert.match(buildingContext, /ktx ingest <connectionId>/);
|
assert.match(buildingContext, /ktx ingest <connectionId>/);
|
||||||
assert.match(buildingContext, /ktx ingest --all/);
|
assert.match(buildingContext, /ktx ingest --all/);
|
||||||
assert.match(contextSources, /ktx ingest <connectionId>/);
|
assert.match(contextSources, /ktx ingest <connectionId>/);
|
||||||
assert.match(contextAsCode, /ktx ingest --all --no-input/);
|
assert.match(reviewingContext, /ktx ingest --all --no-input/);
|
||||||
assert.match(quickstart, /schema context/);
|
assert.match(quickstart, /schema context/);
|
||||||
assert.match(primarySources, /context:\n queryHistory:/);
|
assert.match(primarySources, /context:\n queryHistory:/);
|
||||||
assert.match(rootReadme, /`ktx ingest <id>` \| Build context for one connection/);
|
assert.match(rootReadme, /`ktx ingest <id>` \| Build context for one connection/);
|
||||||
|
|
@ -278,7 +278,7 @@ describe('standalone example docs', () => {
|
||||||
assert.doesNotMatch(buildingContext, /live-database/);
|
assert.doesNotMatch(buildingContext, /live-database/);
|
||||||
assert.doesNotMatch(contextSources, /ktx ingest run --connection-id/);
|
assert.doesNotMatch(contextSources, /ktx ingest run --connection-id/);
|
||||||
assert.doesNotMatch(contextSources, /--adapter <adapter>/);
|
assert.doesNotMatch(contextSources, /--adapter <adapter>/);
|
||||||
assert.doesNotMatch(contextAsCode, /ktx ingest run --connection-id/);
|
assert.doesNotMatch(reviewingContext, /ktx ingest run --connection-id/);
|
||||||
assert.doesNotMatch(quickstart, /Historic SQL/);
|
assert.doesNotMatch(quickstart, /Historic SQL/);
|
||||||
assert.doesNotMatch(quickstart, /--enable-historic-sql/);
|
assert.doesNotMatch(quickstart, /--enable-historic-sql/);
|
||||||
assert.doesNotMatch(quickstart, /press <kbd>d<\/kbd> to detach/);
|
assert.doesNotMatch(quickstart, /press <kbd>d<\/kbd> to detach/);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue