diff --git a/website/README.md b/website/README.md index e7d5094..b1eec7a 100644 --- a/website/README.md +++ b/website/README.md @@ -6,16 +6,47 @@ Vite + React 19 + TypeScript documentation site for the KNDL project. ## Routes -Uses `createHashRouter` — all routes are prefixed with `#` (e.g. `/#/spec`) so the site works on static hosting without a server-side fallback. - -| Hash path | Page | Description | -|-----------|------|-------------| -| `/#/` | LandingPage | Hero, v0.2 feature highlights, quick-start snippet | -| `/#/spec` | SpecPage | Language reference with 8-domain tabbed examples + live playground | -| `/#/spec/full` | SpecFullPage | Full rendered SPECIFICATION.md | -| `/#/workflow` | WorkflowPage | 6-stage agent pipeline animation | -| `/#/explorer` | ExplorerPage | Force-directed graph explorer (pan/zoom/drag, detail panel) | -| `/#/mcp` | McpPage | MCP server docs and tool reference | +Uses `createBrowserRouter` with a GitHub Pages SPA fallback (`public/404.html` stashes the intended pathname in `sessionStorage`; `main.tsx` replays it on boot). Clean URLs (no `#`) are required for real SEO. + +| Path | Page | Description | +|------|------|-------------| +| `/` | LandingPage | Hero, v1.0 feature highlights, quick-start snippet | +| `/spec` | SpecPage | Language reference with 8-domain tabbed examples + live playground | +| `/spec/full` | SpecFullPage | Full rendered SPECIFICATION.md with sticky TOC | +| `/workflow` | WorkflowPage | 6-stage agent pipeline animation (per-stage insight + highlighted layer) | +| `/explorer` | ExplorerPage | Force-directed graph explorer (pan/zoom/drag, detail panel) | +| `/mcp` | McpPage | MCP server docs and tool reference | + +## Machine-readable discovery surfaces + +Everything below is served as a static file and is meant to be fetched by AI agents, search engines, and scripts. + +| URL | Format | Purpose | +|-----|--------|---------| +| `/llms.txt` | markdown | Concise [llmstxt.org](https://llmstxt.org) index of the whole project | +| `/llms-full.txt` | markdown | Spec + EBNF + example index concatenated — single-fetch bundle for LLMs | +| `/spec/SPECIFICATION.md` | markdown | Canonical language reference (mirrored from repo `spec/`) | +| `/spec/kndl.ebnf` | text | Authoritative EBNF grammar (mirrored from repo `spec/grammar/`) | +| `/examples/index.md` | markdown | Index of curated `.kndl` snippets | +| `/examples/*.kndl` | text | Runnable examples (basic-building, intent-overheat, process-shipment, query-aggregation, healthcare-observation, fintech-transaction, robotics-pose, logistics-trace) | +| `/sitemap.xml` | xml | Every indexable URL on the site | +| `/robots.txt` | text | Explicitly allows major AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot, …) | +| `/.well-known/security.txt` | text | Security contact per [securitytxt.org](https://securitytxt.org) | + +The Vite plugin `kndlSpecAssets` in `vite.config.ts` mirrors `spec/SPECIFICATION.md` and `spec/grammar/kndl.ebnf` from the repo root into the built output at these URLs and serves them in dev. It also regenerates `llms-full.txt` from source on each build. + +## SEO per route + +`src/components/SEO.tsx` is a tiny runtime component that each page renders. It updates: + +- `` and `<meta name="description">` +- `<meta name="robots">` with `max-image-preview:large` +- Open Graph (`og:title`, `og:description`, `og:url`, `og:type`, `og:image`, `og:site_name`) +- Twitter cards (`twitter:*`) +- `<link rel="canonical">` and `<link rel="alternate">` (llm index) +- Per-page JSON-LD (`TechArticle` for docs, `SoftwareSourceCode` for the landing page) + +`index.html` also contains a page-level JSON-LD `@graph` covering Organization, WebSite, SoftwareSourceCode, and TechArticle, plus a `<noscript>` pointer to the machine-readable surfaces for crawlers that don't execute JS. ## Stack diff --git a/website/index.html b/website/index.html index f0f95b1..4eb0181 100644 --- a/website/index.html +++ b/website/index.html @@ -3,14 +3,23 @@ <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <meta name="theme-color" content="#0b0f14" /> + + <!-- Default SEO — per-route pages override these via <SEO> at runtime. --> <title>KNDL — Knowledge Node Description Language - + + + + + + + - + @@ -19,16 +28,102 @@ - + + + + + + + + + + + + + +
+ + + + diff --git a/website/package.json b/website/package.json index a23ef43..e6c5f3d 100644 --- a/website/package.json +++ b/website/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -b && vite build", + "build": "tsc -b && vite build && node scripts/prerender.mjs", + "prerender": "node scripts/prerender.mjs", "preview": "vite preview", "test": "vitest run", "test:watch": "vitest", @@ -21,6 +22,7 @@ "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", + "@types/node": "^25.6.0", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "6.0.1", diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml index 0b76386..c8e9194 100644 --- a/website/pnpm-lock.yaml +++ b/website/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: '@testing-library/user-event': specifier: ^14.5.2 version: 14.6.1(@testing-library/dom@10.4.1) + '@types/node': + specifier: ^25.6.0 + version: 25.6.0 '@types/react': specifier: 19.2.14 version: 19.2.14 @@ -38,7 +41,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: 6.0.1 - version: 6.0.1(vite@8.0.8) + version: 6.0.1(vite@8.0.8(@types/node@25.6.0)) '@vitest/ui': specifier: 4.1.4 version: 4.1.4(vitest@4.1.4) @@ -50,10 +53,10 @@ importers: version: 6.0.2 vite: specifier: 8.0.8 - version: 8.0.8 + version: 8.0.8(@types/node@25.6.0) vitest: specifier: 4.1.4 - version: 4.1.4(@vitest/ui@4.1.4)(jsdom@26.1.0)(vite@8.0.8) + version: 4.1.4(@types/node@25.6.0)(@vitest/ui@4.1.4)(jsdom@26.1.0)(vite@8.0.8(@types/node@25.6.0)) packages: @@ -275,6 +278,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -717,6 +723,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + vite@8.0.8: resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1021,6 +1030,10 @@ snapshots: '@types/estree@1.0.8': {} + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -1029,10 +1042,10 @@ snapshots: dependencies: csstype: 3.2.3 - '@vitejs/plugin-react@6.0.1(vite@8.0.8)': + '@vitejs/plugin-react@6.0.1(vite@8.0.8(@types/node@25.6.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.8 + vite: 8.0.8(@types/node@25.6.0) '@vitest/expect@4.1.4': dependencies: @@ -1043,13 +1056,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@8.0.8)': + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0))': dependencies: '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8 + vite: 8.0.8(@types/node@25.6.0) '@vitest/pretty-format@4.1.4': dependencies: @@ -1078,7 +1091,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vitest: 4.1.4(@vitest/ui@4.1.4)(jsdom@26.1.0)(vite@8.0.8) + vitest: 4.1.4(@types/node@25.6.0)(@vitest/ui@4.1.4)(jsdom@26.1.0)(vite@8.0.8(@types/node@25.6.0)) '@vitest/utils@4.1.4': dependencies: @@ -1410,7 +1423,9 @@ snapshots: typescript@6.0.2: {} - vite@8.0.8: + undici-types@7.19.2: {} + + vite@8.0.8(@types/node@25.6.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -1418,12 +1433,13 @@ snapshots: rolldown: 1.0.0-rc.15 tinyglobby: 0.2.16 optionalDependencies: + '@types/node': 25.6.0 fsevents: 2.3.3 - vitest@4.1.4(@vitest/ui@4.1.4)(jsdom@26.1.0)(vite@8.0.8): + vitest@4.1.4(@types/node@25.6.0)(@vitest/ui@4.1.4)(jsdom@26.1.0)(vite@8.0.8(@types/node@25.6.0)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.8) + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -1440,9 +1456,10 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.8 + vite: 8.0.8(@types/node@25.6.0) why-is-node-running: 2.3.0 optionalDependencies: + '@types/node': 25.6.0 '@vitest/ui': 4.1.4(vitest@4.1.4) jsdom: 26.1.0 transitivePeerDependencies: diff --git a/website/public/.nojekyll b/website/public/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/website/public/.well-known/security.txt b/website/public/.well-known/security.txt new file mode 100644 index 0000000..0c51c2f --- /dev/null +++ b/website/public/.well-known/security.txt @@ -0,0 +1,7 @@ +# Security contact for the KNDL project. +# See https://securitytxt.org/ for the format. + +Contact: mailto:me@artdaw.com +Expires: 2027-04-24T00:00:00Z +Preferred-Languages: en +Canonical: https://kndl.artdaw.com/.well-known/security.txt diff --git a/website/public/404.html b/website/public/404.html new file mode 100644 index 0000000..ccd20a7 --- /dev/null +++ b/website/public/404.html @@ -0,0 +1,34 @@ + + + + + + + KNDL — redirecting… + + + + +

