diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cace9460..e2737e73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,7 +217,7 @@ jobs: flags: typescript name: typescript disable_search: true - fail_ci_if_error: false + fail_ci_if_error: true - name: Warn when Codecov token is missing for TypeScript if: env.CODECOV_TOKEN_CONFIGURED != 'true' @@ -236,7 +236,7 @@ jobs: flags: python name: python disable_search: true - fail_ci_if_error: false + fail_ci_if_error: true - name: Warn when Codecov token is missing for Python if: env.CODECOV_TOKEN_CONFIGURED != 'true' diff --git a/.github/workflows/star-history.yml b/.github/workflows/star-history.yml deleted file mode 100644 index b7d90c43..00000000 --- a/.github/workflows/star-history.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Refresh star history chart - -on: - schedule: - # Twice daily at 06:00 and 18:00 UTC. - - cron: "0 6,18 * * *" - workflow_dispatch: - -permissions: - contents: write - -env: - DO_NOT_TRACK: "1" - KTX_TELEMETRY_DISABLED: "1" - NEXT_TELEMETRY_DISABLED: "1" - -concurrency: - group: star-history-refresh - cancel-in-progress: true - -jobs: - refresh: - name: Regenerate assets/star-history.svg - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - # RELEASE_PAT can push to the protected main branch; the default - # GITHUB_TOKEN is rejected by the branch-protection hook (GH006). - token: ${{ secrets.RELEASE_PAT }} - - - name: Fetch fresh star-history SVG - run: | - set -euo pipefail - # cachebust forces star-history to regenerate instead of serving its - # own server-side cache; --location follows the slug-normalizing 301. - url="https://api.star-history.com/svg?repos=Kaelio/ktx&type=Date&cachebust=${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - curl --fail --location --silent --show-error \ - --retry 3 --retry-delay 5 --max-time 60 \ - -o assets/star-history.svg.new "$url" - # Guard against error pages / truncated responses before overwriting. - if ! grep -q "" assets/star-history.svg.new; then - echo "Downloaded file is not a valid SVG; aborting." >&2 - exit 1 - fi - if [ "$(wc -c < assets/star-history.svg.new)" -lt 1000 ]; then - echo "Downloaded SVG is suspiciously small; aborting." >&2 - exit 1 - fi - # The star-history API returns the SVG without a trailing newline, - # which end-of-file-fixer rewrites whenever pre-commit runs - # --all-files on a PR. Because the refresh commit below uses [skip ci], - # the hook never runs against it here, so an un-normalized file - # silently breaks the pre-commit check on every open PR. Normalize to - # exactly one trailing newline before committing. - printf '%s\n' "$(cat assets/star-history.svg.new)" > assets/star-history.svg - rm -f assets/star-history.svg.new - - - name: Commit if changed - run: | - set -euo pipefail - if git diff --quiet -- assets/star-history.svg; then - echo "Star-history chart unchanged; nothing to commit." - exit 0 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add assets/star-history.svg - # [skip ci] keeps this housekeeping commit from triggering KTX CI. - git commit -m "chore: refresh star history chart [skip ci]" - git push diff --git a/.github/workflows/triage-issues.yml b/.github/workflows/triage-issues.yml index e5817e2c..41d2f048 100644 --- a/.github/workflows/triage-issues.yml +++ b/.github/workflows/triage-issues.yml @@ -22,7 +22,7 @@ jobs: github.event.issue.author_association != 'COLLABORATOR' steps: - name: Apply needs-triage label - uses: actions/github-script@v9 + uses: actions/github-script@v7 with: script: | await github.rest.issues.addLabels({ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 681cf0ab..cc2f483d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,18 +14,6 @@ repos: - id: check-case-conflict - id: mixed-line-ending - - repo: https://github.com/tombi-toml/tombi-pre-commit - rev: v1.1.0 - hooks: - - id: tombi-format - args: ["--offline"] - # uv.lock is generated and owned by uv, which writes its own canonical - # TOML layout. tombi reformats that layout differently, so once uv - # regenerates the lock (e.g. after a dependency or version change) - # tombi rewrites it and the hook fails on the modified file. Keep uv - # authoritative for its lockfile; tombi still formats hand-edited TOML. - exclude: ^uv\.lock$ - - repo: https://github.com/asottile/pyupgrade rev: v3.21.2 hooks: diff --git a/AGENTS.md b/AGENTS.md index ffe324bb..3d8c1725 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,25 +64,6 @@ When rules conflict, follow this order: 4. Code quality: types, readable boundaries, focused modules 5. Performance where it matters -## Opinionated Product Defaults - -- **MUST**: Prefer one canonical behavior over configurable alternatives. A new - flag, config field, environment variable, mode, strategy option, adapter hook, - or fallback path is a product feature and must be justified by an explicit - user request or a real correctness requirement. -- **MUST NOT**: Add speculative flexibility for imagined users, migrations, - review preferences, local workflows, or "just in case" scenarios. If the - requested behavior can work with one solid default, implement that default. -- **MUST NOT**: Add boolean switches that create two runtime paths unless both - paths are essential and the user explicitly asked for the choice. Boolean - policy knobs are especially suspect because they double the state space and - test surface. -- **MUST**: When a design seems to need a new option, first try to remove the - need by choosing the stronger default, tightening the invariant, or failing - clearly. Ask the user before adding the option if it still seems necessary. -- **MUST**: Delete obsolete branches, tests, docs, and config when removing a - behavior. Do not preserve dormant compatibility paths. - ## Repository Shape **ktx** is a pnpm + uv workspace. @@ -178,91 +159,6 @@ and naming asymmetries are bugs in waiting — see [`docs/code-design.md`](docs/code-design.md). Treat the `MUST` / `MUST NOT` rules there with the same weight as the ones in this file. -## Design Reasoning Defaults - -When proposing a design, an approach, or any non-trivial change, apply these -defaults and run the self-check before presenting it. They encode the -corrections users most often have to make; reaching these conclusions -autonomously — without being asked the leading question — is the bar. - -- **MUST**: Optimize for the best outcome, not for an unstated constraint. Do not - silently adopt "smallest change", "least effort", "cheapest", or "least user - intervention" as the goal unless the user said so. Default to the most correct, - durable solution, and present cost / effort / scope as information for the user - to weigh — not as a ceiling you impose on their behalf. -- **MUST**: Separate one-time cost from recurring cost before discarding an - option. A fixed cost paid once (a setup-time computation, an extra LLM call - during setup, a contract change) to make every later run cheaper or more - correct is usually worth it. Do not reject it with recurring-cost reasoning; - quantify both sides. (Example smell: "don't add an LLM call to a cost-cutting - feature" — wrong when the call is one-time and the savings recur.) -- **MUST**: Treat a user's example as a representative of a class, not as the - spec. Design for the general population the example stands for, then stress-test - against deliberately different instances — another warehouse, dialect, stack - layout, or input shape — before committing. If a design only works because of an - incidental property of the example (e.g. "the noise happened to be in a separate - schema *on this demo*"), it is curve-fitting; generalize it or state the - assumption explicitly. -- **MUST**: Prefer deriving from the system's own state over enumerating cases. - Favor an allowlist computed from declared/observed state (config, scanned - catalog, query log, the user's own inputs) over a denylist of known-bad - specifics (particular tables, schemas, tools, or vendors). A hardcoded or - hand-maintained list of external specifics is a smell: it rots and fails on the - next stack. The only acceptable static patterns are genuinely universal - invariants (e.g. DB-engine system catalogs) and ktx's own self-emitted - signatures. -- **MUST**: Give each capability one implementation and route every caller - through it. When some behavior — running a query, resolving a credential or - config reference, authenticating, selecting a dialect, loading config — - already has a working implementation that some call sites use, make new or - divergent call sites depend on that path instead of standing up a second one. - Parallel implementations of one capability drift apart silently: a fix, a - newly supported input, or an added case lands on one path and not the other, - so one entry point (a CLI command, an MCP tool, an ingest stage) succeeds - while another fails on the same input. When two paths already do the same - job, collapse onto the shared one and delete the duplicate instead of - keeping both. When fixing a defect that lives on one path, fix the shared - implementation; do not patch the symptom on a forked branch, which preserves - the divergence you set out to remove. -- **SHOULD**: Before inventing an abstraction or hand-rolling structural logic, - search for what already exists and reuse it — the codebase's canonical - representation (a structured ref/key type) instead of a parallel string scheme, - and a mandated/available tool (e.g. `sqlglot` for SQL structure; see - [SQL and Structured Parsing](#sql-and-structured-parsing)) instead of - hand-parsing. Normalize ambiguous input to the canonical form at the boundary; - do not carry the ambiguity downstream. This is the single-source-of-truth / DRY - item from the Priority Hierarchy applied at design time. - -Before presenting a design, answer these explicitly: - -1. Am I optimizing for a goal the user actually stated, or one I assumed? -2. Does this generalize beyond the example in front of me? Name a real case where - it would break. -3. Am I enumerating known-bad cases when I could derive scope from the system's - own declared/observed state? -4. Is there an existing canonical representation or mandated tool I should reuse - instead of building or parsing my own? -5. Am I discarding the better option on a weak or misapplied constraint - (one-time vs recurring cost, "more surface area", "more work now")? -6. Does another entry point already perform this operation through a shared - implementation? If so, am I routing through that path instead of forking a - parallel one — and if I'm fixing a bug, am I fixing the shared layer rather - than one branch? -7. Am I adding a user-visible option or alternate runtime path that the user did - not ask for? If yes, can one opinionated default solve the problem instead? -8. Does this option multiply behavior by caller path, config value, or local - state? If yes, remove it unless it is explicitly required. - -A user question that nudges toward an alternative ("would X help?", "should I -always do Y?", "will you hardcode Z?") is a signal that a better option exists. -Investigate the implied direction and reason it through *before* defending the -original proposal — and prefer to have asked yourself the question first. - -Example: If generated context changes should be saved, choose one save policy -and route ingest, setup, memory, indexing, and docs through it. Do not add an -`auto_commit`-style switch unless the user explicitly asks for staged-only runs -and accepts the extra runtime path. - ## TypeScript Standards - Use Node 22+ and pnpm workspace commands. @@ -382,8 +278,7 @@ use `PascalCase` without the suffix. ## Telemetry -**ktx** ships PostHog usage telemetry. Catalog telemetry events use strict -schemas. When adding commands or events: +**ktx** ships anonymous PostHog telemetry. When adding commands or events: - **MUST NOT**: Add fields that carry user data — file paths, hostnames, environment values, SQL text, schema/table/column names, error messages, @@ -400,24 +295,6 @@ schemas. When adding commands or events: of collected data changes. Adding another event with no new field types needs no docs change. -### Error reports - -**ktx** also sends PostHog Error Tracking `$exception` events when telemetry is -enabled. This channel is separate from the strict catalog event schema and is -used only for exception diagnostics. - -`$exception` events may include stack frames, error class names, raw error -messages, cause chains, `source`, `handled`, `fatal`, runtime version fields, -OS/runtime fields, and the hashed `projectId` when known. Stack frames may -include local file paths and the local username when those appear in paths. - -`$exception` events must never intentionally include secrets, credentials, -database URLs, auth headers, raw argv, raw environment values, SQL text, -schema/table/column names as explicit properties, customer row data, user prompt -text, or raw MCP arguments. Reporters must redact call-site-provided secret -snapshots and common static credential patterns before the SDK serializes the -exception. - ## Documentation and Specs - Keep public documentation in `README.md`, package READMEs, example READMEs, @@ -473,9 +350,8 @@ error messages — including the disambiguation rule for the overloaded word `source` (semantic / primary / context / source of truth) — see [`docs/terminology.md`](docs/terminology.md). Follow that file when choosing between near-synonyms (e.g. `connector` vs `adapter`, `data agent` vs -`database agent`, `context-source ingest` vs `source ingest`). Product-name -rules in this section take precedence over anything in that file when they -conflict. +`database agent`, `fast ingest` vs `schema ingest`). Product-name rules in +this section take precedence over anything in that file when they conflict. ### Updating `docs-site/` After Code Changes @@ -504,8 +380,7 @@ rather than silently skipping it. - **MUST**: Disable monospace ligatures on every surface that uses the `var(--font-mono)` family (Geist Mono). Geist Mono fuses `--` into an em-dash glyph that visually eats the adjacent space, so prompts like - `npx skills add Kaelio/ktx --skill ktx` render as - `Kaelio/ktx--skill ktx`. + `npx skills add Kaelio/ktx --skill ktx` render as `Kaelio/ktx--skill ktx`. - **MUST**: When adding a new container that renders user-visible monospace text outside `` / `
` (e.g. a styled `
` for a copyable prompt), verify the global ligature-off rule in diff --git a/README.md b/README.md index 3f704f04..23b2fa0a 100644 --- a/README.md +++ b/README.md @@ -13,20 +13,16 @@ Documentation Join the ktx Slack community License - Y Combinator P25 + Y Combinator P25

Quickstart · CLI Reference · - Agent Setup · + Agent Setup · Slack

-

- Built and maintained by Kaelio -

- --- **ktx** is a self-improving context layer that teaches agents how to query your @@ -34,25 +30,13 @@ warehouse accurately - from approved metric definitions, joinable columns, and business knowledge it builds and maintains for you. > [!NOTE] -> Run **ktx** with your own LLM API keys or a local agent sign-in — a -> **Claude Pro/Max** subscription through Claude Code, or your local Codex -> authentication. No extra usage billing from **ktx**. +> Run **ktx** with your own LLM API keys or a **Claude Pro/Max** subscription. +> No extra usage billing from **ktx**.

- - Watch the ktx launch video (1:56) - + ktx ingestion flow from source systems through validation to wiki and semantic-layer outputs

-

- Ingestion: ktx ingests databases, BI tools, modeling code, and docs through its context engine (source connectors, context builder, reconciliation, validation) into wiki Markdown and semantic-layer YAML -

- -

- Serving: an agent queries ktx through MCP, which searches the wiki and semantic layer, returns approved metrics, and compiles them into read-only SQL run against the warehouse -

- - ## Why ktx General-purpose agents struggle on data tasks. They re-explore your warehouse @@ -143,14 +127,6 @@ Agent integration ready: yes (codex:project) > If `ktx status` prints `ktx mcp start --project-dir ...`, run it before > opening your agent client. -## Upgrading - -Re-run the global install with the `@latest` tag: - -```bash -npm install -g @kaelio/ktx@latest -``` - ## First commands | Command | Purpose | @@ -188,9 +164,8 @@ then the current directory. Pass `--project-dir ` when scripting. No. **ktx** runs locally. The only data leaving your machine is what you send to the LLM provider you configured. - **Which LLM backends are supported?** - Anthropic API, Google Vertex AI, AI Gateway, the local Claude Code session - through the Claude Agent SDK, and your local Codex authentication through the - Codex SDK. See + Anthropic API, Google Vertex AI, AI Gateway, and the local Claude Code + session through the Claude Agent SDK. See [LLM configuration](https://docs.kaelio.com/ktx/docs/guides/llm-configuration). - **How is ktx different from a dbt or MetricFlow semantic layer?** **ktx** *ingests* those layers and combines them with raw-table @@ -209,7 +184,7 @@ then the current directory. Pass `--project-dir ` when scripting. - [The Context Layer](https://docs.kaelio.com/ktx/docs/concepts/the-context-layer) - [Building Context](https://docs.kaelio.com/ktx/docs/guides/building-context) - [CLI Reference](https://docs.kaelio.com/ktx/docs/cli-reference/ktx) -- [AI Resources](https://docs.kaelio.com/ktx/docs/community/ai-resources) +- [Agent Quickstart](https://docs.kaelio.com/ktx/docs/ai-resources/agent-quickstart) - [Community & Support](https://docs.kaelio.com/ktx/docs/community/support) ## Community @@ -259,17 +234,11 @@ uv run pytest -q ## Telemetry -**ktx** collects privacy-conscious usage telemetry to understand installs and -improve setup, command reliability, and data-agent workflows. Catalog telemetry -events do not record file paths, hostnames, SQL, schema names, table names, -column names, error messages, raw environment values, or argv. Error reports use -PostHog Error Tracking and can include stack frames and raw error messages, -which may contain local file paths or the local username in those paths. -**ktx** redacts secrets, credentials, database URLs, auth headers, argv, raw -environment values, SQL text, row data, and user-typed prompt or MCP argument -text from the explicit `$exception` payload. See -[Telemetry](https://docs.kaelio.com/ktx/docs/community/telemetry) for the event -catalog and opt-out options. +**ktx** collects anonymous usage telemetry from interactive CLI runs to +improve setup, command reliability, and data-agent workflows. No file paths, +hostnames, SQL, schema names, error messages, or argv are recorded. See +[Telemetry](https://docs.kaelio.com/ktx/docs/community/telemetry) for the +event catalog and opt-out options. ## License @@ -279,6 +248,6 @@ catalog and opt-out options.

- ktx Star History Chart + ktx Star History Chart

diff --git a/assets/ktx-lockup.svg b/assets/ktx-lockup.svg index 2e45f8a6..f1bcd2dd 100644 --- a/assets/ktx-lockup.svg +++ b/assets/ktx-lockup.svg @@ -19,9 +19,14 @@ - - - - + + ktx diff --git a/assets/launch-video-thumb.png b/assets/launch-video-thumb.png deleted file mode 100644 index c7505732..00000000 Binary files a/assets/launch-video-thumb.png and /dev/null differ diff --git a/assets/star-history.svg b/assets/star-history.svg deleted file mode 100644 index 8fa7bc08..00000000 --- a/assets/star-history.svg +++ /dev/null @@ -1 +0,0 @@ -star-history.comMay 17May 24May 31Jun 07 200400600800kaelio/ktxStar HistoryDateGitHub Stars diff --git a/docs-site/app/diagram-studio/page.tsx b/docs-site/app/diagram-studio/page.tsx deleted file mode 100644 index 205ebd7a..00000000 --- a/docs-site/app/diagram-studio/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Metadata } from "next"; - -import { DiagramStudio } from "@/components/diagram-studio/studio"; - -export const metadata: Metadata = { - title: "Diagram studio", - robots: { index: false, follow: false }, -}; - -export default function DiagramStudioPage() { - return ; -} diff --git a/docs-site/app/docs/layout.tsx b/docs-site/app/docs/layout.tsx index 5f684ea0..ff7d69a9 100644 --- a/docs-site/app/docs/layout.tsx +++ b/docs-site/app/docs/layout.tsx @@ -2,21 +2,10 @@ import { source } from "@/lib/source"; import { DocsLayout } from "fumadocs-ui/layouts/docs"; import type { ReactNode } from "react"; import { baseOptions } from "@/app/layout.config"; -import { GitHubStars } from "@/components/github-stars"; export default function Layout({ children }: { children: ReactNode }) { return ( - - -
- ), - }} - > + {children} ); diff --git a/docs-site/app/global.css b/docs-site/app/global.css index d6d9ada6..929e06b4 100644 --- a/docs-site/app/global.css +++ b/docs-site/app/global.css @@ -869,147 +869,6 @@ body::after { 50% { opacity: 0.65; transform: scale(0.9); } } -/* ═══════════════════════════════════════════ - GitHub star widget (navbar) - Split pill: GitHub mark + "Star" │ gold star + count. - ═══════════════════════════════════════════ */ -.ktx-stars { - display: inline-flex; - align-items: stretch; - height: 32px; - border-radius: 999px; - border: 1px solid var(--color-fd-border); - background: color-mix(in oklch, var(--color-fd-card) 72%, transparent); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - font-family: var(--font-display), var(--font-sans), sans-serif; - font-size: 13px; - line-height: 1; - color: var(--color-fd-foreground); - text-decoration: none; - overflow: hidden; - box-shadow: 0 1px 2px rgba(27, 27, 24, 0.04); - transition: - transform 0.3s var(--ktx-ease), - box-shadow 0.3s var(--ktx-ease), - border-color 0.3s ease; - animation: ktx-stars-in 0.5s var(--ktx-ease) both; -} - -@keyframes ktx-stars-in { - from { opacity: 0; transform: translateY(-4px); } - to { opacity: 1; transform: translateY(0); } -} - -.ktx-stars:hover { - transform: translateY(-1px); - border-color: color-mix(in oklch, var(--color-fd-primary) 45%, var(--color-fd-border)); - box-shadow: - 0 6px 18px -8px rgba(14, 116, 144, 0.28), - 0 1px 2px rgba(27, 27, 24, 0.05); -} - -.ktx-stars:focus-visible { - outline: 2px solid var(--color-fd-ring); - outline-offset: 2px; -} - -.dark .ktx-stars { - background: color-mix(in oklch, var(--color-fd-card) 60%, transparent); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); -} - -.dark .ktx-stars:hover { - border-color: rgba(34, 211, 238, 0.4); - box-shadow: - 0 6px 18px -8px rgba(34, 211, 238, 0.3), - 0 1px 2px rgba(0, 0, 0, 0.3); -} - -.ktx-stars-seg { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 0 11px; -} - -.ktx-stars-seg--count { - border-left: 1px solid var(--color-fd-border); - background: color-mix(in oklch, var(--color-fd-primary) 6%, transparent); - transition: background 0.3s var(--ktx-ease); -} - -.ktx-stars:hover .ktx-stars-seg--count { - background: color-mix(in oklch, var(--color-fd-primary) 12%, transparent); -} - -.ktx-stars-gh { - width: 15px; - height: 15px; - opacity: 0.85; -} - -.ktx-stars-text { - font-weight: 500; - letter-spacing: -0.01em; -} - -.ktx-stars-star { - width: 14px; - height: 14px; - fill: #f5b301; - transition: transform 0.3s var(--ktx-ease), filter 0.3s var(--ktx-ease); -} - -.ktx-stars:hover .ktx-stars-star { - transform: scale(1.18) rotate(-8deg); - filter: drop-shadow(0 1px 4px rgba(245, 179, 1, 0.55)); -} - -.ktx-stars-count { - font-weight: 600; - font-variant-numeric: tabular-nums; - color: var(--color-fd-foreground); -} - -/* Skeleton shown only on the rare cold (uncached) fetch */ -.ktx-stars--skeleton { - animation: none; -} - -.ktx-stars-skeleton-bar { - display: inline-block; - width: 26px; - height: 11px; - border-radius: 4px; - background: linear-gradient( - 90deg, - var(--color-fd-muted) 25%, - color-mix(in oklch, var(--color-fd-muted-foreground) 28%, var(--color-fd-muted)) 50%, - var(--color-fd-muted) 75% - ); - background-size: 200% 100%; - animation: ktx-stars-shimmer 1.4s ease-in-out infinite; -} - -@keyframes ktx-stars-shimmer { - from { background-position: 200% 0; } - to { background-position: -200% 0; } -} - -/* Compact on phones: drop the "Star" word, keep mark + count */ -@media (max-width: 640px) { - .ktx-stars-text { display: none; } - .ktx-stars-seg { padding: 0 9px; } -} - -@media (prefers-reduced-motion: reduce) { - .ktx-stars { animation: none; transition: none; } - .ktx-stars:hover { transform: none; } - .ktx-stars:hover .ktx-stars-star { transform: none; } - .ktx-stars-skeleton-bar { animation: none; } -} - /* Dot grid */ .dot-grid { background-image: radial-gradient( diff --git a/docs-site/app/layout.config.tsx b/docs-site/app/layout.config.tsx index 4e91b559..3245ab09 100644 --- a/docs-site/app/layout.config.tsx +++ b/docs-site/app/layout.config.tsx @@ -1,13 +1,22 @@ import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; +import { GitHubIcon } from "@/components/github-icon"; import { Logo } from "@/components/logo"; import { SlackIcon } from "@/components/slack-icon"; export const baseOptions: BaseLayoutProps = { nav: { - title: Logo, + title: , transparentMode: "top", }, links: [ + { + type: "icon", + label: "GitHub", + icon: , + text: "GitHub", + url: "https://github.com/kaelio/ktx", + external: true, + }, { type: "icon", label: "Join the ktx Slack community", diff --git a/docs-site/components/diagram-studio/flows.ts b/docs-site/components/diagram-studio/flows.ts deleted file mode 100644 index e63cc512..00000000 --- a/docs-site/components/diagram-studio/flows.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { type Edge, MarkerType, type Node } from "@xyflow/react"; - -import { C } from "./nodes"; - -const EDGE_COLOR = "#b3bcc4"; -const MARKER_COLOR = "#9aa6ad"; - -const labelStyle = { - fontFamily: "var(--font-inter), system-ui, sans-serif", - fontSize: 15, - fontWeight: 600, - fill: C.inkMuted, -}; -const labelBgStyle = { fill: "#ffffff", stroke: C.chipBorder, strokeWidth: 1 }; -const labelBg = { - labelBgPadding: [8, 4] as [number, number], - labelBgBorderRadius: 6, - labelStyle, - labelBgStyle, -}; - -const marker = { type: MarkerType.ArrowClosed, color: MARKER_COLOR, width: 16, height: 16 }; -const edgeStyle = { stroke: EDGE_COLOR, strokeWidth: 2 }; - -/* ============================== INGESTION =============================== */ - -const SRC_W = 300; -const SRC_H = 138; -const SRC_GAP = 24; -const srcY = (i: number) => i * (SRC_H + SRC_GAP); - -export const ingestionNodes: Node[] = [ - { - id: "title", - type: "title", - position: { x: 0, y: -96 }, - data: { - width: 560, - eyebrow: "1 · Ingestion", - title: "ktx builds your context layer", - }, - }, - { - id: "db", - type: "card", - position: { x: 0, y: srcY(0) }, - data: { - width: SRC_W, - height: SRC_H, - accent: C.teal, - rows: [ - { kind: "title", text: "Databases" }, - { kind: "desc", text: "Schemas, keys, query history" }, - { kind: "muted", text: "Postgres · Snowflake · BigQuery · …" }, - ], - handles: [{ side: "right", type: "source", id: "out" }], - }, - }, - { - id: "bi", - type: "card", - position: { x: 0, y: srcY(1) }, - data: { - width: SRC_W, - height: SRC_H, - accent: C.orange, - rows: [ - { kind: "title", text: "BI tools" }, - { kind: "desc", text: "Dashboards, explores, usage" }, - { kind: "muted", text: "Metabase · Looker · …" }, - ], - handles: [{ side: "right", type: "source", id: "out" }], - }, - }, - { - id: "model", - type: "card", - position: { x: 0, y: srcY(2) }, - data: { - width: SRC_W, - height: SRC_H, - accent: C.amber, - rows: [ - { kind: "title", text: "Modeling code" }, - { kind: "desc", text: "Metrics, models, joins, entities" }, - { kind: "muted", text: "dbt · LookML · MetricFlow · …" }, - ], - handles: [{ side: "right", type: "source", id: "out" }], - }, - }, - { - id: "docs", - type: "card", - position: { x: 0, y: srcY(3) }, - data: { - width: SRC_W, - height: SRC_H, - accent: C.emerald, - rows: [ - { kind: "title", text: "Docs & notes" }, - { kind: "desc", text: "Policies, definitions, notes" }, - { kind: "muted", text: "Notion · any text · …" }, - ], - handles: [{ side: "right", type: "source", id: "out" }], - }, - }, - { - id: "engine", - type: "engine", - position: { x: 420, y: 52 }, - data: { - width: 380, - height: 520, - steps: [ - { n: 1, title: "Source connectors", desc: "Read each source in its shape" }, - { n: 2, title: "Context builder", desc: "Evidence into proposed updates" }, - { n: 3, title: "Reconciliation", desc: "Merge with existing context" }, - { n: 4, title: "Validation", desc: "Check references & semantics" }, - ], - handles: [ - { side: "left", type: "target", id: "in" }, - { side: "right", type: "source", id: "out" }, - ], - }, - }, - { - id: "wiki", - type: "card", - position: { x: 900, y: 66 }, - data: { - width: 320, - height: 220, - accent: C.emerald, - rows: [ - { kind: "mono", text: "wiki/*.md", color: C.emerald }, - { kind: "title", text: "Wiki" }, - { kind: "chips", items: ["free-form", "auto-maintained"] }, - { kind: "desc", text: "Definitions, caveats, policies," }, - { kind: "desc", text: "and notes agents can search." }, - ], - handles: [{ side: "left", type: "target", id: "in" }], - }, - }, - { - id: "sl", - type: "card", - position: { x: 900, y: 338 }, - data: { - width: 320, - height: 220, - accent: C.teal, - rows: [ - { kind: "mono", text: "semantic-layer/*.yaml", color: C.teal }, - { kind: "title", text: "Semantic layer" }, - { kind: "chips", items: ["executable", "auto-maintained"] }, - { kind: "desc", text: "Metrics, joins, dimensions, and" }, - { kind: "desc", text: "filters ktx compiles into SQL." }, - ], - handles: [{ side: "left", type: "target", id: "in" }], - }, - }, -]; - -const ingestEdge = (source: string, target: string): Edge => ({ - id: `${source}-${target}`, - source, - target, - sourceHandle: "out", - targetHandle: "in", - type: "default", - style: edgeStyle, - markerEnd: marker, -}); - -export const ingestionEdges: Edge[] = [ - ingestEdge("db", "engine"), - ingestEdge("bi", "engine"), - ingestEdge("model", "engine"), - ingestEdge("docs", "engine"), - ingestEdge("engine", "wiki"), - ingestEdge("engine", "sl"), -]; - -/* =============================== RUNTIME ================================ */ - -export const runtimeNodes: Node[] = [ - { - id: "title", - type: "title", - position: { x: 0, y: -84 }, - data: { - width: 560, - eyebrow: "2 · Serving", - title: "agents query it through MCP", - }, - }, - { - id: "agent", - type: "card", - position: { x: 0, y: 115 }, - data: { - width: 280, - height: 190, - accent: C.neutral, - align: "center", - rows: [ - { kind: "title", text: "Your agent" }, - { kind: "muted", text: "Claude Code · Cursor" }, - { kind: "muted", text: "Codex · OpenCode" }, - ], - handles: [ - { side: "right", type: "source", id: "ask", top: "42%" }, - { side: "right", type: "target", id: "answer", top: "62%" }, - ], - }, - }, - { - id: "hub", - type: "hub", - position: { x: 420, y: 85 }, - data: { - width: 360, - height: 250, - rows: [ - "Search wiki + semantic layer", - "Return approved metrics", - "Compile metrics → SQL", - ], - handles: [ - { side: "left", type: "target", id: "ask", top: "42%" }, - { side: "left", type: "source", id: "answer", top: "62%" }, - { side: "right", type: "source", id: "to-context", top: "30%" }, - { side: "right", type: "source", id: "to-warehouse", top: "72%" }, - ], - }, - }, - { - id: "context", - type: "card", - position: { x: 920, y: 15 }, - data: { - width: 300, - height: 150, - accent: C.teal, - rows: [ - { kind: "title", text: "Context layer" }, - { kind: "mono", text: "wiki/*.md", color: C.emerald }, - { kind: "mono", text: "semantic-layer/*.yaml", color: C.teal }, - ], - handles: [{ side: "left", type: "target", id: "in" }], - }, - }, - { - id: "warehouse", - type: "card", - position: { x: 920, y: 255 }, - data: { - width: 300, - height: 150, - accent: C.slate, - rows: [ - { kind: "title", text: "Warehouse" }, - { - kind: "badge", - text: "read-only", - bg: "#ecf6f8", - border: "#bfe3ea", - color: C.teal, - }, - { kind: "desc", text: "Runs the compiled SQL" }, - ], - handles: [{ side: "left", type: "target", id: "in" }], - }, - }, -]; - -export const runtimeEdges: Edge[] = [ - { - id: "ask", - source: "agent", - sourceHandle: "ask", - target: "hub", - targetHandle: "ask", - type: "default", - label: "ask", - ...labelBg, - style: edgeStyle, - markerEnd: marker, - }, - { - id: "answer", - source: "hub", - sourceHandle: "answer", - target: "agent", - targetHandle: "answer", - type: "default", - label: "answer", - ...labelBg, - style: edgeStyle, - markerEnd: marker, - }, - { - id: "search", - source: "hub", - sourceHandle: "to-context", - target: "context", - targetHandle: "in", - type: "smoothstep", - label: "search + read", - ...labelBg, - style: edgeStyle, - markerStart: marker, - markerEnd: marker, - }, - { - id: "readonly", - source: "hub", - sourceHandle: "to-warehouse", - target: "warehouse", - targetHandle: "in", - type: "smoothstep", - label: "read-only", - ...labelBg, - style: edgeStyle, - markerStart: marker, - markerEnd: marker, - }, -]; diff --git a/docs-site/components/diagram-studio/mascot.tsx b/docs-site/components/diagram-studio/mascot.tsx deleted file mode 100644 index 467f6ee5..00000000 --- a/docs-site/components/diagram-studio/mascot.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Inlined ktx mascot, ported from assets/ktx-mascot.svg. - * - * - `light` renders the dark-bodied mascot for light surfaces. - * - `dark` renders the cream-bodied mascot for dark surfaces (e.g. the ktx - * hub panel), mirroring brand/ktx-mascot-dark.svg. - */ -export function KtxMascot({ - variant = "light", - size = 56, -}: { - variant?: "light" | "dark"; - size?: number; -}) { - const body = variant === "dark" ? "#F5F1EA" : "#1B3139"; - const eye = variant === "dark" ? "#1B3139" : "#F5F1EA"; - return ( - - - - - - - - - - - - ); -} diff --git a/docs-site/components/diagram-studio/nodes.tsx b/docs-site/components/diagram-studio/nodes.tsx deleted file mode 100644 index f648a905..00000000 --- a/docs-site/components/diagram-studio/nodes.tsx +++ /dev/null @@ -1,493 +0,0 @@ -"use client"; - -import { Handle, Position, type Node, type NodeProps } from "@xyflow/react"; - -import { KtxMascot } from "./mascot"; - -/** Fixed palette mirrored from the approved SVG diagrams so the exported PNG - * is theme-independent (one image that reads on light and dark GitHub). */ -export const C = { - ink: "#1b1b18", - inkSoft: "#57534e", - inkMuted: "#8c857f", - cardBorder: "#e2dfd9", - engineBg: "#15323a", - engineBorder: "#23474f", - cyan: "#55dced", - stepNum: "#06262c", - stepTitle: "#f3f1ec", - stepDesc: "#9fb6bc", - hubRow: "#eef4f5", - chipBg: "#faf9f6", - chipBorder: "#e7e5e4", - teal: "#0e7490", - emerald: "#059669", - orange: "#f97316", - amber: "#d97706", - slate: "#334155", - neutral: "#94a3b8", -} as const; - -const DISPLAY = "var(--font-display), system-ui, sans-serif"; -const BODY = "var(--font-inter), system-ui, sans-serif"; -const MONO = "var(--font-mono), ui-monospace, monospace"; - -const CARD_SHADOW = "0 3px 12px rgba(27, 49, 57, 0.10)"; -const ENGINE_SHADOW = "0 6px 22px rgba(2, 12, 15, 0.30)"; - -/** ktx logo mascot size, shared by the engine and hub headers. */ -const LOGO_SIZE = 56; - -type HandleSpec = { - side: "left" | "right"; - type: "source" | "target"; - id: string; - top?: string; -}; - -function Handles({ specs }: { specs?: HandleSpec[] }) { - if (!specs) return null; - return ( - <> - {specs.map((h) => ( - - ))} - - ); -} - -/* ------------------------------- Card node ------------------------------- */ - -type CardRow = - | { kind: "title"; text: string } - | { kind: "mono"; text: string; color: string } - | { kind: "desc"; text: string } - | { kind: "muted"; text: string } - | { kind: "chips"; items: string[] } - | { kind: "badge"; text: string; bg: string; border: string; color: string }; - -type CardData = { - width: number; - height: number; - accent: string; - align?: "center"; - rows: CardRow[]; - handles?: HandleSpec[]; -}; - -function gapFor(kind: CardRow["kind"], prev?: CardRow["kind"]): number { - if (!prev) return 0; - if (kind === "desc" && prev === "desc") return 3; - if (kind === "mono" && prev === "mono") return 2; - if (kind === "title") return 6; - return 10; -} - -function CardRowView({ row }: { row: CardRow }) { - switch (row.kind) { - case "title": - return ( - - {row.text} - - ); - case "mono": - return ( - - {row.text} - - ); - case "desc": - return ( - - {row.text} - - ); - case "muted": - return ( - - {row.text} - - ); - case "chips": - return ( -
- {row.items.map((c) => ( - - {c} - - ))} -
- ); - case "badge": - return ( - - {row.text} - - ); - } -} - -function CardNode({ data }: NodeProps>) { - const center = data.align === "center"; - return ( -
- - - {data.rows.map((row, i) => ( -
- -
- ))} -
- ); -} - -/* ------------------------------ Engine node ------------------------------ */ - -type EngineStep = { n: number; title: string; desc: string }; - -type EngineData = { - width: number; - height: number; - steps: EngineStep[]; - handles?: HandleSpec[]; -}; - -function EngineNode({ data }: NodeProps>) { - return ( -
- - -
- - - ktx - -
-
- {data.steps.map((s) => ( -
- - {s.n} - -
- - {s.title} - - - {s.desc} - -
-
- ))} -
-
- ); -} - -/* -------------------------------- Hub node ------------------------------- */ - -type HubData = { - width: number; - height: number; - rows: string[]; - handles?: HandleSpec[]; -}; - -function HubNode({ data }: NodeProps>) { - return ( -
- - -
- - - ktx - -
-
- {data.rows.map((r) => ( -
- - - {r} - -
- ))} -
-
- ); -} - -/* ------------------------------- Title node ------------------------------ */ - -type TitleData = { width: number; eyebrow: string; title: string }; - -function TitleNode({ data }: NodeProps>) { - return ( -
- - {data.eyebrow} - - - {data.title} - -
- ); -} - -export const nodeTypes = { - card: CardNode, - engine: EngineNode, - hub: HubNode, - title: TitleNode, -}; diff --git a/docs-site/components/diagram-studio/studio.tsx b/docs-site/components/diagram-studio/studio.tsx deleted file mode 100644 index 7b96ae7b..00000000 --- a/docs-site/components/diagram-studio/studio.tsx +++ /dev/null @@ -1,242 +0,0 @@ -"use client"; - -import "@xyflow/react/dist/style.css"; - -import { useCallback, useRef, useState } from "react"; -import { - Background, - BackgroundVariant, - type Edge, - getNodesBounds, - type Node, - ReactFlow, - ReactFlowProvider, - useEdgesState, - useNodesState, - useReactFlow, -} from "@xyflow/react"; -import { toPng } from "html-to-image"; - -import { - ingestionEdges, - ingestionNodes, - runtimeEdges, - runtimeNodes, -} from "./flows"; -import { nodeTypes } from "./nodes"; - -const EXPORT_PADDING = 48; -const EXPORT_PIXEL_RATIO = 2; - -function DiagramCanvasInner({ - initialNodes, - initialEdges, - fileName, - height, - dark, -}: { - initialNodes: Node[]; - initialEdges: Edge[]; - fileName: string; - height: number; - dark: boolean; -}) { - const wrapperRef = useRef(null); - const [nodes, , onNodesChange] = useNodesState(initialNodes); - const [edges, , onEdgesChange] = useEdgesState(initialEdges); - const { getNodes } = useReactFlow(); - const [busy, setBusy] = useState(false); - - const download = useCallback(async () => { - const viewport = wrapperRef.current?.querySelector( - ".react-flow__viewport", - ); - if (!viewport) return; - setBusy(true); - try { - await document.fonts.ready; - const bounds = getNodesBounds(getNodes()); - const outW = Math.ceil(bounds.width + EXPORT_PADDING * 2); - const outH = Math.ceil(bounds.height + EXPORT_PADDING * 2); - const tx = EXPORT_PADDING - bounds.x; - const ty = EXPORT_PADDING - bounds.y; - const dataUrl = await toPng(viewport, { - width: outW, - height: outH, - pixelRatio: EXPORT_PIXEL_RATIO, - // transparent background so one PNG works on light and dark GitHub - style: { - width: `${outW}px`, - height: `${outH}px`, - transform: `translate(${tx}px, ${ty}px) scale(1)`, - }, - }); - const link = document.createElement("a"); - link.download = fileName; - link.href = dataUrl; - link.click(); - } finally { - setBusy(false); - } - }, [fileName, getNodes]); - - return ( -
-
- -
-
- - - -
-
- ); -} - -function btnStyle(disabled: boolean): React.CSSProperties { - return { - fontFamily: "var(--font-inter), system-ui, sans-serif", - fontSize: 13, - fontWeight: 600, - padding: "7px 14px", - borderRadius: 8, - border: "1px solid #0e7490", - background: disabled ? "#9bbdc6" : "#0e7490", - color: "#ffffff", - cursor: disabled ? "default" : "pointer", - }; -} - -function DiagramCanvas(props: { - initialNodes: Node[]; - initialEdges: Edge[]; - fileName: string; - height: number; - dark: boolean; -}) { - return ( - - - - ); -} - -export function DiagramStudio() { - const [dark, setDark] = useState(false); - return ( -
-
-

- ktx diagram studio -

-

- Static diagrams. Export is a transparent 2× PNG framed to the node - bounds — the dark-background toggle is only for previewing. -

- -
- -
-

1 · Ingestion — building the context layer

- -
- -
-

2 · Serving — answering agents at runtime

- -
-
- ); -} - -const sectionTitle: React.CSSProperties = { - fontFamily: "var(--font-display), system-ui, sans-serif", - fontSize: 18, - fontWeight: 600, - color: "#1b1b18", - marginBottom: 12, -}; diff --git a/docs-site/components/github-stars.tsx b/docs-site/components/github-stars.tsx deleted file mode 100644 index d3b328a3..00000000 --- a/docs-site/components/github-stars.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Suspense } from "react"; -import { GitHubIcon } from "@/components/github-icon"; - -const REPO = "kaelio/ktx"; -const REPO_URL = `https://github.com/${REPO}`; -const API_URL = `https://api.github.com/repos/${REPO}`; - -async function fetchStarCount(): Promise { - try { - const res = await fetch(API_URL, { - headers: { Accept: "application/vnd.github+json" }, - // Revalidate hourly. GitHub's unauthenticated REST limit is 60 req/h per - // IP, so a single cached server-side fetch keeps the count fresh while - // never exposing visitors to rate limits or layout shift. - next: { revalidate: 3600 }, - }); - if (!res.ok) return null; - const data = (await res.json()) as { stargazers_count?: unknown }; - return typeof data.stargazers_count === "number" - ? data.stargazers_count - : null; - } catch { - return null; - } -} - -/** Compact, GitHub-style count: 847 → "847", 1234 → "1.2k", 12345 → "12.3k". */ -function formatStars(count: number): string { - if (count < 1000) return count.toLocaleString("en-US"); - const thousands = count / 1000; - const rounded = - thousands >= 100 ? Math.round(thousands) : Math.round(thousands * 10) / 10; - return `${rounded}k`; -} - -function StarGlyph() { - return ( - - ); -} - -async function StarsContent() { - const count = await fetchStarCount(); - const label = - count === null - ? "Star ktx on GitHub" - : `Star ktx on GitHub — ${count.toLocaleString("en-US")} stars`; - - return ( - - - - Star - - {count !== null && ( - - - {formatStars(count)} - - )} - - ); -} - -function StarsSkeleton() { - return ( -