diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index a2678a8..5e75e07 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -16,22 +16,6 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
- analyze-python:
- name: Analyze Python
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v4
- with:
- languages: python
- build-mode: none # Python is interpreted; no build needed
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v4
- with:
- category: "/language:python"
analyze-typescript:
name: Analyze TypeScript
runs-on: ubuntu-latest
diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml
index 9932679..651b7a8 100644
--- a/.github/workflows/deploy-website.yml
+++ b/.github/workflows/deploy-website.yml
@@ -30,7 +30,7 @@ jobs:
- uses: actions/setup-node@v4
with:
- node-version: 20
+ node-version: 24
cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml
diff --git a/.github/workflows/kndl-workflow.yml b/.github/workflows/kndl-workflow.yml
index 31314b6..6ddd012 100644
--- a/.github/workflows/kndl-workflow.yml
+++ b/.github/workflows/kndl-workflow.yml
@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
- node-version: "20"
+ node-version: "24"
cache: "npm"
cache-dependency-path: packages/kndl-memory/package-lock.json
- name: Install dependencies
diff --git a/README.md b/README.md
index fa2ffff..02dc9f6 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,9 @@
[](https://github.com/artdaw/KNDL/actions/workflows/kndl-workflow.yml)
[](LICENSE)
[](packages/kndl-memory)
-[](https://kndl.artdaw.com)
+[](https://kndl.artdaw.com)
+
+### **[→ kndl.artdaw.com](https://kndl.artdaw.com)** — live docs, protocol reference, interactive examples, explorer
diff --git a/website/public/schema/kndl-memory.schema.json b/website/public/schema/kndl-memory.schema.json
new file mode 100644
index 0000000..1acc3cf
--- /dev/null
+++ b/website/public/schema/kndl-memory.schema.json
@@ -0,0 +1,39 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://kndl.artdaw.com/schema/fact.schema.json",
+ "title": "KNDL Fact",
+ "description": "One immutable assertion in the KNDL memory protocol. Every fact carries confidence, decay, provenance, and bitemporal validity. Facts are stored as individual JSON-LD files and never edited — updates are made via supersession.",
+ "type": "object",
+ "required": ["@id", "@type", "statement", "confidence", "source", "validFrom", "recordedAt"],
+ "additionalProperties": true,
+ "properties": {
+ "@context": { "type": ["string", "object"], "description": "JSON-LD context. Recommended: https://kndl.artdaw.com/context/v1.jsonld" },
+ "@id": { "type": "string", "minLength": 1, "description": "Globally unique fact identifier. Recommended pattern: fact:---" },
+ "@type": { "type": "string", "description": "Usually 'Fact'. Use 'Action' for intents/rules." },
+
+ "statement": { "type": "string", "minLength": 1, "description": "Plain-language one-sentence assertion" },
+ "subject": { "type": "string", "description": "Entity URI (for structured triple queries, e.g. customer:9281)" },
+ "predicate": { "type": "string", "description": "Property name (for structured triple queries, e.g. creditScore)" },
+ "object": { "description": "Value or object URI" },
+
+ "confidence": { "type": "number", "minimum": 0, "maximum": 1, "description": "Epistemic certainty 0.0–1.0. Reserve 1.0 for axiomatic facts only." },
+ "decay": { "type": "string", "pattern": "^[0-9.]+/[0-9.]+(?:ns|us|ms|s|m|h|d|w|mo|y)$", "description": "Half-life decay spec: rate/window, e.g. '0.5/30d' halves confidence every 30 days." },
+
+ "source": { "type": "string", "description": "URI of asserting entity. Use human:// for user input, doi: for publications." },
+ "validFrom": { "type": "string", "format": "date-time", "description": "ISO datetime — when the fact became true in the world" },
+ "validUntil": { "type": "string", "format": "date-time", "description": "ISO datetime — explicit expiry (omit for open-ended)" },
+ "observedAt": { "type": "string", "format": "date-time", "description": "ISO datetime — when an agent or sensor directly observed it (vs. heard about it)" },
+ "recordedAt": { "type": "string", "format": "date-time", "description": "ISO datetime — when it entered memory (auto-set by the store)" },
+
+ "supersedes": { "type": "string", "description": "@id of the older fact this assertion replaces. The old fact is hidden from active queries but preserved for as-of time-travel." },
+ "derivedFrom": { "type": "array", "items": { "type": "string" }, "description": "@ids of source facts this was inferred from" },
+ "inference": { "type": "string", "description": "@id of the rule that produced this derived fact" },
+ "negated": { "type": "boolean", "description": "true = this fact is known-false (positive negation under open-world assumption). Never substitute for 'unknown'." },
+
+ "classification": { "type": "string", "enum": ["PII", "PHI", "PCI", "CONFIDENTIAL", "INTERNAL", "PUBLIC"], "description": "Sensitivity label. PHI facts are filtered by default; require allowPhi:true and consent." },
+ "consent": { "type": "string", "description": "@id of the consent scope covering PHI access" },
+ "retention": { "type": "string", "description": "ISO duration or date for scheduled deletion, e.g. P7Y" },
+ "tenant": { "type": "string", "description": "Opaque multi-tenant isolation key" },
+ "tags": { "type": "array", "items": { "type": "string" }, "description": "Free-form labels for search and grouping" }
+ }
+}
diff --git a/website/src/main.tsx b/website/src/main.tsx
index 1e55c31..c7767aa 100644
--- a/website/src/main.tsx
+++ b/website/src/main.tsx
@@ -38,6 +38,12 @@ const router = createBrowserRouter([
{ path: "spec", element: },
{ path: "spec/full", element: },
{ path: "workflow", element: },
+ // Schema file aliases — redirect to the canonical static file so the
+ // browser fetches the raw JSON rather than showing a router error.
+ // (Static files in public/ take precedence once deployed; these
+ // redirects are a fallback for the GitHub Pages SPA 404 intercept.)
+ { path: "schema/kndl-memory.schema.json", element: },
+ { path: "schema/kndl-context.jsonld", element: },
],
},
]);
diff --git a/website/src/pages/ProtocolPage.tsx b/website/src/pages/ProtocolPage.tsx
index 8ebafc5..690a42b 100644
--- a/website/src/pages/ProtocolPage.tsx
+++ b/website/src/pages/ProtocolPage.tsx
@@ -1,6 +1,90 @@
import { SEO, techArticleSchema } from "../components/SEO";
import styles from "./ProtocolPage.module.css";
+// ── JSON syntax highlighter ───────────────────────────────────────────────────
+// Single-pass tokenizer — no external library.
+
+type Token = { type: string; value: string };
+
+function tokenizeJson(src: string): Token[] {
+ const tokens: Token[] = [];
+ let i = 0;
+ while (i < src.length) {
+ // Whitespace / structure
+ if (/[\s,[\]{}]/.test(src[i])) {
+ let v = "";
+ while (i < src.length && /[\s,[\]{}]/.test(src[i])) v += src[i++];
+ tokens.push({ type: "punct", value: v });
+ continue;
+ }
+ // Colon
+ if (src[i] === ":") {
+ tokens.push({ type: "punct", value: src[i++] });
+ continue;
+ }
+ // String
+ if (src[i] === '"') {
+ let v = '"';
+ i++;
+ while (i < src.length && src[i] !== '"') {
+ if (src[i] === "\\") { v += src[i++]; }
+ v += src[i++];
+ }
+ v += '"';
+ i++;
+ // Determine if it's a key (next non-space char is ":")
+ let j = i;
+ while (j < src.length && src[j] === " ") j++;
+ const isKey = src[j] === ":";
+ const raw = v.slice(1, -1);
+ if (isKey) {
+ const type = raw.startsWith("@") ? "at-key" : "key";
+ tokens.push({ type, value: v });
+ } else {
+ // URL vs plain string value
+ const type = raw.startsWith("http") || raw.startsWith("fact:") || raw.startsWith("human://") || raw.startsWith("sensor://") ? "url" : "string";
+ tokens.push({ type, value: v });
+ }
+ continue;
+ }
+ // Number
+ if (/[-\d]/.test(src[i])) {
+ let v = "";
+ while (i < src.length && /[-\d.eE+]/.test(src[i])) v += src[i++];
+ tokens.push({ type: "number", value: v });
+ continue;
+ }
+ // true / false / null
+ if (src.startsWith("true", i)) { tokens.push({ type: "bool", value: "true" }); i += 4; continue; }
+ if (src.startsWith("false", i)) { tokens.push({ type: "bool", value: "false" }); i += 5; continue; }
+ if (src.startsWith("null", i)) { tokens.push({ type: "null", value: "null" }); i += 4; continue; }
+ tokens.push({ type: "punct", value: src[i++] });
+ }
+ return tokens;
+}
+
+const TOKEN_COLORS: Record = {
+ "at-key": "var(--accent)",
+ "key": "var(--accent2)",
+ "string": "var(--accent4)",
+ "url": "#7dd3fc",
+ "number": "#f97316",
+ "bool": "var(--accent3)",
+ "null": "var(--text-dim)",
+ "punct": "var(--text-dim)",
+};
+
+function JsonHighlight({ src }: { src: string }) {
+ const tokens = tokenizeJson(src);
+ return (
+
+ {tokens.map((t, i) => (
+ {t.value}
+ ))}
+
+ );
+}
+
const FACT_EXAMPLE = `{
"@id": "fact:customer-9281-creditscore-20260425T235249Z-b6c88774",
"@type": "Fact",
@@ -91,7 +175,7 @@ export default function ProtocolPage() {
fact-customer-9281-creditscore.fact.json
-
{FACT_EXAMPLE}
+