+ You were headed somewhere specific. If you are not redirected automatically, go to + kndl.artdaw.com. +

+

+ Agents looking for machine-readable content: + /llms.txt · + /spec/SPECIFICATION.md · + /spec/kndl.ebnf. +

+ + diff --git a/website/public/examples/basic-building.kndl b/website/public/examples/basic-building.kndl new file mode 100644 index 0000000..fe23679 --- /dev/null +++ b/website/public/examples/basic-building.kndl @@ -0,0 +1,26 @@ +// basic-building.kndl — sensor reading in a building, with decay. +// +// Demonstrates: node declaration, inline edge, ~confidence, ~source, +// ~valid (temporal range), ~decay (confidence degradation over time). + +node @building_7 :: Building { + name = "Klingelhöferstraße 7" + city = "Berlin" +} + +node @floor_3 :: Floor { + number = 3 + ~source "system://digital-twin" +} + +edge @floor_3 -[located_in]-> @building_7 + +node @sensor_t001 :: Temperature { + value = 22.5 °C + location -> @floor_3 + ~confidence 0.94 + ~source "sensor://bldg-7/floor-3/t-001" + ~valid 2026-04-10T14:00Z .. 2026-04-10T14:05Z + ~recorded 2026-04-10T14:05:03Z + ~decay 0.95 / 1h +} diff --git a/website/public/examples/fintech-transaction.kndl b/website/public/examples/fintech-transaction.kndl new file mode 100644 index 0000000..9ea8bef --- /dev/null +++ b/website/public/examples/fintech-transaction.kndl @@ -0,0 +1,41 @@ +// fintech-transaction.kndl — double-entry transaction with a signed source. +// +// Demonstrates: Money literals (Decimal + ISO 4217), balanced debits/credits +// as edges, ~signature for cryptographic provenance, ~tenant isolation. + +import { Account, Transaction } from "kndl://std/fin" + +context @tenant_acme { + ~tenant "acme" + ~access { read: ["role:acme-ops"], write: ["role:acme-admins"] } + + node @acct_cash :: Account { + name = "Operating Cash" + currency = "USD" + } + + node @acct_revenue :: Account { + name = "SaaS Revenue" + currency = "USD" + } + + // Transaction itself is a node; the two `posting` edges are the legs. + node @txn_0001 :: Transaction { + memo = "Invoice #A-1984 paid" + posted_at = 2026-04-22T14:05:00Z + ~source "db://erp/journal/0001" + ~signature { + alg = "ed25519" + key = "did:web:acme.example#sign-1" + sig = b"u8f+2lEq…==" + } + } + + // Debit 1200.00 USD to cash, credit 1200.00 USD to revenue. + edge @txn_0001 -[debits]-> @acct_cash { + amount = 1200.00d USD + } + edge @txn_0001 -[credits]-> @acct_revenue { + amount = 1200.00d USD + } +} diff --git a/website/public/examples/healthcare-observation.kndl b/website/public/examples/healthcare-observation.kndl new file mode 100644 index 0000000..7cead98 --- /dev/null +++ b/website/public/examples/healthcare-observation.kndl @@ -0,0 +1,55 @@ +// healthcare-observation.kndl — clinical observation with FHIR alignment. +// +// Demonstrates: Code for standard terminologies, bitemporal +// (~valid / ~observed / ~recorded), ~negated (strong negation under +// open-world assumption), ~classification for PHI, ~consent scoping. + +import { Patient, Observation, Condition } from "kndl://std/fhir" + +node @pat_001 :: Patient { + mrn = "MRN-42193" + ~classification "PHI" + ~consent -> @consent_0001 +} + +node @consent_0001 :: Consent { + scope = "treatment" + granted = 2026-03-12 +} + +// Positive observation: SpO2 reading from a calibrated device. +node @obs_4821 :: Observation { + subject -> @pat_001 + code = Code<"LOINC">("59408-5") // SpO2 in arterial blood + value = 96.0 + unit = "%" + ~source "fhir://hospital/Observation/4821" + ~observed 2026-04-20T09:14Z + ~recorded 2026-04-20T09:14:03Z + ~valid 2026-04-20T09:14Z .. 2026-04-20T09:14Z + ~confidence 0.98 + ~classification "PHI" +} + +// Strong negation: patient has *no* history of diabetes. +// Absence of this node would NOT mean the same thing — open-world. +node @pat_001.hx_diabetes :: Condition { + subject -> @pat_001 + code = Code<"ICD-10">("E11") + ~negated true + ~confidence 0.95 + ~source "user://dr-wong" + ~recorded 2026-04-20T09:10Z + ~classification "PHI" +} + +// Differential diagnosis — categorical uncertainty. +node @dx_working :: Condition { + subject -> @pat_001 + ~source "agent://clinical-reasoner" + ~uncertainty categorical { + "J45.9": 0.60, // asthma + "J44.9": 0.30, // COPD + "R05.9": 0.10 // cough, unspecified + } +} diff --git a/website/public/examples/index.html b/website/public/examples/index.html new file mode 100644 index 0000000..07fad03 --- /dev/null +++ b/website/public/examples/index.html @@ -0,0 +1,151 @@ + + + + + + + KNDL Examples — curated .kndl snippets + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
Curated snippets
+

