From 4d4296f3973a88b1d5609ca86c296501bf22d4db Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Thu, 21 May 2026 15:42:50 +0200 Subject: [PATCH] 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. --- docs-site/app/global.css | 43 ++ docs-site/components/context-review-loop.tsx | 704 ++++++++++++++++++ docs-site/components/flow-canvas.tsx | 118 +++ docs-site/components/product-mechanics.tsx | 86 +-- docs-site/components/semantic-layer-flow.tsx | 101 +-- .../content/docs/concepts/context-as-code.mdx | 114 --- docs-site/content/docs/concepts/meta.json | 2 +- .../concepts/semantic-layer-internals.mdx | 2 +- .../docs/concepts/the-context-layer.mdx | 4 +- .../content/docs/concepts/wiki-retrieval.mdx | 2 +- docs-site/content/docs/guides/meta.json | 2 +- .../content/docs/guides/reviewing-context.mdx | 164 ++++ docs-site/next-env.d.ts | 2 +- scripts/examples-docs.test.mjs | 6 +- 14 files changed, 1062 insertions(+), 288 deletions(-) create mode 100644 docs-site/components/context-review-loop.tsx create mode 100644 docs-site/components/flow-canvas.tsx delete mode 100644 docs-site/content/docs/concepts/context-as-code.mdx create mode 100644 docs-site/content/docs/guides/reviewing-context.mdx diff --git a/docs-site/app/global.css b/docs-site/app/global.css index b9124304..a4cebc55 100644 --- a/docs-site/app/global.css +++ b/docs-site/app/global.css @@ -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 ═══════════════════════════════════════════ */ diff --git a/docs-site/components/context-review-loop.tsx b/docs-site/components/context-review-loop.tsx new file mode 100644 index 00000000..38814026 --- /dev/null +++ b/docs-site/components/context-review-loop.tsx @@ -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; +type IngestNode = Node; +type DiffNode = Node; +type ReviewNode = Node; +type MergedNode = Node; +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 ( + + {children} + + ); +} + +function FlowHandles({ noTop = false }: { noTop?: boolean }) { + return ( + <> + {!noTop ? ( + + ) : null} + + + + + ); +} + +function SourcesHandles() { + return ( + <> + + + + ); +} + +function SourcesNodeView({ data }: NodeProps) { + return ( +
+ + {data.badge} +

+ {data.title} +

+

+ {data.caption} +

+
+ {data.items.map((item) => ( + + + ))} +
+
+ ); +} + +function IngestNodeView({ data }: NodeProps) { + return ( +
+
+ ); +} + +function DiffLine({ line }: { line: DiffFileLine }) { + if (line.kind === "hunk") { + return ( +
+ {line.text} +
+ ); + } + 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 ( +
+ + {line.text} +
+ ); +} + +function DiffFileBlock({ file }: { file: DiffFile }) { + return ( +
+
+ + {file.path} + + + +{file.added} + + {file.removed > 0 ? ( + + -{file.removed} + + ) : null} +
+
+ {file.lines.map((line, idx) => ( + + ))} +
+
+ ); +} + +function DiffNodeView({ data }: NodeProps) { + return ( +
+ +
+ {data.badge} +

+ {data.title} +

+ + + {data.branch} + +
+

+ {data.caption} +

+
+ {data.files.map((file) => ( + + ))} +
+
+ ); +} + +function ReviewNodeView({ data }: NodeProps) { + return ( +
+ +
+ {data.badge} +

+ {data.title} +

+
+

+ {data.caption} +

+
    + {data.checks.map((check) => ( +
  • + + {check} +
  • + ))} +
+
+ ); +} + +function MergedNodeView({ data }: NodeProps) { + return ( +
+ +
+ {data.badge} +

+ {data.title} +

+
+

+ {data.caption} +

+
+ {data.paths.map((path) => ( + + + ))} +
+
+ ); +} + +const nodeTypes = { + sources: SourcesNodeView, + ingest: IngestNodeView, + diff: DiffNodeView, + review: ReviewNodeView, + merged: MergedNodeView, +}; + +export function ContextReviewLoop() { + return ( +
+
+
+ + The review loop + + +

+ Every ingest is a diff you can refuse +

+

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

+

+

+
+ + +
+
+ ); +} diff --git a/docs-site/components/flow-canvas.tsx b/docs-site/components/flow-canvas.tsx new file mode 100644 index 00000000..cc39f9e5 --- /dev/null +++ b/docs-site/components/flow-canvas.tsx @@ -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 = { + 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; + 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({ + nodes, + edges, + nodeTypes, + edgeTypes, + canvasStyle, + className, + fitViewOptions = DEFAULT_FIT_VIEW, + maxZoom = 1.5, + translateExtent, + ariaLabel, +}: FlowCanvasProps) { + const [minZoom, setMinZoom] = useState(0.15); + const handleInit = useCallback>( + (instance) => { + requestAnimationFrame(() => { + void instance.fitView(fitViewOptions).then(() => { + setMinZoom(instance.getZoom()); + }); + }); + }, + [fitViewOptions], + ); + + return ( +
+
+ Drag to pan · ⌘/Ctrl + scroll to zoom +
+ + + + +
+ ); +} diff --git a/docs-site/components/product-mechanics.tsx b/docs-site/components/product-mechanics.tsx index cabc149f..642e1107 100644 --- a/docs-site/components/product-mechanics.tsx +++ b/docs-site/components/product-mechanics.tsx @@ -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() {

-
- - - -
+ className="mechanics-canvas" + fitViewOptions={{ padding: 0.04 }} + ariaLabel="ktx ingestion flow diagram" + />