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

); }