KNDL Examples

+

+ Each file parses against the v1.0 grammar. Use them as starting points, + test fixtures, or tutorial material. Every file is self-contained. +

+
+ +
+ + basic-building.kndl + Node declaration, typed edges, ~confidence, ~source, ~valid, ~decay. Start here. + + + intent-overheat.kndl + Reactive intent with a query trigger and multiple emit actions. + + + process-shipment.kndl + Stateful process with five states, guarded transitions, and a compensate block. + + + query-aggregation.kndl + Multi-hop path pattern, group by, aggregation functions. + + + healthcare-observation.kndl + Code<System> terminologies, bitemporal annotations, ~negated, ~classification "PHI". + + + fintech-transaction.kndl + Money literals, double-entry via balanced edges, ~signature. + + + robotics-pose.kndl + Frame, Pose<Frame>, Gaussian ~uncertainty. + + + logistics-trace.kndl + GTIN identifiers, multi-hop ships_to* path query, chain-of-custody attestations. + +
+ +

+ Machine-readable index at index.md. + Spec: SPECIFICATION.md · + Grammar: kndl.ebnf · + LLM bundle: llms-full.txt. +

+
+ + diff --git a/website/public/examples/index.md b/website/public/examples/index.md new file mode 100644 index 0000000..eff7290 --- /dev/null +++ b/website/public/examples/index.md @@ -0,0 +1,18 @@ +# KNDL Examples + +Curated `.kndl` snippets. Each file is self-contained and parses against the +v1.0 grammar (`/spec/kndl.ebnf`). Use them as starting points, test fixtures, +or tutorial material. + +| File | Demonstrates | +|------|--------------| +| [`basic-building.kndl`](basic-building.kndl) | Node declaration, typed edges, `~confidence`, `~source`, `~valid`, `~decay`. | +| [`intent-overheat.kndl`](intent-overheat.kndl) | Reactive intent with a query trigger and multiple `emit` actions. | +| [`process-shipment.kndl`](process-shipment.kndl) | Stateful process with five states, transitions, and a `compensate` block. | +| [`query-aggregation.kndl`](query-aggregation.kndl) | Multi-hop path pattern, `group by`, aggregation functions. | +| [`healthcare-observation.kndl`](healthcare-observation.kndl) | `Code` for SNOMED/LOINC, bitemporal annotations, `~negated`, `~classification`. | +| [`fintech-transaction.kndl`](fintech-transaction.kndl) | `Money` literals, double-entry via balanced edges, `~signature`. | +| [`robotics-pose.kndl`](robotics-pose.kndl) | `Frame`, `Pose`, Gaussian `~uncertainty`. | +| [`logistics-trace.kndl`](logistics-trace.kndl) | GTIN identifiers, multi-hop `ships_to*` path, chain-of-custody signatures. | + +Everything here is MIT-licensed, same as the rest of the KNDL project. diff --git a/website/public/examples/intent-overheat.kndl b/website/public/examples/intent-overheat.kndl new file mode 100644 index 0000000..0627f5c --- /dev/null +++ b/website/public/examples/intent-overheat.kndl @@ -0,0 +1,44 @@ +// intent-overheat.kndl — reactive rule that emits an alert and work order +// when a high-confidence temperature exceeds a threshold. +// +// Demonstrates: intent declaration, query trigger, multiple emits, +// ~priority, ~cooldown. + +import { Temperature, Alert, WorkOrder, Notification } from "kndl://std/iot" + +intent @overheat_response :: AutoAction { + trigger = query { + match ?t :: Temperature + -[located_in]-> ?zone :: Zone + where + ?t.value > 30 °C + && ?t.~confidence > 0.8 + && ?t.~valid overlaps now + && ?zone.occupied == true + } + + do { + emit node :: Alert { + severity = "critical" + message = "Overheat detected" + related -> ?t + ~source "agent://claude-sonnet-4.6" + } + + emit node :: WorkOrder { + title = "HVAC review" + assignee -> @facilities_team + related -> ?zone + ~source "agent://claude-sonnet-4.6" + } + + emit node :: Notification { + channel = "slack://facilities" + message = "Auto-adjusted HVAC, see WO-{uuid()}" + ~priority 0.7 + } + } + + ~priority 0.9 + ~cooldown 15m +} diff --git a/website/public/examples/logistics-trace.kndl b/website/public/examples/logistics-trace.kndl new file mode 100644 index 0000000..7a4d382 --- /dev/null +++ b/website/public/examples/logistics-trace.kndl @@ -0,0 +1,41 @@ +// logistics-trace.kndl — package trace across multiple hubs. +// +// Demonstrates: GTIN source identifiers, multi-hop path queries, per-edge +// timestamps, chain-of-custody signatures via ~attestation. + +node @pkg_4821 :: Package { + gtin = "gtin:04012345678901" + sku = "KNDL-BOOK-001" + ~source "db://wms/packages/4821" +} + +node @hub_frankfurt :: Hub { code = "FRA" } +node @hub_singapore :: Hub { code = "SIN" } +node @hub_tokyo :: Hub { code = "NRT" } + +// Each edge has its own timestamp and signature of the scanning agent. +edge @pkg_4821 -[ships_to]-> @hub_frankfurt { + scanned_at = 2026-04-18T08:14:00Z + ~attestation -> @scan_fra_4821 +} +edge @pkg_4821 -[ships_to]-> @hub_singapore { + scanned_at = 2026-04-19T22:07:00Z + ~attestation -> @scan_sin_4821 +} +edge @pkg_4821 -[ships_to]-> @hub_tokyo { + scanned_at = 2026-04-20T11:52:00Z + ~attestation -> @scan_nrt_4821 +} + +node @scan_fra_4821 :: Attestation { + issuer = "did:web:logistics.example#fra-scanner" + claim = "scan:origin" + evidence = b"u8f+2lEq…==" +} +// (other scan attestations elided for brevity) + +// Example query: reconstruct the path for any package. +query trace { + match ?p = @pkg_4821 -[ships_to*]-> ?dest :: Hub + return { path: ?p, hops: len(?p), destination: ?dest } +} diff --git a/website/public/examples/process-shipment.kndl b/website/public/examples/process-shipment.kndl new file mode 100644 index 0000000..44da3cf --- /dev/null +++ b/website/public/examples/process-shipment.kndl @@ -0,0 +1,43 @@ +// process-shipment.kndl — stateful workflow for a shipment lifecycle. +// +// Demonstrates: process declaration, states, transitions with guard +// (`where`), `do` side effects, and `compensate` rollback on reversal. + +import { Alert, Workflow } from "kndl://std/agents" + +process @shipment_sm :: Workflow { + state picked + state packed + state shipped + state delivered + state lost { ~priority 1.0 } + + on pack_complete in picked -> packed + do { + emit update @shipment { packed_at = now() } + } + + on scan_at_dock in packed -> shipped + where ?event.location == "dock" + do { + emit update @shipment { shipped_at = now() } + } + + on delivery_scan in shipped -> delivered + compensate { + // Runs if a later event reverses delivery (e.g. recall). + emit node :: Alert { + severity = "warn" + message = "delivery rollback — shipment returned" + } + } + + on no_contact_48h in shipped -> lost + where elapsed(?shipment.shipped_at) > 48h + do { + emit node :: Alert { + severity = "critical" + message = "shipment missing for 48h" + } + } +} diff --git a/website/public/examples/query-aggregation.kndl b/website/public/examples/query-aggregation.kndl new file mode 100644 index 0000000..8c2f7c3 --- /dev/null +++ b/website/public/examples/query-aggregation.kndl @@ -0,0 +1,31 @@ +// query-aggregation.kndl — multi-hop paths + group by + aggregation. +// +// Demonstrates: path patterns with repetition, optional matches, group +// by clause (distinct from aggregation functions), `with edges N`. + +query campus_power_by_site_day { + // 1-to-5 `contains` hops from a campus down to any meter. + match ?m :: PowerMeasurement + <-[contains*1..5]- @campus + + match ?m -[at]-> ?site :: Site + + optional match ?fault :: SystemFault + -[affects]-> ?site + where ?fault.~valid overlaps now + + where + ?m.~confidence > 0.8 + && ?m.~observed overlaps 2026-Q1 + + group by ?site, day(?m.~observed) + + return { + site = ?site, + day = day(?m.~observed), + total_kwh = sum(?m.value), + peak_kwh = max(?m.value), + sample_n = count(?m), + has_fault = ?fault != null + } +} diff --git a/website/public/examples/robotics-pose.kndl b/website/public/examples/robotics-pose.kndl new file mode 100644 index 0000000..96f76a3 --- /dev/null +++ b/website/public/examples/robotics-pose.kndl @@ -0,0 +1,48 @@ +// robotics-pose.kndl — end-effector pose with coordinate frames and Gaussian uncertainty. +// +// Demonstrates: Frame as a first-class node, Pose parameterised type, +// ~uncertainty gaussian { mean, stddev } for aleatoric variability separate +// from the scalar ~confidence (epistemic). + +import { Frame, Pose } from "kndl://std/frames" + +node @world :: Frame { + id = "world" +} + +node @base :: Frame { + id = "base_link" + parent -> @world +} + +node @tool :: Frame { + id = "tool0" + parent -> @base +} + +// End-effector pose in the base frame. Scalar confidence = 0.99 +// (calibrated camera, bright scene). Aleatoric stddev of 3 cm on each axis. +node @grasp_target_0421 :: Pose<@base> { + x = 0.320 + y = 0.015 + z = 0.505 + qx = 0.0 + qy = 0.0 + qz = 0.0 + qw = 1.0 + + ~source "agent://perception/estimator-v3" + ~observed 2026-04-23T10:02:17.512Z + ~confidence 0.99 + ~uncertainty gaussian { mean: 0.0, stddev: 0.03 } +} + +// An uncertain candidate grasp — bounded by an interval instead. +node @grasp_candidate_a :: Pose<@base> { + x = 0.28 + y = 0.11 + z = 0.49 + qx = 0.0; qy = 0.0; qz = 0.0; qw = 1.0 + ~uncertainty interval { min: -0.05, max: 0.05 } + ~confidence 0.72 +} diff --git a/website/public/llms.txt b/website/public/llms.txt new file mode 100644 index 0000000..5fd322a --- /dev/null +++ b/website/public/llms.txt @@ -0,0 +1,66 @@ +# KNDL — Knowledge Node Description Language + +> KNDL (pronounced "kindle") is a graph-based knowledge representation language +> purpose-built for AI agents. Every assertion carries **confidence** (0.0–1.0), +> **provenance** (source URI or cryptographic signature), **temporal scope** +> (bitemporal: valid / observed / recorded), and **typed relationships** as +> first-class constructs. Knowledge + behaviour (intents and processes) live in +> the same language. Current version: **v1.0** (April 2026). + +This file follows the [llmstxt.org](https://llmstxt.org) convention. Everything +below links to plain-text or markdown resources optimised for machine reading. + +## Canonical sources + +- [Language specification (markdown)](https://kndl.artdaw.com/spec/SPECIFICATION.md): Complete v1.0 reference — types, meta-annotations, queries, processes, binary format, conformance levels. +- [EBNF grammar](https://kndl.artdaw.com/spec/kndl.ebnf): Authoritative formal grammar. +- [Full bundle](https://kndl.artdaw.com/llms-full.txt): Spec + EBNF + example index concatenated for single-fetch LLM consumption. + +## Examples + +- [Examples index](https://kndl.artdaw.com/examples/index.md): Curated list of runnable .kndl snippets. +- [Basic IoT / building](https://kndl.artdaw.com/examples/basic-building.kndl): Sensor node with confidence, decay, and typed edges. +- [Intent (overheat alert)](https://kndl.artdaw.com/examples/intent-overheat.kndl): Reactive trigger-action pattern over a query. +- [Process (shipment state machine)](https://kndl.artdaw.com/examples/process-shipment.kndl): Stateful workflow with compensation. +- [Query (aggregation)](https://kndl.artdaw.com/examples/query-aggregation.kndl): Multi-hop path query with group-by. +- [Healthcare (FHIR observation)](https://kndl.artdaw.com/examples/healthcare-observation.kndl): Coded terminologies and bitemporal annotations. +- [FinTech (transaction)](https://kndl.artdaw.com/examples/fintech-transaction.kndl): Money type, double-entry constraint, signature. +- [Robotics (pose with frame)](https://kndl.artdaw.com/examples/robotics-pose.kndl): Coordinate frames and Gaussian uncertainty. + +## Key concepts + +- **Node**: A typed, identified container for fields and meta-annotations. `node @sensor_01 :: Temperature { value = 22.5 °C; ~confidence 0.94 }`. +- **Edge**: Typed, directed relationship. `edge @room_204 -[located_in]-> @floor_2`. +- **Meta-annotations** (`~key value`): `~confidence`, `~valid`, `~recorded`, `~observed`, `~decay`, `~source`, `~negated`, `~uncertainty`, `~signature`, `~access`, ... +- **Intent**: Reactive trigger-action rule fired when a query matches. +- **Process**: Stateful workflow with `state`, `on ... in ... -> ...`, `compensate` clauses. +- **Query**: Declarative match with multi-hop paths, confidence predicates, `group by` aggregation. +- **Quantity**: Dimensionally safe values — `22.5 °C`, `5 m/s`, `100 kWh`. +- **Money**: `19.99d USD` with ISO 4217 currency codes. + +## Domain profiles + +KNDL ships conventional types and source-URI schemes for each major domain: +IoT/PropTech, FinTech, Healthcare (FHIR), Logistics, Robotics, Smart Factory +(ISA-95), Networking/Security, eCommerce. See §B of the specification. + +## Discovery + +- [Sitemap](https://kndl.artdaw.com/sitemap.xml) +- [Robots](https://kndl.artdaw.com/robots.txt) +- [Security contact](https://kndl.artdaw.com/.well-known/security.txt) +- [Source on GitHub](https://github.com/artdaw/KNDL) + +## Reference implementations + +- **Python library** (`pip install kndl`): parser → AST → compiler → KNDLGraph → serializer, with SQLite/PostgreSQL storage. +- **MCP server** (`pip install kndl-mcp`): exposes the graph as Model Context Protocol tools for Claude Desktop and other agents. + +## Pages (rendered for humans, but safe to fetch) + +- [Home](https://kndl.artdaw.com/): Design goals and quick-start. +- [Spec overview](https://kndl.artdaw.com/spec): Concepts with domain-tabbed examples. +- [Full spec (rendered)](https://kndl.artdaw.com/spec/full): Same content as SPECIFICATION.md with a sticky TOC. +- [Agent workflow](https://kndl.artdaw.com/workflow): 6-stage pipeline from ingest to communicate. +- [MCP server](https://kndl.artdaw.com/mcp): Tool reference and install guide. +- [Graph explorer](https://kndl.artdaw.com/explorer): Interactive force-directed visualiser with a live editor. diff --git a/website/public/robots.txt b/website/public/robots.txt new file mode 100644 index 0000000..01e73bd --- /dev/null +++ b/website/public/robots.txt @@ -0,0 +1,63 @@ +# KNDL — Knowledge Node Description Language +# https://kndl.artdaw.com/ + +User-agent: * +Allow: / + +# AI crawlers / training bots — explicitly allowed. +# KNDL is an open specification; we want it to be easy for agents to find. + +User-agent: GPTBot +Allow: / + +User-agent: OAI-SearchBot +Allow: / + +User-agent: ChatGPT-User +Allow: / + +User-agent: ClaudeBot +Allow: / + +User-agent: Claude-Web +Allow: / + +User-agent: anthropic-ai +Allow: / + +User-agent: PerplexityBot +Allow: / + +User-agent: Perplexity-User +Allow: / + +User-agent: Google-Extended +Allow: / + +User-agent: GoogleOther +Allow: / + +User-agent: CCBot +Allow: / + +User-agent: Applebot-Extended +Allow: / + +User-agent: Bytespider +Allow: / + +User-agent: FacebookBot +Allow: / + +User-agent: Meta-ExternalAgent +Allow: / + +# Primary machine-readable entry points: +# /llms.txt — concise index for LLMs (llmstxt.org) +# /llms-full.txt — spec + EBNF + examples bundled +# /spec/SPECIFICATION.md — canonical language reference +# /spec/kndl.ebnf — authoritative grammar +# /examples/ — curated .kndl examples +# /sitemap.xml — full URL index + +Sitemap: https://kndl.artdaw.com/sitemap.xml diff --git a/website/public/sitemap.xml b/website/public/sitemap.xml new file mode 100644 index 0000000..730fb9b --- /dev/null +++ b/website/public/sitemap.xml @@ -0,0 +1,69 @@ + + + + https://kndl.artdaw.com/ + 2026-04-24 + weekly + 1.0 + + + https://kndl.artdaw.com/spec + 2026-04-24 + weekly + 0.9 + + + https://kndl.artdaw.com/spec/full + 2026-04-24 + weekly + 0.9 + + + https://kndl.artdaw.com/workflow + 2026-04-24 + monthly + 0.7 + + + https://kndl.artdaw.com/mcp + 2026-04-24 + monthly + 0.7 + + + https://kndl.artdaw.com/explorer + 2026-04-24 + monthly + 0.6 + + + https://kndl.artdaw.com/spec/SPECIFICATION.md + 2026-04-24 + weekly + 0.9 + + + https://kndl.artdaw.com/spec/kndl.ebnf + 2026-04-24 + weekly + 0.8 + + + https://kndl.artdaw.com/llms.txt + 2026-04-24 + weekly + 0.9 + + + https://kndl.artdaw.com/llms-full.txt + 2026-04-24 + weekly + 0.9 + + + https://kndl.artdaw.com/examples/ + 2026-04-24 + monthly + 0.7 + + diff --git a/website/scripts/prerender.mjs b/website/scripts/prerender.mjs new file mode 100644 index 0000000..c979187 --- /dev/null +++ b/website/scripts/prerender.mjs @@ -0,0 +1,243 @@ +#!/usr/bin/env node +// Post-build prerender for GitHub Pages SEO. +// +// Vite builds a single dist/index.html. Direct hits on /spec, /workflow, etc. +// would otherwise return the 404 fallback (with HTTP 404 status). We stamp +// out one HTML shell per route so each path is served with status 200 and +// the correct , <meta>, <link rel="canonical">, Open Graph, Twitter, +// and JSON-LD already in the markup. +// +// At runtime, the <SEO> component in src/components/SEO.tsx re-applies the +// same values in place (no duplication, no flash) so we stay consistent. +// +// The SEO copy below MUST match the <SEO ... /> props in the corresponding +// page components. Keep them in sync; if they drift, the runtime values +// win but crawlers will see whatever we prerendered here. + +import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = dirname(here); +const DIST = join(root, "dist"); +const ORIGIN = "https://kndl.artdaw.com"; +const DEFAULT_IMAGE = `${ORIGIN}/kndl.png`; + +// ── Schema.org helpers ──────────────────────────────────────────────────── + +function techArticle({ headline, description, path, dateModified }) { + return { + "@context": "https://schema.org", + "@type": "TechArticle", + headline, + description, + url: ORIGIN + path, + mainEntityOfPage: ORIGIN + path, + dateModified: dateModified ?? new Date().toISOString().slice(0, 10), + inLanguage: "en", + publisher: { "@type": "Organization", name: "KNDL", url: ORIGIN }, + image: DEFAULT_IMAGE, + }; +} + +// ── Route metadata ──────────────────────────────────────────────────────── + +const ROUTES = [ + { + path: "/spec", + outDir: "spec", + title: "KNDL Language Specification — types, meta-annotations, domain profiles", + description: + "KNDL language reference: primitive types (Quantity, Money, Vector), meta-annotations (~confidence, ~valid, ~recorded, ~negated, ~uncertainty), query language with multi-hop paths, processes, and eight domain profiles (IoT, FinTech, Healthcare, Logistics, Robotics, Smart Factory, Networking, eCommerce).", + type: "article", + keywords: + "KNDL specification, knowledge graph language, AI agent memory, confidence score, temporal decay, provenance, EBNF grammar", + jsonLd: techArticle({ + headline: "KNDL Language Specification", + description: + "Reference for the Knowledge Node Description Language — types, meta-annotations, queries, processes, and domain profiles.", + path: "/spec", + }), + }, + { + path: "/spec/full", + outDir: "spec/full", + title: "KNDL Specification v1.0 — Full Reference", + description: + "Full KNDL v1.0 specification: lexical structure, type system (Quantity, Money, Vector, Frame, Code, Localized), core constructs, query language with multi-hop paths, processes, uncertainty model, serialization (text + binary), and conformance levels. Raw markdown available at /spec/SPECIFICATION.md.", + type: "article", + keywords: + "KNDL spec v1.0, EBNF grammar, knowledge graph, agent memory, semantic data, confidence, provenance", + dateModified: "2026-04-23", + jsonLd: techArticle({ + headline: "KNDL Language Specification v1.0", + description: "Complete reference for the Knowledge Node Description Language version 1.0.", + path: "/spec/full", + dateModified: "2026-04-23", + }), + }, + { + path: "/workflow", + outDir: "workflow", + title: "KNDL Agent Workflow — 6-Stage Pipeline (Ingest → Communicate)", + description: + "Walk through how an AI agent actually uses KNDL: Ingest raw input, Produce confidence-scored nodes, Merge into the knowledge graph, Reason with probabilistic queries, Act via intents, Communicate grounded responses. Per-stage insights and integration architecture.", + type: "article", + keywords: + "AI agent workflow, KNDL pipeline, knowledge graph reasoning, intent-action pattern, agent memory", + jsonLd: techArticle({ + headline: "KNDL Agent Workflow — 6-Stage Pipeline", + description: + "How an AI agent uses KNDL as a cognitive substrate across Ingest, Produce, Merge, Reason, Act, and Communicate stages.", + path: "/workflow", + }), + }, + { + path: "/mcp", + outDir: "mcp", + title: "KNDL MCP Server — Use KNDL from Claude & AI Agents", + description: + "KNDL MCP server docs: 13 Model Context Protocol tools (kndl_parse, kndl_add_node, kndl_query_nodes, kndl_neighborhood, kndl_add_intent, and more). Install with pip, connect to Claude Desktop or any MCP-compatible agent.", + type: "article", + keywords: + "KNDL MCP, Model Context Protocol, Claude Desktop, AI agent tools, knowledge graph tools, MCP server", + jsonLd: techArticle({ + headline: "KNDL MCP Server", + description: + "Expose the KNDL knowledge graph as Model Context Protocol tools for Claude and other AI agents.", + path: "/mcp", + }), + }, + { + path: "/explorer", + outDir: "explorer", + title: "KNDL Graph Explorer — Interactive Force-Directed Visualization", + description: + "Visualise a KNDL knowledge graph live. Edit KNDL source in the browser and watch nodes, typed edges, and confidence scores render as a force-directed graph. Zoom, pan, drag, and inspect node details.", + type: "website", + keywords: + "KNDL graph explorer, knowledge graph visualization, force-directed layout, KNDL playground", + jsonLd: techArticle({ + headline: "KNDL Graph Explorer", + description: + "Interactive force-directed visualization of KNDL knowledge graphs with a live editor.", + path: "/explorer", + }), + }, +]; + +// ── HTML stamping ───────────────────────────────────────────────────────── + +function esc(s) { + return String(s) + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """); +} + +function setTagContent(html, matcher, replacement) { + if (!matcher.test(html)) { + throw new Error(`Prerender: couldn't find tag matching ${matcher}`); + } + return html.replace(matcher, replacement); +} + +function stamp(template, route) { + const url = ORIGIN + route.path; + let html = template; + + html = setTagContent( + html, + /<title>[\s\S]*?<\/title>/, + `<title>${esc(route.title)}`, + ); + + html = setTagContent( + html, + /]*>/, + ``, + ); + + html = setTagContent( + html, + /]*>/, + ``, + ); + + html = setTagContent( + html, + /]*>/, + ``, + ); + + html = setTagContent( + html, + /]*>/, + ``, + ); + html = setTagContent( + html, + /]*>/, + ``, + ); + html = setTagContent( + html, + /]*>/, + ``, + ); + html = setTagContent( + html, + /]*>/, + ``, + ); + + html = setTagContent( + html, + /]*>/, + ``, + ); + html = setTagContent( + html, + /]*>/, + ``, + ); + html = setTagContent( + html, + /]*>/, + ``, + ); + + // Append per-route JSON-LD right before . The runtime + // component will target the same `data-seo="page"` script tag and + // overwrite its textContent with the same value on mount. + if (route.jsonLd) { + const jsonLdScript = ` \n `; + html = html.replace(/\s*<\/head>/, `\n${jsonLdScript}`); + } + + return html; +} + +// ── Main ────────────────────────────────────────────────────────────────── + +function main() { + const template = readFileSync(join(DIST, "index.html"), "utf8"); + let count = 0; + for (const r of ROUTES) { + const html = stamp(template, r); + const outDir = join(DIST, r.outDir); + mkdirSync(outDir, { recursive: true }); + writeFileSync(join(outDir, "index.html"), html); + count++; + } + console.log(`prerender: stamped ${count} route shell${count === 1 ? "" : "s"}`); + for (const r of ROUTES) { + console.log(` ${r.path.padEnd(14)} -> dist/${r.outDir}/index.html`); + } +} + +main(); diff --git a/website/src/components/SEO.tsx b/website/src/components/SEO.tsx new file mode 100644 index 0000000..08447e7 --- /dev/null +++ b/website/src/components/SEO.tsx @@ -0,0 +1,167 @@ +import { useEffect } from "react"; + +const ORIGIN = "https://kndl.artdaw.com"; +const DEFAULT_IMAGE = `${ORIGIN}/kndl.png`; + +export interface SEOProps { + title: string; + description: string; + /** Path including leading slash, e.g. "/spec". Used for canonical + OG URLs. */ + path: string; + /** Absolute image URL; defaults to site-wide OG image. */ + image?: string; + /** Open Graph type — "website" for index pages, "article" for spec/docs. */ + type?: "website" | "article"; + /** Optional JSON-LD object (schema.org). Gets serialised into a script tag. */ + jsonLd?: Record | Record[]; + /** ISO 8601 date for schema.org dateModified. */ + dateModified?: string; + /** Optional comma-separated keywords. */ + keywords?: string; +} + +export function SEO({ + title, + description, + path, + image = DEFAULT_IMAGE, + type = "website", + jsonLd, + dateModified, + keywords, +}: SEOProps) { + useEffect(() => { + const url = ORIGIN + path; + + document.title = title; + document.documentElement.setAttribute("lang", "en"); + + setMeta("description", description, "name"); + if (keywords) setMeta("keywords", keywords, "name"); + setMeta("robots", "index,follow,max-image-preview:large", "name"); + + setMeta("og:title", title, "property"); + setMeta("og:description", description, "property"); + setMeta("og:url", url, "property"); + setMeta("og:type", type, "property"); + setMeta("og:image", image, "property"); + setMeta("og:site_name", "KNDL", "property"); + + setMeta("twitter:card", "summary_large_image", "name"); + setMeta("twitter:title", title, "name"); + setMeta("twitter:description", description, "name"); + setMeta("twitter:url", url, "name"); + setMeta("twitter:image", image, "name"); + + if (dateModified) { + setMeta("article:modified_time", dateModified, "property"); + } + + setLink("canonical", url); + setLink("alternate", "/llms.txt", "llm"); + + if (jsonLd) { + setJsonLd(jsonLd); + } else { + clearJsonLd(); + } + }, [title, description, path, image, type, dateModified, keywords, JSON.stringify(jsonLd)]); + + return null; +} + +function setMeta(key: string, content: string, attr: "name" | "property") { + const selector = `meta[${attr}="${key}"]`; + let tag = document.head.querySelector(selector); + if (!tag) { + tag = document.createElement("meta"); + tag.setAttribute(attr, key); + document.head.appendChild(tag); + } + tag.setAttribute("content", content); +} + +function setLink(rel: string, href: string, id?: string) { + const selector = id + ? `link[rel="${rel}"][data-id="${id}"]` + : `link[rel="${rel}"]:not([data-id])`; + let tag = document.head.querySelector(selector); + if (!tag) { + tag = document.createElement("link"); + tag.rel = rel; + if (id) tag.setAttribute("data-id", id); + document.head.appendChild(tag); + } + tag.href = href; +} + +function setJsonLd(data: Record | Record[]) { + let tag = document.head.querySelector( + 'script[type="application/ld+json"][data-seo="page"]', + ); + if (!tag) { + tag = document.createElement("script"); + tag.type = "application/ld+json"; + tag.setAttribute("data-seo", "page"); + document.head.appendChild(tag); + } + tag.textContent = JSON.stringify(data); +} + +function clearJsonLd() { + const tag = document.head.querySelector( + 'script[type="application/ld+json"][data-seo="page"]', + ); + if (tag) tag.remove(); +} + +// ── Schema.org helpers ───────────────────────────────────────────────────── + +export const ORG_SCHEMA = { + "@context": "https://schema.org", + "@type": "Organization", + name: "KNDL", + url: ORIGIN, + logo: DEFAULT_IMAGE, + sameAs: [ + "https://github.com/artdaw/KNDL", + ], +}; + +export function techArticleSchema(params: { + headline: string; + description: string; + path: string; + dateModified?: string; +}) { + return { + "@context": "https://schema.org", + "@type": "TechArticle", + headline: params.headline, + description: params.description, + url: ORIGIN + params.path, + mainEntityOfPage: ORIGIN + params.path, + dateModified: params.dateModified ?? new Date().toISOString().slice(0, 10), + inLanguage: "en", + publisher: { + "@type": "Organization", + name: "KNDL", + url: ORIGIN, + }, + image: DEFAULT_IMAGE, + }; +} + +export function softwareSourceCodeSchema() { + return { + "@context": "https://schema.org", + "@type": "SoftwareSourceCode", + name: "KNDL — Knowledge Node Description Language", + codeRepository: "https://github.com/artdaw/KNDL", + programmingLanguage: "KNDL", + url: ORIGIN, + description: + "A graph-based knowledge representation language for AI agents — typed nodes, confidence scores, temporal decay, cryptographic provenance, and native intent/process blocks.", + license: "https://opensource.org/licenses/MIT", + }; +} diff --git a/website/src/main.tsx b/website/src/main.tsx index 10f524a..ded9718 100644 --- a/website/src/main.tsx +++ b/website/src/main.tsx @@ -1,6 +1,6 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import { createHashRouter, RouterProvider } from "react-router"; +import { createBrowserRouter, RouterProvider } from "react-router"; import App from "./App"; import LandingPage from "./pages/LandingPage"; import SpecPage from "./pages/SpecPage"; @@ -10,7 +10,18 @@ import McpPage from "./pages/McpPage"; import ExplorerPage from "./pages/ExplorerPage"; import "./styles/tokens.css"; -const router = createHashRouter([ +// GitHub Pages SPA fallback: 404.html stashes the intended path in +// sessionStorage and redirects to '/'. Here we replay it before the +// router reads window.location. +const redirectedPath = sessionStorage.getItem("kndl:redirect"); +if (redirectedPath) { + sessionStorage.removeItem("kndl:redirect"); + if (redirectedPath !== window.location.pathname + window.location.search) { + window.history.replaceState(null, "", redirectedPath); + } +} + +const router = createBrowserRouter([ { path: "/", element: , diff --git a/website/src/pages/ExplorerPage.tsx b/website/src/pages/ExplorerPage.tsx index 3a33eb0..acfa1c7 100644 --- a/website/src/pages/ExplorerPage.tsx +++ b/website/src/pages/ExplorerPage.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { parseKNDL, typeColor, GraphData, GraphNodeData } from "../utils/kndlParser"; import { useForceLayout, Position } from "../hooks/useForceLayout"; +import { SEO, techArticleSchema } from "../components/SEO"; import styles from "./ExplorerPage.module.css"; // ── Sample KNDL ─────────────────────────────────────────────────────────────── @@ -273,6 +274,19 @@ export default function ExplorerPage() { return (
+ {/* Toolbar */}
diff --git a/website/src/pages/LandingPage.tsx b/website/src/pages/LandingPage.tsx index f15bb48..703de6d 100644 --- a/website/src/pages/LandingPage.tsx +++ b/website/src/pages/LandingPage.tsx @@ -1,5 +1,6 @@ import { Link } from "react-router"; import CodeBlock from "../components/CodeBlock"; +import { SEO, softwareSourceCodeSchema } from "../components/SEO"; import styles from "./LandingPage.module.css"; const HERO_EXAMPLE = `node @sensor_t001 :: Temperature { @@ -54,6 +55,14 @@ const FEATURES = [ export default function LandingPage() { return (
+ {/* Hero */}
diff --git a/website/src/pages/McpPage.tsx b/website/src/pages/McpPage.tsx index b72e7ce..b48867d 100644 --- a/website/src/pages/McpPage.tsx +++ b/website/src/pages/McpPage.tsx @@ -1,4 +1,5 @@ import CodeBlock from "../components/CodeBlock"; +import { SEO, techArticleSchema } from "../components/SEO"; import styles from "./McpPage.module.css"; const TOOLS = [ @@ -67,6 +68,19 @@ confidence to 0.6 due to sensor uncertainty"`; export default function McpPage() { return (
+
{/* Header */} diff --git a/website/src/pages/SpecFullPage.module.css b/website/src/pages/SpecFullPage.module.css index 5eb358b..1db596d 100644 --- a/website/src/pages/SpecFullPage.module.css +++ b/website/src/pages/SpecFullPage.module.css @@ -56,6 +56,13 @@ color: var(--text-dim); } +.headerActions { + display: flex; + align-items: center; + gap: 18px; + flex-wrap: wrap; +} + .rawLink { font-family: var(--font-mono); font-size: 12px; diff --git a/website/src/pages/SpecFullPage.tsx b/website/src/pages/SpecFullPage.tsx index 7268331..c15a1e2 100644 --- a/website/src/pages/SpecFullPage.tsx +++ b/website/src/pages/SpecFullPage.tsx @@ -11,6 +11,7 @@ import { BlockRenderer, } from "../utils/mdRenderer"; import type { TocEntry } from "../utils/mdRenderer"; +import { SEO, techArticleSchema } from "../components/SEO"; import styles from "./SpecFullPage.module.css"; // ── Parse once at module level ──────────────────────────────────────────────── @@ -90,6 +91,21 @@ export default function SpecFullPage() { return (
+ {/* Page header */}
@@ -98,14 +114,20 @@ export default function SpecFullPage() {

Language Specification

v1.0.0 · April 2026
- - View raw ↗ - +
diff --git a/website/src/pages/SpecPage.tsx b/website/src/pages/SpecPage.tsx index 5757eb2..99a5dba 100644 --- a/website/src/pages/SpecPage.tsx +++ b/website/src/pages/SpecPage.tsx @@ -6,6 +6,7 @@ import { useState } from "react"; import { Link } from "react-router"; import { highlightKNDL } from "../components/CodeBlock"; +import { SEO, techArticleSchema } from "../components/SEO"; import styles from "./SpecPage.module.css"; // ── Inline code block ───────────────────────────────────────────────────────── @@ -730,6 +731,19 @@ export default function SpecPage() { const [activeDomain, setActiveDomain] = useState("iot"); return (
+ {/* Hero */}
diff --git a/website/src/pages/WorkflowPage.tsx b/website/src/pages/WorkflowPage.tsx index 4d99e1e..654fd4e 100644 --- a/website/src/pages/WorkflowPage.tsx +++ b/website/src/pages/WorkflowPage.tsx @@ -6,6 +6,7 @@ import { useState, useEffect } from "react"; import styles from "./WorkflowPage.module.css"; import { highlightKNDL } from "../components/CodeBlock"; +import { SEO, techArticleSchema } from "../components/SEO"; // ── Integration architecture layers (rendered per-stage with one highlighted) ─ @@ -421,6 +422,19 @@ export default function WorkflowPage() { return (
+
{/* Header */}
diff --git a/website/tsconfig.node.json b/website/tsconfig.node.json index 9c43072..f1f1afc 100644 --- a/website/tsconfig.node.json +++ b/website/tsconfig.node.json @@ -11,7 +11,8 @@ "noEmit": true, "strict": true, "noUnusedLocals": true, - "noUnusedParameters": true + "noUnusedParameters": true, + "types": ["node"] }, "include": ["vite.config.ts"] } diff --git a/website/vite.config.ts b/website/vite.config.ts index 45931a3..283601d 100644 --- a/website/vite.config.ts +++ b/website/vite.config.ts @@ -1,8 +1,110 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig, type Plugin } from "vitest/config"; import react from "@vitejs/plugin-react"; +import { readFileSync, existsSync } from "node:fs"; +import { resolve } from "node:path"; + +// ── Plugin: expose spec/SPECIFICATION.md and spec/grammar/kndl.ebnf at +// stable URLs (/spec/SPECIFICATION.md, /spec/kndl.ebnf) in both dev +// and build. Also emits /llms-full.txt = preamble + spec + EBNF so +// agents can slurp everything in one request. +function kndlSpecAssets(): Plugin { + const repoRoot = resolve(__dirname, ".."); + const specPath = resolve(repoRoot, "spec/SPECIFICATION.md"); + const ebnfPath = resolve(repoRoot, "spec/grammar/kndl.ebnf"); + const examplesIndexPath = resolve(__dirname, "public/examples/index.md"); + + const read = (p: string) => (existsSync(p) ? readFileSync(p, "utf8") : ""); + + const buildLlmsFull = () => { + const spec = read(specPath); + const ebnf = read(ebnfPath); + const examplesIdx = read(examplesIndexPath); + return [ + "# KNDL — Full Machine-Readable Bundle", + "", + "This file bundles the KNDL specification, EBNF grammar, and example", + "index into a single document for LLM consumption. It is regenerated", + "at build time from the canonical sources in the repository.", + "", + "Canonical URLs:", + "- Spec: https://kndl.artdaw.com/spec/SPECIFICATION.md", + "- Grammar: https://kndl.artdaw.com/spec/kndl.ebnf", + "- Examples: https://kndl.artdaw.com/examples/", + "- Index: https://kndl.artdaw.com/llms.txt", + "", + "---", + "", + "# PART 1 — SPECIFICATION", + "", + spec, + "", + "---", + "", + "# PART 2 — EBNF GRAMMAR", + "", + "```ebnf", + ebnf, + "```", + "", + "---", + "", + "# PART 3 — EXAMPLE INDEX", + "", + examplesIdx, + "", + ].join("\n"); + }; + + const serveMap: Record string> = { + "/spec/SPECIFICATION.md": () => read(specPath), + "/spec/kndl.ebnf": () => read(ebnfPath), + "/llms-full.txt": () => buildLlmsFull(), + }; + + return { + name: "kndl-spec-assets", + + // Dev: serve files straight from disk at their stable URLs. + configureServer(server) { + server.middlewares.use((req, res, next) => { + const url = (req.url || "").split("?")[0]; + const handler = serveMap[url]; + if (!handler) return next(); + const body = handler(); + const ext = url.endsWith(".ebnf") + ? "text/plain; charset=utf-8" + : url.endsWith(".md") + ? "text/markdown; charset=utf-8" + : "text/plain; charset=utf-8"; + res.setHeader("Content-Type", ext); + res.setHeader("Access-Control-Allow-Origin", "*"); + res.end(body); + }); + }, + + // Build: emit the files into the final bundle. + generateBundle() { + this.emitFile({ + type: "asset", + fileName: "spec/SPECIFICATION.md", + source: read(specPath), + }); + this.emitFile({ + type: "asset", + fileName: "spec/kndl.ebnf", + source: read(ebnfPath), + }); + this.emitFile({ + type: "asset", + fileName: "llms-full.txt", + source: buildLlmsFull(), + }); + }, + }; +} export default defineConfig({ - plugins: [react()], + plugins: [react(), kndlSpecAssets()], resolve: { alias: { "@": "/src", @@ -20,3 +122,4 @@ export default defineConfig({ css: false, }, }); +