diff --git a/.github/workflows/kndl-workflow.yml b/.github/workflows/kndl-workflow.yml
index 89cb062..31314b6 100644
--- a/.github/workflows/kndl-workflow.yml
+++ b/.github/workflows/kndl-workflow.yml
@@ -10,32 +10,19 @@ permissions:
contents: read
jobs:
- python:
- name: Python library
+ node-test:
+ name: kndl-memory (Node)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: astral-sh/setup-uv@v4
+ - uses: actions/setup-node@v4
with:
- version: "latest"
+ node-version: "20"
+ cache: "npm"
+ cache-dependency-path: packages/kndl-memory/package-lock.json
- name: Install dependencies
- run: cd packages/python && uv sync --all-extras
- - name: Lint
- run: cd packages/python && uv run ruff check src tests && uv run mypy src
+ run: cd packages/kndl-memory && npm install
+ - name: Build
+ run: cd packages/kndl-memory && npm run build
- name: Test
- run: cd packages/python && uv run pytest -v --tb=short
-
- mcp-server:
- name: MCP server
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: astral-sh/setup-uv@v4
- with:
- version: "latest"
- - name: Install dependencies
- run: cd packages/mcp-server && uv sync --all-extras
- - name: Lint
- run: cd packages/mcp-server && uv run ruff check src tests && uv run mypy src
- - name: Test
- run: cd packages/mcp-server && uv run pytest -v --tb=short
+ run: cd packages/kndl-memory && npm test
diff --git a/Makefile b/Makefile
index 2f0ddbc..6e8caed 100644
--- a/Makefile
+++ b/Makefile
@@ -1,69 +1,63 @@
.PHONY: help \
- py-install py-test py-test-cov py-lint py-build \
- mcp-install mcp-run mcp-run-http mcp-test \
+ kndl-install kndl-build kndl-test kndl-lint kndl-eval \
web-install web-dev web-build web-preview \
- install build test lint clean
+ mcp-run mcp-run-http \
+ install build test clean
+
+NODE = node
+PNPM = pnpm
# ── Default: list targets ─────────────────────────────────────────────────────
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
- awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}'
-
-# ── Python library ────────────────────────────────────────────────────────────
-py-install: ## Install Python library deps
- cd packages/python && uv sync --all-extras
-
-py-test: ## Run Python tests
- cd packages/python && uv run pytest -v
+ awk 'BEGIN {FS = ":.*?## "}; {printf " %-22s %s\n", $$1, $$2}'
-py-test-cov: ## Run Python tests with coverage
- cd packages/python && uv run pytest --cov=src/kndl --cov-report=term-missing
+# ── @kndl/memory package ──────────────────────────────────────────────────────
+kndl-install: ## Install @kndl/memory deps
+ cd packages/kndl-memory && $(PNPM) install
-py-lint: ## Lint Python (ruff + mypy)
- cd packages/python && uv run ruff check src tests && uv run mypy src
+kndl-build: ## Build @kndl/memory (ESM + type declarations)
+ cd packages/kndl-memory && $(PNPM) run build
-py-build: ## Build Python wheel
- cd packages/python && uv build
+kndl-test: ## Run @kndl/memory tests (stores + remote, 36 tests)
+ cd packages/kndl-memory && $(PNPM) test
-# ── MCP server ────────────────────────────────────────────────────────────────
-mcp-install: ## Install MCP server deps
- cd packages/mcp-server && uv sync --all-extras
+kndl-lint: ## Type-check @kndl/memory
+ cd packages/kndl-memory && npx tsc --noEmit
-mcp-run: ## Start MCP server (stdio)
- cd packages/mcp-server && uv run python -m kndl_mcp
+kndl-eval: ## Run eval and publish results to website/public/eval/results.json
+ cd packages/kndl-memory && npx tsx eval/runner.ts \
+ --out ../../website/public/eval/results.json
-mcp-run-http: ## Start MCP server (HTTP, port 8000)
- cd packages/mcp-server && uv run python -m kndl_mcp --http
+publish-eval: kndl-eval web-build ## Run eval, publish results, build website
-mcp-lint: ## Lint MCP (ruff + mypy)
- cd packages/mcp-server && uv run ruff check src tests && uv run mypy src
+# ── MCP server ────────────────────────────────────────────────────────────────
+mcp-run: ## Start kndl-memory-mcp (stdio, default storage)
+ cd packages/kndl-memory && $(NODE) dist/server.js
-mcp-test: ## Run MCP integration tests
- cd packages/mcp-server && uv run pytest -v
+mcp-run-http: ## Start kndl-memory-mcp (HTTP port 8000, DEBUG logging)
+ cd packages/kndl-memory && LOG_LEVEL=DEBUG $(NODE) dist/server.js --http
# ── Website ───────────────────────────────────────────────────────────────────
-web-install: ## Install website pnpm deps
- cd website && pnpm install
+web-install: ## Install website deps
+ cd website && $(PNPM) install
web-dev: ## Start Vite dev server
- cd website && pnpm run dev
+ cd website && $(PNPM) run dev
web-build: ## Build website for production
- cd website && pnpm run build
+ cd website && $(PNPM) run build
web-preview: ## Preview production build
- cd website && pnpm run preview
+ cd website && $(PNPM) run preview
# ── Aggregates ────────────────────────────────────────────────────────────────
-install: py-install mcp-install ## Install all packages
-
-build: py-build ## Build all packages
+install: kndl-install web-install ## Install all packages
-test: py-test mcp-test ## Run all test suites
+build: kndl-build web-build ## Build all packages
-lint: py-lint mcp-lint ## Run all linters
+test: kndl-test ## Run all tests
-clean: ## Remove build artifacts and venvs
- cd packages/python && rm -rf dist .venv
- cd packages/mcp-server && rm -rf dist .venv
+clean: ## Remove build artifacts and node_modules
+ cd packages/kndl-memory && rm -rf dist node_modules
cd website && rm -rf dist node_modules
diff --git a/README.md b/README.md
index dca6abf..fa2ffff 100644
--- a/README.md
+++ b/README.md
@@ -1,141 +1,243 @@
-[](./kndl.png)
+
-# KNDL — Knowledge Node Description Language
+
-Give your AI agent a memory it can reason over.
+# KNDL — Knowledge Node Data Link
+
+**The format Anthropic Memory was waiting for**
[](https://github.com/artdaw/KNDL/actions/workflows/kndl-workflow.yml)
-[](https://github.com/artdaw/KNDL/actions/workflows/codeql.yml)
-
-KNDL is a language for describing knowledge as a directed graph. Every fact is a **node** with typed fields. Relationships are **edges** with types and weights. Every assertion carries a **confidence score**, optional provenance, and a temporal validity window — so agents always know how much to trust what they know.
-
-```kndl
-node @sensor_t001 :: Temperature<°C> {
- value = 22.5
- unit = "°C"
- location -> @building_7
- ~confidence 0.94
- ~source "sensor://bldg-7/t-001"
- ~valid 2026-04-10T14:00Z .. *
- ~decay 0.95 / 1h
- ~uncertainty Gaussian { mean = 22.5 stddev = 0.3 }
+[](LICENSE)
+[](packages/kndl-memory)
+[](https://kndl.artdaw.com)
+
+
+
+---
+
+Anthropic just shipped Memory for agents — a filesystem.
+But filesystems are dumb about confidence, time, and source.
+Agents fill them with markdown that **can't be queried, won't decay, and loses provenance the moment it's written.**
+
+**KNDL is the format that makes Memory actually smart.**
+
+```
+Anthropic Memory = WHERE filesystem, persistence, permissions
+KNDL = WHAT the format of files Claude writes
+kndl-mcp / CLI = HOW query, decay, contradiction detection, provenance
+```
+
+---
+
+## Get started in 60 seconds
+
+```bash
+git clone https://github.com/artdaw/kndl
+cd kndl/packages/kndl-memory
+pnpm install && pnpm build
+```
+
+Add to **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
+
+```json
+{
+ "mcpServers": {
+ "kndl": {
+ "command": "node",
+ "args": ["/path/to/kndl/packages/kndl-memory/dist/server.js"],
+ "env": { "KNDL_STORAGE": "sqlite:///Users/you/.kndl/memory.db" }
+ }
+ }
}
+```
+
+Restart Claude Desktop. Now ask: *"Remember that Alice is a staff engineer with confidence 0.95."*
+
+Claude writes a `.fact.json` file. Next session, it reads it back — with the confidence level, provenance, and decay exactly as you wrote them.
+
+---
-intent @overheat :: Action {
- trigger = @sensor_t001.value > 28.0
- do { emit node :: Alert { severity = "critical" } }
- ~priority 0.9
- ~cooldown 5m
+## Why not just markdown?
+
+| | Markdown | **KNDL** |
+|---|:---:|:---:|
+| Know when a fact went stale | ✗ | ✅ decay + `effective_confidence` |
+| Surface contradictions between sources | ✗ | ✅ ranked by recency + confidence |
+| Trace a claim to its origin | ✗ | ✅ `source` URI + `derivedFrom` chain |
+| Time-travel ("what did we believe last week?") | ✗ | ✅ `as_of` bitemporal query |
+| Open-world negation ("no known allergy") | ✗ | ✅ `negated: true` |
+| PHI / PII sensitivity gating | ✗ | ✅ `classification` filtered by default |
+
+---
+
+## The fact shape
+
+One immutable file per assertion. Every fact is a JSON-LD document:
+
+```json
+{
+ "@context": "https://kndl.artdaw.com/context/v1.jsonld",
+ "@id": "fact:alice-role-20260426t100000z-ab12cd34",
+ "@type": "Fact",
+
+ "statement": "Alice is a staff engineer on the payments team",
+ "subject": "person:alice",
+ "predicate": "role",
+ "object": "staff engineer, payments",
+
+ "confidence": 0.95,
+ "decay": "0.5/180d",
+
+ "source": "human://gleb",
+ "validFrom": "2026-04-26T10:00:00Z",
+ "recordedAt": "2026-04-26T10:00:00Z"
}
```
-## Why KNDL
+`decay: "0.5/180d"` — confidence halves every 180 days.
+Effective confidence at query time: `confidence × rate ^ (elapsed / window)`.
+
+**Facts are immutable.** Updates create a new fact with `supersedes` pointing at the old one.
+History is preserved for time-travel; only the latest is shown in active queries.
+
+---
+
+## MCP tools (11)
+
+| Tool | What it does |
+|------|-------------|
+| `assert_fact` | Write a new immutable fact |
+| `query_facts` | Read active facts with decay-adjusted confidence at `as_of` |
+| `contradictions` | Find conflicting facts, ranked by recency + confidence |
+| `supersede_fact` | Replace a fact — old version preserved for time-travel |
+| `as_of` | Bitemporal query: what did memory believe at timestamp X? |
+| `provenance_chain` | Walk `derivedFrom` + `supersedes` backward to the source |
+| `subscribe` | Get notified when a fact changes |
+| `unsubscribe` | Cancel a subscription |
+| `list_subscriptions` | List active subscriptions |
+| `sync_memory_store` | Pull from an Anthropic Memory Store (needs `ANTHROPIC_API_KEY`) |
+| `list_memory_stores` | List configured remote stores + last-sync timestamps |
+
+---
-Existing formats were designed for humans (Markdown), machines (JSON), or documents (XML). None were designed for **agents** — entities that need to reason about knowledge, track certainty, attribute provenance, and traverse relationships.
+## Storage
-| Feature | JSON / YAML | KNDL |
-|---------|-------------|------|
-| Confidence scores | ✗ | ✓ native |
-| Temporal decay | ✗ | ✓ native |
-| Provenance tracking | ✗ | ✓ native |
-| Typed graph edges | ✗ | ✓ native |
-| Trigger-action intents | ✗ | ✓ native |
-| Uncertainty distributions | ✗ | ✓ native |
-| Parameterised types | ✗ | ✓ native |
+Set `KNDL_STORAGE` to choose where facts live:
-## Packages
+| URL | Backend | Use case |
+|-----|---------|----------|
+| `fs:/memory` | Filesystem (one `.fact.json` per fact) | **Anthropic Memory mount** |
+| `sqlite:./kndl.db` | SQLite, WAL mode | **Claude Desktop standalone** ← default |
+| `duckdb:./kndl.duckdb` | DuckDB columnar | Analytical workloads |
+| `supabase:?key=` | Supabase + RLS | Multi-tenant cloud |
-| Package | Version | Description |
-|---------|---------|-------------|
-| [`packages/python`](packages/python) | 1.0.0 | Reference implementation — parser, compiler, graph API, storage |
-| [`packages/mcp-server`](packages/mcp-server) | 1.0.0 | MCP server — use KNDL from Claude Desktop and any MCP client |
-| [`website`](website) | — | Documentation site (React + Vite) |
+---
-## Quickstart
+## Use with Anthropic Memory (Skill)
-**Python library**
+Copy the Skill bundle into your agent's skills directory:
```bash
-pip install kndl
+# Drop into your Anthropic Memory store
+cp -r skills/kndl-memory/SKILL.md /memory/skills/
+cp -r skills/kndl-memory/context/ /memory/context/
```
-```python
-import kndl
+Claude then automatically writes structured facts instead of markdown, queries them with decay applied, and surfaces contradictions before answering.
-graph = kndl.compile("""
-node @alice :: Person {
- name = "Alice"
- role = "Engineer"
- ~confidence 0.95
- ~source "agent://hr"
-}
-edge @alice -[works_at]-> @acme { ~weight 1.0 }
-""")
+---
+
+## CLI reference
+
+```bash
+export KNDL_STORAGE=fs:./memory # or sqlite:, duckdb:, supabase:
+
+# Write a fact
+kndl add \
+ --statement "Alice is a staff engineer, payments" \
+ --subject person:alice --predicate role \
+ --confidence 0.95 --source "human://gleb" \
+ --decay "0.5/180d" --valid-from now
+
+# Query active facts (decay applied)
+kndl query --subject person:alice
+
+# Find contradictions
+kndl contradict --subject person:alice --predicate role
-engineers = graph.query_nodes(type_name="Person", min_confidence=0.9)
-print(kndl.serialize(graph))
+# Time-travel
+kndl as-of 2026-01-01T00:00:00Z --subject person:alice
+
+# Audit trail
+kndl provenance --id fact:alice-role-...
+
+# Sync from Anthropic Memory Store
+kndl remote add --provider anthropic --store-id store_abc --label personal
+kndl remote pull personal
```
-**MCP server (Claude Desktop)**
+---
+
+## HTTP server (multi-agent / Goose)
+
+Run one server for shared memory across Claude Desktop + Goose + LM Studio:
```bash
-pip install kndl-mcp
+KNDL_STORAGE=sqlite:./shared.db \
+ node packages/kndl-memory/dist/server.js --http
+# → http://localhost:8000/mcp
```
-Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
+**Goose** (`~/.config/goose/config.yaml`):
+```yaml
+extensions:
+ kndl:
+ type: streamable_http
+ url: http://localhost:8000/mcp
+```
+**LM Studio** (`~/.lmstudio/mcp.json`):
```json
-{
- "mcpServers": {
- "kndl": { "command": "uvx", "args": ["kndl-mcp"] }
- }
-}
+{ "mcpServers": { "kndl": { "type": "http", "url": "http://localhost:8000/mcp" } } }
```
-Restart Claude Desktop, then ask: *"Remember that Alice is a senior engineer with confidence 0.95."*
-
-## Features
-
-- **Parameterised types** — `Observation>`, `Quantity<°C>`
-- **Processes & state machines** — `process @sm :: StateMachine { ... }`
-- **Uncertainty distributions** — `~uncertainty Gaussian { mean = X stddev = Y }`
-- **Multi-hop query patterns** — `-[T*]->`, `-[T*3]->`, `-[T*2..5]->`
-- **Undirected typed edges** — `-[T]-` in addition to `->` and `<-`
-- **Expanded meta-annotations** — `~recorded`, `~observed`, `~negated`, `~deadline`, `~classification`, `~retention`
-- **Extended duration units** — `ns`, `us`, `mo`, `y`
+---
-## Repository layout
+## Repository
```
-packages/
- python/ Python reference implementation (kndl)
- mcp-server/ MCP server (kndl-mcp)
-website/ Documentation site
-spec/ KNDL language specification (Markdown)
-.github/ CI workflows
+packages/kndl-memory/ @kndl/memory — TypeScript library + MCP server + CLI
+ src/
+ core.ts decay math, fact construction, shared query algorithms
+ types.ts Fact, FactInput, FactStore interface
+ stores/ fs · sqlite · duckdb · supabase backends
+ remote/ Anthropic Memory Store sync
+ server.ts kndl-memory-mcp MCP server (stdio + HTTP)
+ cli.ts kndl CLI
+ eval/runner.ts eval runner — 33 questions, Claude-as-judge
+ tests/ 36 passing tests
+
+skills/kndl-memory/ Claude Skill bundle (drop into /memory/skills/)
+ SKILL.md conventions Claude follows
+ context/v1.jsonld JSON-LD @context
+ examples/ 8 domain bundles · 42 facts
+
+website/ docs — kndl.artdaw.com (React + Vite)
```
-## Development
+---
+
+## Eval
+
+KNDL must beat vanilla (facts pasted in system prompt) on ≥ 70% of 33 questions to ship.
```bash
-# Python library
-cd packages/python
-uv sync --all-extras
-uv run pytest -v # 245 tests
-uv run ruff check src tests
-uv run mypy src
-
-# MCP server
-cd packages/mcp-server
-uv sync --all-extras
-uv run pytest tests/ -v # 80 tests
-
-# Website
-cd website
-pnpm install
-pnpm dev
+export ANTHROPIC_API_KEY=sk-ant-...
+make publish-eval # runs eval → writes results → builds website
```
+---
+
## License
-MIT
+MIT — [kndl.artdaw.com](https://kndl.artdaw.com)
diff --git a/kndl.png b/kndl.png
index 90f11c3..beb92b8 100644
Binary files a/kndl.png and b/kndl.png differ
diff --git a/llms.txt b/llms.txt
deleted file mode 100644
index 7dfef2c..0000000
--- a/llms.txt
+++ /dev/null
@@ -1,367 +0,0 @@
-# KNDL — Knowledge Node Description Language
-
-> **Live site:** https://artdaw.github.io/KNDL/
-> **Spec version:** v1.0.0
-> **Python library:** `kndl` 1.0.0 · **MCP server:** `kndl-mcp` 1.0.0
-
-## What it is
-
-A declarative language and runtime for representing structured knowledge as a typed, confidence-annotated directed graph. Designed for AI agents that need to reason over connected facts with temporal validity, decay, and provenance.
-
-Core concepts:
-- **Node** — a typed entity with arbitrary fields and meta-annotations
-- **Edge** — a typed, directed (or undirected) relationship between two nodes; first-class, not just a field
-- **Intent** — a reactive trigger-action rule that fires when a condition is met
-- **Process** — a state-machine block with typed transitions and `goto` actions
-- **Meta** — every node and edge carries: confidence (0–1), source URI, validity window, decay rate, tags, priority, cooldown, recorded, observed, negated, deadline, classification, retention, uncertainty
-- **Confidence decay** — exponential: `confidence × (decay_rate ^ (elapsed / decay_duration_seconds))`
-- **Parameterised types** — `Type ` generic schemas; e.g. `Observation>`
-- **Uncertainty distributions** — `~uncertainty Gaussian { mean = X stddev = Y }`
-- **Multi-hop queries** — `-[T*]->`, `-[T*3]->`, `-[T*2..5]->` path patterns
-
-## Syntax example
-
-```kndl
-// Parameterised type
-type Observation where C <: Code {
- code : C
- value : Float
- subject : Patient
-}
-
-node @obs_4421 :: Observation> {
- code = "8310-5"
- value = 38.2
- unit = "°C"
- subject -> @patient_p001
- ~confidence 0.96
- ~source "ehr://encounter-4421"
- ~recorded 2026-04-10T09:15:00Z
- ~observed 2026-04-10T09:10:00Z
- ~uncertainty Gaussian { mean = 38.2 stddev = 0.1 }
-}
-
-// Process / state machine
-process @grasp_sm :: StateMachine {
- states = ["idle", "approaching", "grasping", "lifting"]
- initial = "idle"
- @idle -> @approaching { trigger = "pickup_cmd" }
- @approaching -> @grasping { trigger = @joint_01.angle > 30 }
- @grasping -> @lifting { trigger = @joint_01.torque > 1.8 }
-}
-
-// Intent (reactive rule)
-intent @alert :: Action {
- trigger = @obs_4421.value > 39.0
- do { emit node :: Alert { severity = "critical" } }
- ~priority 0.9
- ~cooldown 5m
-}
-
-// Undirected edge
-edge @room_204 -[adjacent_to]- @room_205
-
-// Multi-hop query
-query supply_chain {
- match ?supplier -[supplies*2..4]-> ?product
- where ?supplier.~confidence > 0.8
- return { supplier: ?supplier, product: ?product }
-}
-```
-
-## Repository layout
-
-```
-kndl/
-├── spec/
-│ ├── SPECIFICATION.md authoritative language spec
-│ └── grammar/kndl.ebnf full EBNF grammar
-├── packages/
-│ ├── python/ Python 3.12+ reference implementation (kndl 1.0.0)
-│ │ ├── src/kndl/
-│ │ │ ├── lexer.py tokenizer → list[Token]
-│ │ │ ├── parser.py recursive-descent → Program AST
-│ │ │ ├── ast_nodes.py all AST dataclasses (TypeExpr params, EdgePattern hops, EmitAction goto, ProcessDecl)
-│ │ │ ├── compiler.py AST → KNDLGraph
-│ │ │ ├── graph.py KNDLGraph, GraphNode, GraphEdge, GraphIntent, KNDLMeta
-│ │ │ ├── serializer.py KNDLGraph → KNDL text
-│ │ │ ├── storage.py KNDLStorage Protocol + create_storage() factory
-│ │ │ ├── backends/
-│ │ │ │ ├── sqlite_backend.py stdlib sqlite3, zero extra deps
-│ │ │ │ └── postgres_backend.py psycopg2, JSONB + GIN indexes
-│ │ │ ├── py.typed PEP 561 marker
-│ │ │ └── __init__.py public API: parse(), compile(), serialize(), tokenize()
-│ │ └── tests/
-│ │ ├── test_kndl.py 52 tests — baseline pipeline
-│ │ ├── test_kndl_extended.py 65 tests — edge cases, roundtrip
-│ │ ├── test_storage.py 24 tests — backends, factory, persistence
-│ │ ├── test_processes.py 72 tests — processes, decimal, group-by, reverse edges
-│ │ └── test_advanced_types.py 32 tests — parameterised types, multi-hop, undirected, uncertainty, goto
-│ └── mcp-server/ MCP server wrapping the Python library (kndl-mcp 1.0.0)
-│ ├── src/kndl_mcp/
-│ │ ├── server.py FastMCP server, 14 tools, 5 resources
-│ │ ├── _meta.py _duration_to_seconds() — supports ns/us/ms/s/m/h/d/w/mo/y
-│ │ ├── __main__.py entry: python -m kndl_mcp [--http]
-│ │ └── __init__.py
-│ └── tests/test_tools.py 80 integration tests
-└── website/ Vite 8 + React 19 + TypeScript (hash-routed, deployed to GitHub Pages)
- └── src/
- ├── main.tsx createHashRouter — all routes under #/
- ├── App.tsx shell with Nav
- ├── pages/
- │ ├── LandingPage.tsx hero, feature grid, CTA row
- │ ├── SpecPage.tsx 8-domain tabbed examples, meta table, playground
- │ ├── SpecFullPage.tsx full spec rendered from SPECIFICATION.md via Vite ?raw
- │ ├── WorkflowPage.tsx 6-stage agent pipeline animation
- │ ├── ExplorerPage.tsx force-directed graph explorer (SVG, pan/zoom/drag)
- │ └── McpPage.tsx
- ├── utils/kndlParser.ts browser-side regex KNDL parser
- ├── hooks/useForceLayout.ts spring force simulation, RAF-based
- └── components/CodeBlock.tsx highlightKNDL() syntax highlighter
-```
-
-## Python public API
-
-```python
-import kndl
-
-kndl.compile(source: str) -> KNDLGraph # parse + compile
-kndl.parse(source: str) -> Program # AST only
-kndl.serialize(graph: KNDLGraph) -> str # graph → KNDL text
-kndl.tokenize(source: str) -> list[Token] # lex only
-```
-
-## KNDLGraph methods
-
-```python
-# Nodes
-graph.add_node(node: GraphNode) -> GraphNode
-graph.get_node(node_id: str) -> GraphNode | None
-graph.update_node(node_id, fields=None, meta_updates=None) -> GraphNode | None
-graph.remove_node(node_id: str) -> bool # also removes connected edges
-
-# Edges
-graph.add_edge(edge: GraphEdge) -> GraphEdge
-graph.get_edge(edge_id: str) -> GraphEdge | None
-graph.remove_edge(edge_id: str) -> bool
-graph.get_outgoing_edges(node_id, edge_type=None) -> list[GraphEdge]
-graph.get_incoming_edges(node_id, edge_type=None) -> list[GraphEdge]
-
-# Intents
-graph.add_intent(intent: GraphIntent) -> GraphIntent
-graph.remove_intent(intent_id: str) -> bool
-
-# Query
-graph.query_nodes(type_name=None, min_confidence=0.0, field_filters=None, apply_decay=True) -> list[GraphNode]
-graph.query_neighborhood(node_id: str, hops: int = 1) -> dict # {nodes: [...], edges: [...]}
-
-# Serialization
-graph.to_dict() -> dict # JSON-compatible snapshot
-graph.from_dict(d) -> KNDLGraph # classmethod
-graph.from_storage(storage) -> KNDLGraph # classmethod, bulk-loads then re-attaches
-
-# State
-graph.nodes: dict[str, GraphNode]
-graph.edges: dict[str, GraphEdge]
-graph.intents: dict[str, GraphIntent]
-graph.processes: dict[str, Any]
-graph.types: dict[str, dict]
-```
-
-## KNDLMeta dataclass
-
-```python
-@dataclass
-class KNDLMeta:
- # Core
- confidence: float = 1.0
- source: str = ""
- valid_start: str | None = None # ISO 8601
- valid_end: str | None = None # ISO 8601 or "*" for open-ended
- decay_rate: float | None = None # e.g. 0.95
- decay_duration_seconds: float | None = None
- supersedes: str | None = None # node/edge ID this replaces
- derived_from: list[str] # provenance chain
- access: str = ""
- priority: float = 0.5
- cooldown_seconds: float | None = None
- tags: list[str]
- weight: float | None = None # edges
- custom: dict[str, Any]
- recorded: str | None = None # when fact was recorded in the system
- observed: str | None = None # when the event was actually observed
- negated: bool = False # asserts the fact is false
- deadline: str | None = None # time by which action must complete
- classification: str | None = None # security classification label
- retention: str | None = None # retention policy string e.g. "7y"
- uncertainty: dict[str, Any] | None = None # §9: gaussian/interval/categorical/histogram
-
- def effective_confidence(at_time: datetime | None = None) -> float
- # formula: confidence × (decay_rate ^ (elapsed / decay_duration_seconds))
-```
-
-## to_dict() key names (important for tests and MCP responses)
-
-- Nodes: `id`, `type` (not `type_name`), `fields`, `meta`
-- Edges: `id`, `source` (not `source_id`), `target` (not `target_id`), `type` (not `edge_type`), `direction`, `fields`, `meta`
-- Intents: `id`, `type`, `trigger` → `{kind, data}`, `actions`, `meta`
-- Stats (kndl_graph_stats): `node_count`, `edge_count`, `intent_count`, `process_count`, `type_distribution`
-
-## Storage layer
-
-Protocol: `packages/python/src/kndl/storage.py`
-
-```python
-class KNDLStorage(Protocol):
- def load(self) -> tuple[list[dict], list[dict], list[dict]] # nodes, edges, intents
- def upsert_node(node: GraphNode) -> None
- def delete_node(node_id: str) -> None
- def upsert_edge(edge: GraphEdge) -> None
- def delete_edge(edge_id: str) -> None
- def upsert_intent(intent: GraphIntent) -> None
- def delete_intent(intent_id: str) -> None
- def clear() -> None
- def close() -> None
-
-def create_storage(database_url: str | None = None) -> KNDLStorage | None
-```
-
-DATABASE_URL values:
-- unset / `"memory"` → returns None (in-memory only)
-- `sqlite:///./kndl.db` → SQLiteStorage (stdlib sqlite3)
-- `sqlite:///:memory:` → SQLite in-memory (tests)
-- `postgresql://user:pw@host/db` → PostgresStorage (psycopg2-binary)
-
-SQLite backend: `INSERT OR REPLACE`, immediate `commit()`, indexes on `type_name`, `source_id`, `target_id`, `edge_type`.
-PostgreSQL backend: JSONB columns, GIN indexes, `ON CONFLICT DO UPDATE`, auto-reconnect via `_ensure_connection()`.
-
-## MCP server
-
-Package: `packages/mcp-server`
-Entry point: `python -m kndl_mcp` (stdio) or `python -m kndl_mcp --http` (port 8000)
-
-Holds a single global `_graph: KNDLGraph`. Storage initialised once at import via `create_storage()`.
-
-### 14 tools
-
-| Tool | Key parameters |
-|------|---------------|
-| `kndl_parse` | `source: str` — parses KNDL including `process` blocks |
-| `kndl_add_node` | `node_id, type_name, fields, confidence, source, valid_start, valid_end, decay_rate, decay_duration, tags, recorded, observed, negated, deadline, classification, retention, uncertainty` |
-| `kndl_get_node` | `node_id` |
-| `kndl_update_node` | `node_id, fields, meta_updates` |
-| `kndl_remove_node` | `node_id` |
-| `kndl_add_edge` | `source_id, target_id, edge_type, direction ("forward"/"reverse"/"undirected"), fields, confidence` |
-| `kndl_query_nodes` | `type_name, min_confidence, field_filters, apply_decay` |
-| `kndl_neighborhood` | `node_id, hops (1–5)` |
-| `kndl_add_intent` | `intent_id, type_name, trigger_kind, trigger_data, actions, meta` |
-| `kndl_merge_graphs` | `source: str` |
-| `kndl_serialize` | _(none)_ |
-| `kndl_graph_stats` | _(none)_ → returns `node_count`, `edge_count`, `intent_count`, `process_count`, `type_distribution` |
-| `kndl_get_types` | `type_name: str \| None` |
-| `kndl_reset` | _(none)_ — clears in-memory graph and persistent store |
-
-All tools return `{"status": "ok", ...}` or `{"status": "error", "message": "..."}`.
-
-### 5 resources
-
-| URI | Content |
-|-----|---------|
-| `kndl://spec/version` | `"KNDL Specification vX.Y.Z"` |
-| `kndl://spec/grammar` | Full EBNF grammar text |
-| `kndl://spec/language` | Full SPECIFICATION.md text |
-| `kndl://graph/types` | JSON snapshot of type declarations |
-| `kndl://graph/summary` | Plain-text node/edge/intent/process count summary |
-
-## Parser quirks (do not "fix" without updating compiler + tests)
-
-- **Decay expressions**: `0.95 / 1h` is parsed as `BinaryOp(op="/", left=Literal(0.95), right=Literal("1h"))`, not a `DecayExpr`. Compiler `_build_meta` handles both forms.
-- **Constrained types**: `where { expr }` braces required — not inline.
-- **Export syntax**: `export type Foo { ... }` — not `export { Foo }`.
-- **Query trigger**: `trigger = query name { match ... return ... }` — inline body, not a reference.
-- **Optional match**: `optional match ?var :: Type` — `match` follows `optional`.
-- **`in` keyword**: reserved as `KW_IN`. Use `located_in`, `contained_in`, etc. for edge types.
-- **Duration units**: `ns`, `us`, `ms`, `s`, `m`, `h`, `d`, `w`, `mo`, `y`. Lexer matches longest prefix; `ms` beats `m`, `mo` beats `m`.
-- **Parameterised types**: `Type ` — `<` after a type name starts a param list, `>` closes it. Nested: `Code<"LOINC">`.
-- **Undirected edges**: `-[Type]-` — `TYPED_ARROW_START` + type name + `RBRACKET` + `OP_MINUS` (no `>`).
-- **Multi-hop patterns**: `-[Type*]->` unbounded; `-[Type*3]->` exact; `-[Type*2..5]->` range; `-[Type*2..]->` lower-bound only.
-- **Named struct literals**: `TypeName { key = value }` — identifier followed by `{` in expression position creates a `MapLiteral` with `_type` key.
-- **`goto` in do-blocks**: `goto STATE` creates `EmitAction(action_type="goto", goto_state=STATE)`.
-
-## Serializer invariant
-
-Nodes with inline edge fields (`->`) are serialized as standalone `edge @a -[type]-> @b` declarations only. Prevents 2× edge duplication on roundtrip.
-
-## Meta-annotation keys (compiler `_build_meta`)
-
-`confidence`, `source`, `valid`, `decay`, `supersedes`, `derived`, `priority`, `cooldown`, `tags`, `weight`, `access`, `recorded`, `observed`, `negated`, `deadline`, `classification`, `retention`, `uncertainty`
-
-## Website architecture
-
-- React Router v7: `createHashRouter` + `RouterProvider` + ` ` — hash routing for GitHub Pages static hosting
-- Routes: `/#/` · `/#/spec` · `/#/spec/full` · `/#/workflow` · `/#/explorer` · `/#/mcp`
-- `SpecPage.tsx`: 8-domain tabbed profiles (IoT, FinTech, eCommerce, Logistics, Medicine, Robotics, Smart Factory, Networking); `activeDomain` state drives `DOMAINS` filter
-- `SpecFullPage.tsx`: imports `SPECIFICATION.md` via Vite `?raw`, custom block tokenizer + `BlockRenderer`
-- `ExplorerPage.tsx`: SVG canvas, always-mounted canvas div (display:none when editor active to preserve ResizeObserver)
-- `useForceLayout.ts`: RAF simulation, `MAX_ITER=220`, `REPEL=3800`, `ATTRACT=0.018`, guards against `width=0/height=0`
-- `highlightKNDL()` in `CodeBlock.tsx`: regex chain → `dangerouslySetInnerHTML`
-- `vite.config.ts`: `base: process.env.GITHUB_PAGES === "true" ? "/KNDL/" : "/"` — assets load from sub-path on Pages
-
-## Build and test commands
-
-```bash
-make py-install # uv sync --all-extras (packages/python)
-make py-test # uv run pytest -v (245 tests across 5 files)
-make py-test-cov # pytest with coverage report
-make py-lint # ruff check + mypy
-make py-build # uv build
-
-make mcp-install # uv sync --all-extras (packages/mcp-server)
-make mcp-test # uv run pytest -v (80 tests)
-make mcp-lint # ruff check + mypy
-make mcp-run # python -m kndl_mcp (stdio)
-make mcp-run-http # python -m kndl_mcp --http
-
-make web-install # pnpm install
-make web-dev # vite dev server
-make web-build # vite build (set GITHUB_PAGES=true for /KNDL/ base)
-make web-preview # serve production build locally
-
-make install # py-install + mcp-install
-make test # py-test + mcp-test
-make lint # py-lint + mcp-lint
-make clean # remove dist/, .venv/, node_modules/
-```
-
-## CI
-
-`.github/workflows/kndl-workflow.yml` — two parallel jobs on push/PR to main:
-- `python`: `uv sync` → `ruff + mypy` → `pytest` (245 tests)
-- `mcp-server`: `uv sync` → `ruff + mypy` → `pytest` (80 tests)
-
-`.github/workflows/deploy-website.yml` — triggered on push to main when `website/**` changes (or `workflow_dispatch`):
-- `build`: pnpm install → `pnpm build` (`GITHUB_PAGES=true`) → upload artifact
-- `deploy`: `actions/deploy-pages@v4` → https://artdaw.github.io/KNDL/
-
-`.github/workflows/codeql.yml` — CodeQL analysis: Python (`build-mode: none`), TypeScript (`build-mode: none`, `source-root: website/src`).
-
-## Optional dependencies
-
-```toml
-# packages/python
-[project.optional-dependencies]
-postgres = ["psycopg2-binary>=2.9"]
-dotenv = ["python-dotenv>=1.0"]
-
-# packages/mcp-server
-[project.optional-dependencies]
-postgres = ["psycopg2-binary>=2.9"]
-dev = ["pytest", "pytest-asyncio", "pytest-cov", "ruff", "mypy", "python-dotenv"]
-```
-
-`python-dotenv` auto-loads `.env` from CWD if installed. `DATABASE_URL` in environment takes precedence.
-
-## Specification
-
-Full language reference: [`spec/SPECIFICATION.md`](spec/SPECIFICATION.md)
-EBNF grammar: [`spec/grammar/kndl.ebnf`](spec/grammar/kndl.ebnf)
diff --git a/packages/kndl-memory/eval/runner.ts b/packages/kndl-memory/eval/runner.ts
new file mode 100644
index 0000000..5f7f5b7
--- /dev/null
+++ b/packages/kndl-memory/eval/runner.ts
@@ -0,0 +1,276 @@
+#!/usr/bin/env tsx
+// eval/runner.ts — KNDL eval runner.
+//
+// Drives Claude in vanilla mode (facts pasted into system prompt) and uses
+// Claude-as-judge to auto-score each response binary right/wrong.
+//
+// MCP mode (facts accessed via kndl-memory-mcp tools) is documented below
+// but requires a running server; run that side manually for now.
+//
+// Usage:
+// export ANTHROPIC_API_KEY=sk-ant-...
+// tsx eval/runner.ts [--scenario ] [--out results.json]
+//
+// Output: JSON to stdout + summary to stderr.
+// Saves to --out path if provided (default: eval/results.json).
+
+import { readFileSync, readdirSync, writeFileSync } from "node:fs";
+import { join, resolve } from "node:path";
+import Anthropic from "@anthropic-ai/sdk";
+
+// ── Types ─────────────────────────────────────────────────────────────────────
+
+interface Question {
+ id: string;
+ archetype: string;
+ prompt: string;
+ correct_behavior: string;
+ vanilla_failure_mode: string;
+ setup_note?: string;
+}
+
+interface Scenario {
+ id: string;
+ name: string;
+ corpus_dir: string;
+ eval_date: string;
+ questions: Question[];
+}
+
+interface EvalSuite {
+ version: string;
+ scenarios: Scenario[];
+}
+
+interface QuestionResult {
+ scenario_id: string;
+ question_id: string;
+ archetype: string;
+ prompt: string;
+ correct_behavior: string;
+ vanilla_answer: string;
+ vanilla_pass: boolean;
+ judge_reasoning: string;
+ eval_date: string;
+ model: string;
+}
+
+interface EvalResults {
+ run_at: string;
+ model: string;
+ total: number;
+ passed: number;
+ failed: number;
+ pass_rate: number;
+ threshold: number;
+ verdict: "PASS" | "FAIL" | "BORDERLINE";
+ by_archetype: Record;
+ questions: QuestionResult[];
+}
+
+// ── Corpus loader ─────────────────────────────────────────────────────────────
+
+const SKILLS_DIR = resolve(new URL(".", import.meta.url).pathname, "../../../skills/kndl-memory");
+const EVAL_DIR = join(SKILLS_DIR, "eval");
+const SUITE_PATH = join(EVAL_DIR, "questions.json");
+
+function loadCorpus(corpusDir: string): string {
+ const dir = join(SKILLS_DIR, corpusDir);
+ let files: string[];
+ try {
+ files = readdirSync(dir).filter((f) => f.endsWith(".fact.json"));
+ } catch {
+ return `(no corpus found at ${corpusDir})`;
+ }
+ const facts = files.map((f) => JSON.parse(readFileSync(join(dir, f), "utf8")));
+ return JSON.stringify(facts, null, 2);
+}
+
+// ── Scoring (Claude-as-judge) ─────────────────────────────────────────────────
+
+const JUDGE_PROMPT = (question: string, correctBehavior: string, vanillaFailure: string, answer: string) => `\
+You are scoring an AI answer on a knowledge-representation question.
+
+QUESTION:
+${question}
+
+CORRECT BEHAVIOR:
+${correctBehavior}
+
+TYPICAL VANILLA FAILURE MODE:
+${vanillaFailure}
+
+AI ANSWER:
+${answer}
+
+Score the answer. Answer with EXACTLY one of:
+PASS — the answer exhibits the correct behavior (may be worded differently but hits the key points)
+FAIL — the answer exhibits the vanilla failure mode or otherwise misses the key points
+
+Then on the next line, one sentence explaining your score.`;
+
+async function judgeAnswer(
+ client: Anthropic,
+ model: string,
+ q: Question,
+ answer: string,
+): Promise<{ pass: boolean; reasoning: string }> {
+ const resp = await client.messages.create({
+ model,
+ max_tokens: 200,
+ messages: [{
+ role: "user",
+ content: JUDGE_PROMPT(q.prompt, q.correct_behavior, q.vanilla_failure_mode, answer),
+ }],
+ });
+
+ const text = resp.content.filter((b) => b.type === "text").map((b) => (b as { text: string }).text).join("").trim();
+ const pass = text.toUpperCase().startsWith("PASS");
+ const lines = text.split("\n");
+ const reasoning = lines.slice(1).join(" ").trim() || text;
+ return { pass, reasoning };
+}
+
+// ── Vanilla evaluation ────────────────────────────────────────────────────────
+
+async function evalVanilla(
+ client: Anthropic,
+ model: string,
+ scenario: Scenario,
+ question: Question,
+): Promise<{ answer: string; pass: boolean; reasoning: string }> {
+ const corpus = loadCorpus(scenario.corpus_dir);
+ const system = `\
+You are an AI assistant with access to structured memory facts about ${scenario.name}.
+Today's date for evaluation purposes is ${scenario.eval_date}.
+${question.setup_note ? `\nNote: ${question.setup_note}\n` : ""}
+Here are the memory facts (JSON-LD format):
+
+${corpus}
+
+Answer questions based ONLY on these facts. Apply confidence scores, decay, and provenance as appropriate.`;
+
+ const resp = await client.messages.create({
+ model,
+ max_tokens: 600,
+ system,
+ messages: [{ role: "user", content: question.prompt }],
+ });
+
+ const answer = resp.content
+ .filter((b) => b.type === "text")
+ .map((b) => (b as { text: string }).text)
+ .join("")
+ .trim();
+
+ const { pass, reasoning } = await judgeAnswer(client, model, question, answer);
+ return { answer, pass, reasoning };
+}
+
+// ── Main ──────────────────────────────────────────────────────────────────────
+
+async function main(): Promise {
+ const apiKey = process.env.ANTHROPIC_API_KEY;
+ if (!apiKey) {
+ process.stderr.write("error: ANTHROPIC_API_KEY not set\n");
+ process.exit(1);
+ }
+
+ const args = process.argv.slice(2);
+
+ // Safe flag parsing: indexOf returns -1 when absent; -1+1=0 reads the wrong value.
+ function flag(name: string): string | null {
+ const i = args.indexOf(name);
+ return i !== -1 && i + 1 < args.length ? args[i + 1] : null;
+ }
+
+ const scenarioFilter = flag("--scenario");
+ const outPath = flag("--out") ?? join(EVAL_DIR, "results.json");
+ // Default to claude-sonnet-4-6 — cost-effective for eval workloads.
+ // Override with --model claude-opus-4-7 for highest accuracy.
+ const model = flag("--model") ?? "claude-sonnet-4-6";
+
+ const suite = JSON.parse(readFileSync(SUITE_PATH, "utf8")) as EvalSuite;
+ const client = new Anthropic({ apiKey });
+
+ const results: QuestionResult[] = [];
+ const byArchetype: Record = {};
+
+ const scenarios = scenarioFilter
+ ? suite.scenarios.filter((s) => s.id === scenarioFilter)
+ : suite.scenarios;
+
+ for (const scenario of scenarios) {
+ process.stderr.write(`\n▶ ${scenario.name} (${scenario.questions.length} questions)\n`);
+ for (const q of scenario.questions) {
+ process.stderr.write(` ${q.id} (${q.archetype})… `);
+ try {
+ const { answer, pass, reasoning } = await evalVanilla(client, model, scenario, q);
+ results.push({
+ scenario_id: scenario.id,
+ question_id: q.id,
+ archetype: q.archetype,
+ prompt: q.prompt,
+ correct_behavior: q.correct_behavior,
+ vanilla_answer: answer,
+ vanilla_pass: pass,
+ judge_reasoning: reasoning,
+ eval_date: scenario.eval_date,
+ model,
+ });
+ if (!byArchetype[q.archetype]) byArchetype[q.archetype] = { total: 0, passed: 0 };
+ byArchetype[q.archetype].total++;
+ if (pass) byArchetype[q.archetype].passed++;
+ process.stderr.write(pass ? "✔ PASS\n" : "✖ FAIL\n");
+ } catch (e) {
+ process.stderr.write(`ERROR: ${(e as Error).message}\n`);
+ results.push({
+ scenario_id: scenario.id, question_id: q.id, archetype: q.archetype,
+ prompt: q.prompt, correct_behavior: q.correct_behavior,
+ vanilla_answer: `[error: ${(e as Error).message}]`,
+ vanilla_pass: false, judge_reasoning: "eval error",
+ eval_date: scenario.eval_date, model,
+ });
+ }
+ // Rate limiting: small pause between calls
+ await new Promise((r) => setTimeout(r, 500));
+ }
+ }
+
+ const total = results.length;
+ const passed = results.filter((r) => r.vanilla_pass).length;
+ const passRate = total > 0 ? passed / total : 0;
+ const THRESHOLD = 0.7;
+ const verdict = passRate >= THRESHOLD ? "PASS" : passRate >= THRESHOLD - 0.1 ? "BORDERLINE" : "FAIL";
+
+ const output: EvalResults = {
+ run_at: new Date().toISOString(),
+ model,
+ total, passed, failed: total - passed,
+ pass_rate: Math.round(passRate * 1000) / 10,
+ threshold: THRESHOLD * 100,
+ verdict,
+ by_archetype: byArchetype,
+ questions: results,
+ };
+
+ writeFileSync(outPath, JSON.stringify(output, null, 2));
+
+ process.stderr.write(`\n${"─".repeat(50)}\n`);
+ process.stderr.write(`Total: ${total} | Pass: ${passed} | Fail: ${total - passed}\n`);
+ process.stderr.write(`Pass rate: ${output.pass_rate}% (threshold: ${THRESHOLD * 100}%)\n`);
+ process.stderr.write(`Verdict: ${verdict}\n`);
+ if (verdict === "FAIL") {
+ process.stderr.write(`\n⚠ KNDL did not beat vanilla on ≥70% of questions.\n`);
+ process.stderr.write(`Per v2.md §13: fix the protocol before shipping v2.0.\n`);
+ }
+ process.stderr.write(`Results saved to: ${outPath}\n`);
+
+ process.stdout.write(JSON.stringify(output, null, 2) + "\n");
+ process.exit(verdict === "FAIL" ? 1 : 0);
+}
+
+main().catch((e) => {
+ process.stderr.write(`fatal: ${(e as Error).message}\n`);
+ process.exit(1);
+});
diff --git a/packages/kndl-memory/package-lock.json b/packages/kndl-memory/package-lock.json
new file mode 100644
index 0000000..3c3d50c
--- /dev/null
+++ b/packages/kndl-memory/package-lock.json
@@ -0,0 +1,3246 @@
+{
+ "name": "@kndl/memory",
+ "version": "2.0.0-alpha.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@kndl/memory",
+ "version": "2.0.0-alpha.1",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.0.0",
+ "@types/better-sqlite3": "^7.6.13",
+ "better-sqlite3": "^12.9.0",
+ "chokidar": "^5.0.0",
+ "express": "^5.2.1",
+ "zod": "^3.23.0"
+ },
+ "bin": {
+ "kndl": "dist/cli.js",
+ "kndl-memory-mcp": "dist/server.js"
+ },
+ "devDependencies": {
+ "@anthropic-ai/sdk": "^0.91.1",
+ "@types/express": "^5.0.6",
+ "@types/node": "^20.0.0",
+ "tsup": "^8.0.0",
+ "tsx": "^4.0.0",
+ "typescript": "^5.4.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@anthropic-ai/sdk": {
+ "version": "0.91.1",
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz",
+ "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-schema-to-ts": "^3.1.1"
+ },
+ "bin": {
+ "anthropic-ai-sdk": "bin/cli"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
+ "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
+ "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
+ "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
+ "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
+ "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
+ "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
+ "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
+ "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
+ "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
+ "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
+ "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
+ "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
+ "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
+ "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
+ "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
+ "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
+ "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
+ "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
+ "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
+ "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
+ "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@hono/node-server": {
+ "version": "1.19.14",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
+ "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.14.1"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk": {
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
+ "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@hono/node-server": "^1.19.9",
+ "ajv": "^8.17.1",
+ "ajv-formats": "^3.0.1",
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "express": "^5.2.1",
+ "express-rate-limit": "^8.2.1",
+ "hono": "^4.11.4",
+ "jose": "^6.1.3",
+ "json-schema-typed": "^8.0.2",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.25 || ^4.0",
+ "zod-to-json-schema": "^3.25.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@cfworker/json-schema": "^4.1.1",
+ "zod": "^3.25 || ^4.0"
+ },
+ "peerDependenciesMeta": {
+ "@cfworker/json-schema": {
+ "optional": true
+ },
+ "zod": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
+ "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
+ "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
+ "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
+ "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
+ "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
+ "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
+ "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
+ "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
+ "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
+ "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
+ "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
+ "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
+ "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
+ "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
+ "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
+ "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
+ "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
+ "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
+ "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
+ "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
+ "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
+ "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
+ "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/better-sqlite3": {
+ "version": "7.6.13",
+ "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
+ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/express": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
+ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^5.0.0",
+ "@types/serve-static": "^2"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
+ "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.39",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
+ "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
+ "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/better-sqlite3": {
+ "version": "12.9.0",
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz",
+ "integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "prebuild-install": "^7.1.1"
+ },
+ "engines": {
+ "node": "20.x || 22.x || 23.x || 24.x || 25.x"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/bundle-require": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz",
+ "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "load-tsconfig": "^0.2.3"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "esbuild": ">=0.18"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/confbox": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/consola": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
+ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
+ "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.7",
+ "@esbuild/android-arm": "0.27.7",
+ "@esbuild/android-arm64": "0.27.7",
+ "@esbuild/android-x64": "0.27.7",
+ "@esbuild/darwin-arm64": "0.27.7",
+ "@esbuild/darwin-x64": "0.27.7",
+ "@esbuild/freebsd-arm64": "0.27.7",
+ "@esbuild/freebsd-x64": "0.27.7",
+ "@esbuild/linux-arm": "0.27.7",
+ "@esbuild/linux-arm64": "0.27.7",
+ "@esbuild/linux-ia32": "0.27.7",
+ "@esbuild/linux-loong64": "0.27.7",
+ "@esbuild/linux-mips64el": "0.27.7",
+ "@esbuild/linux-ppc64": "0.27.7",
+ "@esbuild/linux-riscv64": "0.27.7",
+ "@esbuild/linux-s390x": "0.27.7",
+ "@esbuild/linux-x64": "0.27.7",
+ "@esbuild/netbsd-arm64": "0.27.7",
+ "@esbuild/netbsd-x64": "0.27.7",
+ "@esbuild/openbsd-arm64": "0.27.7",
+ "@esbuild/openbsd-x64": "0.27.7",
+ "@esbuild/openharmony-arm64": "0.27.7",
+ "@esbuild/sunos-x64": "0.27.7",
+ "@esbuild/win32-arm64": "0.27.7",
+ "@esbuild/win32-ia32": "0.27.7",
+ "@esbuild/win32-x64": "0.27.7"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventsource": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+ "license": "MIT",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz",
+ "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-rate-limit": {
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz",
+ "integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==",
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "10.1.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": ">= 4.11"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "license": "MIT"
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/fix-dts-default-cjs-exports": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz",
+ "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "magic-string": "^0.30.17",
+ "mlly": "^1.7.4",
+ "rollup": "^4.34.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
+ "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hono": {
+ "version": "4.12.15",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz",
+ "integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/ip-address": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
+ "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/jose": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
+ "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/joycon": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
+ "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/json-schema-to-ts": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
+ "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "ts-algebra": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/json-schema-typed": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
+ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/load-tsconfig": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz",
+ "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
+ "node_modules/mlly": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
+ "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "pathe": "^2.0.3",
+ "pkg-types": "^1.3.1",
+ "ufo": "^1.6.3"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-abi": {
+ "version": "3.89.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz",
+ "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkce-challenge": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
+ "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/pkg-types": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.1.8",
+ "mlly": "^1.7.4",
+ "pathe": "^2.0.1"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
+ "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
+ "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.2",
+ "@rollup/rollup-android-arm64": "4.60.2",
+ "@rollup/rollup-darwin-arm64": "4.60.2",
+ "@rollup/rollup-darwin-x64": "4.60.2",
+ "@rollup/rollup-freebsd-arm64": "4.60.2",
+ "@rollup/rollup-freebsd-x64": "4.60.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.2",
+ "@rollup/rollup-linux-arm64-musl": "4.60.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.2",
+ "@rollup/rollup-linux-loong64-musl": "4.60.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-musl": "4.60.2",
+ "@rollup/rollup-openbsd-x64": "4.60.2",
+ "@rollup/rollup-openharmony-arm64": "4.60.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.2",
+ "@rollup/rollup-win32-x64-gnu": "4.60.2",
+ "@rollup/rollup-win32-x64-msvc": "4.60.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
+ "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/ts-algebra": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
+ "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/tsup": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz",
+ "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bundle-require": "^5.1.0",
+ "cac": "^6.7.14",
+ "chokidar": "^4.0.3",
+ "consola": "^3.4.0",
+ "debug": "^4.4.0",
+ "esbuild": "^0.27.0",
+ "fix-dts-default-cjs-exports": "^1.0.0",
+ "joycon": "^3.1.1",
+ "picocolors": "^1.1.1",
+ "postcss-load-config": "^6.0.1",
+ "resolve-from": "^5.0.0",
+ "rollup": "^4.34.8",
+ "source-map": "^0.7.6",
+ "sucrase": "^3.35.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.11",
+ "tree-kill": "^1.2.2"
+ },
+ "bin": {
+ "tsup": "dist/cli-default.js",
+ "tsup-node": "dist/cli-node.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@microsoft/api-extractor": "^7.36.0",
+ "@swc/core": "^1",
+ "postcss": "^8.4.12",
+ "typescript": ">=4.5.0"
+ },
+ "peerDependenciesMeta": {
+ "@microsoft/api-extractor": {
+ "optional": true
+ },
+ "@swc/core": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tsup/node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/tsup/node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/tsx": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.27.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/ufo": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
+ "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.25.2",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
+ "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.25.28 || ^4"
+ }
+ }
+ }
+}
diff --git a/packages/kndl-memory/package.json b/packages/kndl-memory/package.json
new file mode 100644
index 0000000..5613b0a
--- /dev/null
+++ b/packages/kndl-memory/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "@kndl/memory",
+ "version": "2.0.0",
+ "description": "Confidence-, time-, and provenance-aware memory for AI agents. JSON-LD fact store with decay, supersession, and bitemporal queries. Ships an MCP server (kndl-memory-mcp) and a CLI (kndl) for use from Claude Skills.",
+ "type": "module",
+ "bin": {
+ "kndl": "./dist/cli.js",
+ "kndl-memory-mcp": "./dist/server.js"
+ },
+ "main": "./dist/core.js",
+ "types": "./dist/core.d.ts",
+ "exports": {
+ ".": "./dist/core.js",
+ "./cli": "./dist/cli.js",
+ "./server": "./dist/server.js"
+ },
+ "scripts": {
+ "build": "tsup src/core.ts src/cli.ts src/server.ts --format esm --dts --clean --shims",
+ "kndl": "tsx src/cli.ts",
+ "mcp": "tsx src/server.ts",
+ "test": "tsx --test tests/stores.test.ts tests/remote.test.ts",
+ "eval": "tsx eval/runner.ts"
+ },
+ "files": [
+ "dist",
+ "context"
+ ],
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.0.0",
+ "@types/better-sqlite3": "^7.6.13",
+ "better-sqlite3": "^12.9.0",
+ "chokidar": "^5.0.0",
+ "express": "^5.2.1",
+ "zod": "^3.23.0"
+ },
+ "devDependencies": {
+ "@anthropic-ai/sdk": "^0.91.1",
+ "@types/express": "^5.0.6",
+ "@types/node": "^20.0.0",
+ "tsup": "^8.0.0",
+ "tsx": "^4.0.0",
+ "typescript": "^5.4.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "license": "MIT"
+}
diff --git a/packages/kndl-memory/src/cli.ts b/packages/kndl-memory/src/cli.ts
new file mode 100644
index 0000000..bf65317
--- /dev/null
+++ b/packages/kndl-memory/src/cli.ts
@@ -0,0 +1,380 @@
+#!/usr/bin/env node
+// kndl CLI — invoked from the kndl-memory Skill via bash.
+//
+// Usage:
+// kndl add --statement "..." --confidence 0.9 --source "..." [--subject ...] [--predicate ...]
+// [--object json] [--decay "0.5/30d"] [--valid-from now|ISO] [--observed-at ISO]
+// [--classification PHI|PII|...] [--consent ] [--tenant ]
+// [--derived-from id1 id2 ...] [--negated]
+//
+// kndl supersede --old-id [add args]
+// kndl query [--subject ...] [--predicate ...] [--as-of now|ISO] [--min-confidence 0.0] [--tenant ...] [--allow-phi]
+// kndl contradictions [--subject ...] [--predicate ...]
+// kndl provenance --id [--max-depth 8]
+// kndl list [--subject ...]
+// kndl show --id
+//
+// Env:
+// KNDL_STORAGE Storage URL (default fs:./memory). See stores/index.ts for formats.
+// KNDL_MEMORY_DIR Legacy alias for fs: (overridden by KNDL_STORAGE).
+
+import type { FactInput } from "./types.js";
+import { makeStore } from "./stores/index.js";
+import { AnthropicMemoryClient } from "./remote/anthropic.js";
+import { pull } from "./remote/sync.js";
+import { loadRemoteConfigs, addRemote, removeRemote, saveRemoteConfigs } from "./remote/config.js";
+
+interface Args {
+ positional: string[];
+ flags: Record;
+}
+
+function parseArgs(argv: string[]): Args {
+ const out: Args = { positional: [], flags: {} };
+ for (let i = 0; i < argv.length; i++) {
+ const a = argv[i];
+ if (a.startsWith("--")) {
+ const key = a.slice(2);
+ const next = argv[i + 1];
+ if (next === undefined || next.startsWith("--")) {
+ out.flags[key] = true;
+ } else {
+ const collected: string[] = [];
+ while (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
+ collected.push(argv[++i]);
+ }
+ out.flags[key] = collected.length === 1 ? collected[0] : collected;
+ }
+ } else {
+ out.positional.push(a);
+ }
+ }
+ return out;
+}
+
+function s(v: unknown): string | undefined {
+ return typeof v === "string" ? v : undefined;
+}
+
+function n(v: unknown): number | undefined {
+ if (typeof v === "string") {
+ const x = parseFloat(v);
+ return Number.isFinite(x) ? x : undefined;
+ }
+ return undefined;
+}
+
+function b(v: unknown): boolean {
+ return v === true || v === "true";
+}
+
+function arr(v: unknown): string[] | undefined {
+ if (Array.isArray(v)) return v;
+ if (typeof v === "string") return [v];
+ return undefined;
+}
+
+function requireFlag(v: string | undefined, name: string): string {
+ if (v === undefined) fail(`missing required --${name}`);
+ return v;
+}
+
+function fail(msg: string): never {
+ process.stderr.write(`error: ${msg}\n`);
+ process.exit(1);
+}
+
+function out(obj: unknown): void {
+ process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
+}
+
+function buildInput(flags: Record): FactInput {
+ const conf = n(flags.confidence);
+ if (conf === undefined) fail("--confidence is required and must be a number");
+ let object: unknown = flags.object;
+ if (typeof object === "string") {
+ try { object = JSON.parse(object); } catch { /* leave as string */ }
+ }
+ return {
+ statement: requireFlag(s(flags.statement), "statement"),
+ confidence: conf,
+ source: requireFlag(s(flags.source), "source"),
+ subject: s(flags.subject),
+ predicate: s(flags.predicate),
+ object,
+ decay: s(flags.decay),
+ validFrom: s(flags["valid-from"]),
+ validUntil: s(flags["valid-until"]),
+ observedAt: s(flags["observed-at"]),
+ classification: s(flags.classification),
+ consent: s(flags.consent),
+ tenant: s(flags.tenant),
+ derivedFrom: arr(flags["derived-from"]),
+ negated: b(flags.negated),
+ };
+}
+
+const HELP = `kndl — confidence-, time-, and provenance-aware memory CLI
+
+Commands:
+ add Write a new fact
+ supersede Write a fact replacing an older one (preserves history)
+ query Read active facts with effective confidence at as_of time
+ contradictions Find disagreeing active facts about same subject/predicate
+ provenance Walk derivedFrom + supersedes backward
+ list List fact IDs
+ show Print a fact by ID
+ migrate Migrate v1 SQLite database → v2 JSON-LD facts
+ remote Manage and sync Anthropic Memory Store remotes
+ help Show this message
+
+Env:
+ KNDL_STORAGE Storage URL, e.g. fs:./memory sqlite:./kndl.db
+ KNDL_MEMORY_DIR Legacy alias — equivalent to KNDL_STORAGE=fs:
+ ANTHROPIC_API_KEY Required for remote sync commands
+ KNDL_REMOTE_STORES "anthropic::" shorthand (no file needed)
+
+Run \`kndl --help\` for options, or read SKILL.md.
+`;
+
+async function main(argv: string[]): Promise {
+ const cmd = argv[0];
+ const { flags } = parseArgs(argv.slice(1));
+
+ if (!cmd || cmd === "help" || flags.help) {
+ process.stdout.write(HELP);
+ return 0;
+ }
+
+ const store = makeStore();
+
+ try {
+ switch (cmd) {
+ case "add": {
+ const r = await store.assertFact(buildInput(flags));
+ out({ id: r.id });
+ return 0;
+ }
+ case "supersede": {
+ const oldId = requireFlag(s(flags["old-id"]), "old-id");
+ const r = await store.supersedeFact(oldId, buildInput(flags));
+ out({ id: r.id, supersedes: r.supersedes });
+ return 0;
+ }
+ case "query": {
+ out(await store.query({
+ subject: s(flags.subject),
+ predicate: s(flags.predicate),
+ asOf: s(flags["as-of"]),
+ minConfidence: n(flags["min-confidence"]),
+ tenant: s(flags.tenant),
+ allowPhi: b(flags["allow-phi"]),
+ }));
+ return 0;
+ }
+ case "contradictions": {
+ out(await store.contradictions({ subject: s(flags.subject), predicate: s(flags.predicate) }));
+ return 0;
+ }
+ case "provenance": {
+ const id = requireFlag(s(flags.id), "id");
+ out(await store.provenanceChain(id, n(flags["max-depth"])));
+ return 0;
+ }
+ case "list": {
+ out(await store.list(s(flags.subject)));
+ return 0;
+ }
+ case "show": {
+ const id = requireFlag(s(flags.id), "id");
+ const f = await store.show(id);
+ if (!f) { process.stderr.write(`not found: ${id}\n`); return 1; }
+ out(f);
+ return 0;
+ }
+ case "migrate": {
+ // kndl migrate --from sqlite:./kndl-v1.db --to ./memory
+ // Reads a v1 KNDL SQLite database (Python schema) and writes JSON-LD
+ // facts for each Node, Edge, and Intent into the target memory directory.
+ const from = requireFlag(s(flags.from), "from");
+ const to = s(flags.to) ?? "./memory";
+ const dryRun = b(flags["dry-run"]);
+ const { createRequire } = await import("node:module");
+ const req = createRequire(import.meta.url);
+ const Database = req("better-sqlite3") as typeof import("better-sqlite3");
+ const { mkdirSync } = await import("node:fs");
+ const { join: pathJoin } = await import("node:path");
+ const { writeFileSync, existsSync } = await import("node:fs");
+ const { nowIso: _nowIso } = await import("./core.js");
+
+ const dbPath = from.replace(/^sqlite:\/\/\//, "").replace(/^sqlite:\/\//, "");
+ const db = new Database(dbPath, { readonly: true });
+
+ let nodes: unknown[], edges: unknown[], intents: unknown[];
+ try {
+ nodes = db.prepare("SELECT id, type_name, fields_json, meta_json FROM kndl_nodes").all() as unknown[];
+ edges = db.prepare("SELECT id, source_id, target_id, edge_type, direction, fields_json, meta_json FROM kndl_edges").all() as unknown[];
+ intents = db.prepare("SELECT id, type_name, trigger_kind, trigger_data, actions_json, meta_json FROM kndl_intents").all() as unknown[];
+ } catch {
+ fail(`Could not read v1 schema from ${dbPath}. Is this a KNDL v1 database?`);
+ } finally {
+ db.close();
+ }
+
+ const factsDir = pathJoin(to, "facts");
+ if (!dryRun) mkdirSync(factsDir, { recursive: true });
+
+ let written = 0;
+ const now = _nowIso();
+
+ function writeFact(fact: Record): void {
+ const id = fact["@id"] as string;
+ const fname = id.replace(/[^a-z0-9]+/gi, "-").toLowerCase().slice(0, 100) + ".fact.json";
+ const path = pathJoin(factsDir, fname);
+ if (!dryRun) {
+ if (existsSync(path)) return; // idempotent
+ writeFileSync(path, JSON.stringify(fact, null, 2));
+ }
+ written++;
+ }
+
+ for (const row of nodes!) {
+ const r = row as { id: string; type_name: string; fields_json: string; meta_json: string };
+ const meta = JSON.parse(r.meta_json || "{}");
+ const fields = JSON.parse(r.fields_json || "{}");
+ const fieldStr = Object.entries(fields).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
+ writeFact({
+ "@context": "https://kndl.artdaw.com/context/v1.jsonld",
+ "@id": `fact:v1-node-${r.id}`,
+ "@type": "Fact",
+ "statement": `${r.id} (${r.type_name})${fieldStr ? ": " + fieldStr : ""}`,
+ "subject": `node:${r.id}`,
+ "predicate": "isa",
+ "object": r.type_name,
+ "confidence": meta.confidence ?? 1.0,
+ "source": meta.source || "kndl://v1-migration",
+ "validFrom": meta.valid_start ?? meta.recorded ?? now,
+ "recordedAt": meta.recorded ?? now,
+ "tags": ["v1-migration", "node", ...(meta.tags ?? [])],
+ ...(meta.decay_rate && meta.decay_duration_seconds
+ ? { "decay": `${meta.decay_rate}/${meta.decay_duration_seconds}s` }
+ : {}),
+ });
+ }
+
+ for (const row of edges!) {
+ const r = row as { id: string; source_id: string; target_id: string; edge_type: string; direction: string; fields_json: string; meta_json: string };
+ const meta = JSON.parse(r.meta_json || "{}");
+ writeFact({
+ "@context": "https://kndl.artdaw.com/context/v1.jsonld",
+ "@id": `fact:v1-edge-${r.id}`,
+ "@type": "Fact",
+ "statement": `${r.source_id} ${r.edge_type} ${r.target_id}`,
+ "subject": `node:${r.source_id}`,
+ "predicate": r.edge_type,
+ "object": `node:${r.target_id}`,
+ "confidence": meta.confidence ?? 1.0,
+ "source": meta.source || "kndl://v1-migration",
+ "validFrom": now,
+ "recordedAt": now,
+ "tags": ["v1-migration", "edge"],
+ });
+ }
+
+ for (const row of intents!) {
+ const r = row as { id: string; type_name: string; trigger_kind: string; trigger_data: string; actions_json: string; meta_json: string };
+ const meta = JSON.parse(r.meta_json || "{}");
+ writeFact({
+ "@context": "https://kndl.artdaw.com/context/v1.jsonld",
+ "@id": `fact:v1-intent-${r.id}`,
+ "@type": "Action",
+ "statement": `Intent ${r.id}: when ${r.trigger_data}`,
+ "subject": `intent:${r.id}`,
+ "predicate": "trigger",
+ "object": r.trigger_data,
+ "confidence": meta.priority ?? 0.5,
+ "source": "kndl://v1-migration",
+ "validFrom": now,
+ "recordedAt": now,
+ "tags": ["v1-migration", "intent", r.trigger_kind],
+ });
+ }
+
+ const summary = {
+ from: dbPath, to,
+ dry_run: dryRun,
+ nodes: (nodes!).length,
+ edges: (edges!).length,
+ intents: (intents!).length,
+ facts_written: written,
+ };
+ out(summary);
+ if (dryRun) process.stderr.write("(dry-run — no files written)\n");
+ return 0;
+ }
+
+ case "remote": {
+ const sub = argv.slice(1);
+ const subCmd = sub[0];
+ const { flags: rflags } = parseArgs(sub.slice(1));
+ switch (subCmd) {
+ case "add": {
+ const label = requireFlag(s(rflags.label), "label");
+ const storeId = requireFlag(s(rflags["store-id"]), "store-id");
+ const provider = (s(rflags.provider) ?? "anthropic") as "anthropic";
+ addRemote({
+ label, provider, store_id: storeId,
+ default_confidence: Number(rflags["default-confidence"] ?? 0.85),
+ push: false,
+ });
+ out({ added: label, store_id: storeId, provider });
+ return 0;
+ }
+ case "pull": {
+ const label = requireFlag(s(rflags._) ?? s(sub[1]), "label");
+ const apiKey = process.env.ANTHROPIC_API_KEY;
+ if (!apiKey) fail("ANTHROPIC_API_KEY is not set");
+ const remotes = loadRemoteConfigs();
+ const config = remotes.find((r) => r.label === label);
+ if (!config) fail(`Remote '${label}' not found. Run \`kndl remote add\` first.`);
+ const client = new AnthropicMemoryClient(apiKey);
+ const store = makeStore();
+ const result = await pull(client, store, config!);
+ const idx = remotes.findIndex((r) => r.label === label);
+ if (idx >= 0) remotes[idx] = config!;
+ saveRemoteConfigs(remotes);
+ out(result);
+ return 0;
+ }
+ case "ls":
+ case "list": {
+ out(loadRemoteConfigs().map((r) => ({
+ label: r.label, provider: r.provider, store_id: r.store_id,
+ last_synced_at: r.last_synced_at ?? null,
+ })));
+ return 0;
+ }
+ case "rm":
+ case "remove": {
+ const label = requireFlag(s(rflags._) ?? s(sub[1]), "label");
+ const removed = removeRemote(label);
+ out({ removed, label });
+ return 0;
+ }
+ default:
+ fail(`unknown remote sub-command: ${subCmd ?? "(none)"}. Try: add, pull, ls, rm`);
+ }
+ return 0;
+ }
+ default:
+ fail(`unknown command: ${cmd}. Run \`kndl help\` for usage.`);
+ }
+ } catch (e) {
+ fail((e as Error).message);
+ }
+}
+
+main(process.argv.slice(2)).then(process.exit).catch((e) => {
+ process.stderr.write(`fatal: ${(e as Error).message}\n`);
+ process.exit(1);
+});
diff --git a/packages/kndl-memory/src/core.ts b/packages/kndl-memory/src/core.ts
new file mode 100644
index 0000000..f7de64a
--- /dev/null
+++ b/packages/kndl-memory/src/core.ts
@@ -0,0 +1,248 @@
+// core.ts — pure logic shared by all store backends and the CLI.
+//
+// No filesystem, no database, no network. Only:
+// • decay math
+// • time utilities
+// • fact construction
+// • query / contradiction / provenance algorithms operating on Fact[]
+
+import { createHash } from "node:crypto";
+import type {
+ Fact, FactInput, QueryOptions, QueryResult, QueryResultFact,
+ ContradictionEntry, ContradictionsResult, ProvenanceNode, ProvenanceResult,
+} from "./types.js";
+
+export type { Fact, FactInput, QueryOptions, QueryResult, QueryResultFact,
+ ContradictionEntry, ContradictionsResult, ProvenanceNode, ProvenanceResult };
+export type { FactStore, AssertResult, SupersedeResult } from "./types.js";
+
+// ── Decay math ────────────────────────────────────────────────────────────────
+
+const UNIT_SECONDS: Record = {
+ ns: 1e-9, us: 1e-6, ms: 1e-3,
+ s: 1, m: 60, h: 3600,
+ d: 86_400, w: 7 * 86_400,
+ mo: 30 * 86_400, y: 365 * 86_400,
+};
+
+const DUR_RE = /^(\d+(?:\.\d+)?)(ns|us|ms|s|m|h|d|w|mo|y)$/;
+
+export function parseDurationSeconds(s: string): number {
+ const m = DUR_RE.exec(s.trim());
+ if (!m) throw new Error(`bad duration: ${JSON.stringify(s)}`);
+ return parseFloat(m[1]) * UNIT_SECONDS[m[2]];
+}
+
+export interface DecaySpec { rate: number; windowSeconds: number; }
+
+export function parseDecay(decay: string | null | undefined): DecaySpec | null {
+ if (!decay) return null;
+ if (!decay.includes("/")) throw new Error(`bad decay (need rate/window): ${decay}`);
+ const [rateStr, windowStr] = decay.split("/", 2);
+ const rate = parseFloat(rateStr);
+ const windowSeconds = parseDurationSeconds(windowStr);
+ if (!(rate > 0 && rate < 1)) throw new Error(`decay rate must be in (0,1): ${rate}`);
+ if (!(windowSeconds > 0)) throw new Error(`decay window must be positive: ${windowSeconds}`);
+ return { rate, windowSeconds };
+}
+
+export function effectiveConfidence(fact: Fact, atIso: string): number {
+ const base = fact.confidence ?? 0;
+ const spec = parseDecay(fact.decay);
+ if (!spec) return base;
+ const anchorIso = fact.validFrom ?? fact.observedAt ?? fact.recordedAt;
+ if (!anchorIso) return base;
+ const elapsed = (new Date(atIso).getTime() - new Date(anchorIso).getTime()) / 1000;
+ if (elapsed <= 0) return base;
+ return base * Math.pow(spec.rate, elapsed / spec.windowSeconds);
+}
+
+// ── Time utilities ────────────────────────────────────────────────────────────
+
+export function nowIso(): string {
+ return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
+}
+
+export function normalizeTime(s: string | undefined, fallback: string): string {
+ if (!s || s === "now") return fallback;
+ const d = new Date(s);
+ if (isNaN(d.getTime())) throw new Error(`bad datetime: ${s}`);
+ return d.toISOString().replace(/\.\d{3}Z$/, "Z");
+}
+
+// ── Fact construction ────────────────────────────────────────────────────────
+
+export function slugify(s: string): string {
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || "fact";
+}
+
+export function makeId(subject: string | undefined, predicate: string | undefined, statement: string): string {
+ const ts = nowIso().replace(/[-:]/g, "").replace("T", "t").replace("Z", "z");
+ const h = createHash("sha256").update(statement).digest("hex").slice(0, 8);
+ const parts = ["fact"];
+ if (subject) parts.push(slugify(subject));
+ if (predicate) parts.push(slugify(predicate));
+ return `${parts.join(":")}-${ts}-${h}`;
+}
+
+export function factFilename(id: string): string {
+ return slugify(id.replace(/:/g, "-")) + ".fact.json";
+}
+
+export function buildFact(input: FactInput, contextRel: string, supersedesId?: string): Fact {
+ if (!(input.confidence >= 0 && input.confidence <= 1)) {
+ throw new Error(`confidence must be in [0,1]: ${input.confidence}`);
+ }
+ if (input.decay) parseDecay(input.decay); // validate
+
+ const recordedAt = nowIso();
+ const validFrom = normalizeTime(input.validFrom, recordedAt);
+ const id = makeId(input.subject, input.predicate, input.statement);
+
+ const fact: Fact = {
+ "@context": contextRel,
+ "@id": id,
+ "@type": "Fact",
+ statement: input.statement,
+ confidence: input.confidence,
+ source: input.source,
+ validFrom,
+ recordedAt,
+ };
+ if (input.observedAt) fact.observedAt = normalizeTime(input.observedAt, recordedAt);
+ if (input.validUntil) fact.validUntil = normalizeTime(input.validUntil, recordedAt);
+ if (input.subject) fact.subject = input.subject;
+ if (input.predicate) fact.predicate = input.predicate;
+ if (input.object !== undefined) fact.object = input.object;
+ if (input.decay) fact.decay = input.decay;
+ if (input.classification) fact.classification = input.classification;
+ if (input.consent) fact.consent = input.consent;
+ if (input.tenant) fact.tenant = input.tenant;
+ if (input.derivedFrom) fact.derivedFrom = input.derivedFrom;
+ if (input.negated) fact.negated = true;
+ if (supersedesId) fact.supersedes = supersedesId;
+ if (input.tags?.length) fact.tags = input.tags;
+ return fact;
+}
+
+// ── Shared query algorithms ──────────────────────────────────────────────────
+// These operate on an in-memory Fact[] and are store-agnostic.
+
+export function supersededIds(facts: Fact[]): Set {
+ const out = new Set();
+ for (const f of facts) if (f.supersedes) out.add(f.supersedes);
+ return out;
+}
+
+function factMatches(f: Fact, subject?: string, predicate?: string): boolean {
+ if (subject && f.subject !== subject) return false;
+ if (predicate && f.predicate !== predicate) return false;
+ return true;
+}
+
+function round4(n: number): number {
+ return Math.round(n * 10_000) / 10_000;
+}
+
+export function applyQuery(facts: Fact[], opts: QueryOptions = {}): QueryResult {
+ const superseded = supersededIds(facts);
+ const asOf = normalizeTime(opts.asOf, nowIso());
+ const asOfMs = new Date(asOf).getTime();
+ const minConf = opts.minConfidence ?? 0;
+
+ const rows: QueryResultFact[] = facts
+ .filter((f) => !superseded.has(f["@id"]))
+ .filter((f) => factMatches(f, opts.subject, opts.predicate))
+ .filter((f) => !opts.tenant || f.tenant === opts.tenant)
+ .filter((f) => new Date(f.recordedAt).getTime() <= asOfMs)
+ .filter((f) => !(f.classification === "PHI" && !opts.allowPhi))
+ .map((f) => ({ ...f, effective_confidence: round4(effectiveConfidence(f, asOf)) }))
+ .filter((f) => f.effective_confidence >= minConf)
+ .sort((a, b) => b.effective_confidence - a.effective_confidence);
+
+ return { as_of: asOf, count: rows.length, facts: rows };
+}
+
+export function findContradictions(
+ facts: Fact[],
+ opts: { subject?: string; predicate?: string } = {},
+): ContradictionsResult {
+ const superseded = supersededIds(facts);
+ const asOf = nowIso();
+
+ const groups = new Map();
+ for (const f of facts) {
+ if (superseded.has(f["@id"])) continue;
+ if (!factMatches(f, opts.subject, opts.predicate)) continue;
+ const key = JSON.stringify([f.subject ?? null, f.predicate ?? null]);
+ let bucket = groups.get(key);
+ if (!bucket) groups.set(key, bucket = []);
+ bucket.push(f);
+ }
+
+ const conflicts: ContradictionEntry[] = [];
+ for (const group of groups.values()) {
+ if (group.length < 2) continue;
+ const distinct = new Set(group.map((g) => JSON.stringify([g.object ?? null, !!g.negated])));
+ if (distinct.size <= 1) continue;
+ const ranked = [...group].sort((a, b) => {
+ const an = a.negated ? 1 : 0, bn = b.negated ? 1 : 0;
+ if (an !== bn) return an - bn;
+ const ar = new Date(a.recordedAt).getTime(), br = new Date(b.recordedAt).getTime();
+ if (ar !== br) return br - ar;
+ const ae = effectiveConfidence(a, asOf), be = effectiveConfidence(b, asOf);
+ if (ae !== be) return be - ae;
+ return (a.derivedFrom?.length ?? 0) - (b.derivedFrom?.length ?? 0);
+ });
+ conflicts.push({
+ subject: ranked[0].subject,
+ predicate: ranked[0].predicate,
+ preferred: {
+ id: ranked[0]["@id"],
+ object: ranked[0].object ?? null,
+ negated: ranked[0].negated ?? false,
+ effective_confidence: round4(effectiveConfidence(ranked[0], asOf)),
+ },
+ conflicts_with: ranked.slice(1).map((g) => ({
+ id: g["@id"],
+ object: g.object ?? null,
+ negated: g.negated ?? false,
+ effective_confidence: round4(effectiveConfidence(g, asOf)),
+ })),
+ });
+ }
+ return { count: conflicts.length, conflicts };
+}
+
+export function buildProvenanceChain(
+ byId: Map,
+ rootId: string,
+ maxDepth = 8,
+): ProvenanceResult {
+ const visited = new Set();
+ const chain: ProvenanceNode[] = [];
+
+ const walk = (id: string, depth: number): void => {
+ if (depth > maxDepth || visited.has(id)) return;
+ visited.add(id);
+ const f = byId.get(id);
+ if (!f) {
+ chain.push({ id, missing: true });
+ return;
+ }
+ chain.push({
+ id: f["@id"],
+ statement: f.statement,
+ source: f.source,
+ confidence: f.confidence,
+ recordedAt: f.recordedAt,
+ derivedFrom: f.derivedFrom ?? [],
+ supersedes: f.supersedes,
+ });
+ for (const ref of f.derivedFrom ?? []) walk(ref, depth + 1);
+ if (f.supersedes) walk(f.supersedes, depth + 1);
+ };
+
+ walk(rootId, 0);
+ return { root: rootId, depth: chain.length, chain };
+}
diff --git a/packages/kndl-memory/src/notify.ts b/packages/kndl-memory/src/notify.ts
new file mode 100644
index 0000000..a44f470
--- /dev/null
+++ b/packages/kndl-memory/src/notify.ts
@@ -0,0 +1,207 @@
+// notify.ts — change detection and broadcast layer.
+//
+// Adapts each backend's native change feed into a shared EventEmitter so the
+// MCP server's broadcast loop is store-agnostic.
+//
+// In-process writes: NotifyingStore wraps any FactStore and fires "change" on
+// every assertFact / supersedeFact call. This covers:
+// - all writes from the same MCP process (the common case)
+//
+// Cross-process writes (a second process writing to the same store):
+// - FsFactStore: chokidar watches the facts/ directory for new .fact.json files
+// - SqliteFactStore: SQLite update_hook fires for any INSERT in the same connection;
+// for cross-process we attach a polling loop on kndl_changes table
+//
+// DuckDB and Supabase cross-process detection is not implemented in v2.0 —
+// for those backends, use the HTTP transport (single process, all writes through it).
+
+import { EventEmitter } from "node:events";
+import { randomUUID } from "node:crypto";
+import type { FactStore, FactInput, QueryOptions, QueryResult,
+ ContradictionsResult, ProvenanceResult, AssertResult, SupersedeResult, Fact } from "./types";
+
+// ── Change event ──────────────────────────────────────────────────────────────
+
+export interface ChangeEvent {
+ factId: string;
+ type: "created" | "superseded";
+}
+
+// ── NotifyingStore — wraps any FactStore, emits "change" on every write ───────
+
+export class NotifyingStore implements FactStore {
+ readonly emitter = new EventEmitter();
+
+ constructor(private readonly inner: FactStore) {}
+
+ private emit(factId: string, type: ChangeEvent["type"]): void {
+ this.emitter.emit("change", { factId, type } satisfies ChangeEvent);
+ }
+
+ async assertFact(input: FactInput, supersedesId?: string): Promise {
+ const r = await this.inner.assertFact(input, supersedesId);
+ this.emit(r.id, supersedesId ? "superseded" : "created");
+ return r;
+ }
+
+ async supersedeFact(oldId: string, input: FactInput): Promise {
+ const r = await this.inner.supersedeFact(oldId, input);
+ this.emit(r.id, "superseded");
+ return r;
+ }
+
+ async query(opts?: QueryOptions): Promise {
+ return this.inner.query(opts);
+ }
+
+ async contradictions(opts?: { subject?: string; predicate?: string }): Promise {
+ return this.inner.contradictions(opts);
+ }
+
+ async provenanceChain(rootId: string, maxDepth?: number): Promise {
+ return this.inner.provenanceChain(rootId, maxDepth);
+ }
+
+ async list(subject?: string): Promise {
+ return this.inner.list(subject);
+ }
+
+ async show(id: string): Promise {
+ return this.inner.show(id);
+ }
+
+ async close(): Promise {
+ return this.inner.close?.();
+ }
+}
+
+// ── FS cross-process watcher (chokidar) ───────────────────────────────────────
+
+export async function attachFsWatcher(
+ factsDir: string,
+ handler: (event: ChangeEvent) => void,
+): Promise<() => void> {
+ const chokidar = await import("chokidar");
+ const watcher = chokidar.watch(factsDir, {
+ persistent: false,
+ ignoreInitial: true,
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 },
+ });
+
+ watcher.on("add", (filePath: string) => {
+ if (!filePath.endsWith(".fact.json")) return;
+ // Derive the factId from the filename: remove .fact.json, unsluggify : separators
+ const basename = filePath.split("/").pop()!.replace(/\.fact\.json$/, "");
+ // The filename is slug(id) — we can't recover the exact @id, so emit with
+ // the filename as a stand-in; the server will show(id) to get the real fact.
+ handler({ factId: basename, type: "created" });
+ });
+
+ return () => { watcher.close(); };
+}
+
+// ── SQLite cross-process polling ──────────────────────────────────────────────
+// Polls a lightweight kndl_changes table every POLL_MS for new rows.
+// The table is created lazily; if it doesn't exist the poller does nothing.
+
+export function attachSqlitePoller(
+ db: import("better-sqlite3").Database,
+ handler: (event: ChangeEvent) => void,
+ pollMs = 2000,
+): () => void {
+ // Create the change-log table if not present
+ try {
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS kndl_changes (
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
+ fact_id TEXT NOT NULL,
+ event_type TEXT NOT NULL,
+ changed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
+ )
+ `);
+ } catch {
+ // read-only connection or other issue — polling won't work
+ return () => {};
+ }
+
+ let lastSeq: number = (db.prepare(
+ "SELECT COALESCE(MAX(seq), 0) AS s FROM kndl_changes",
+ ).get() as { s: number }).s;
+
+ const timer = setInterval(() => {
+ try {
+ const rows = db.prepare(
+ "SELECT seq, fact_id, event_type FROM kndl_changes WHERE seq > ? ORDER BY seq",
+ ).all(lastSeq) as { seq: number; fact_id: string; event_type: string }[];
+
+ for (const r of rows) {
+ lastSeq = r.seq;
+ handler({ factId: r.fact_id, type: r.event_type as ChangeEvent["type"] });
+ }
+ } catch {
+ // table might have been dropped — ignore
+ }
+ }, pollMs);
+
+ timer.unref(); // don't prevent process exit
+
+ return () => clearInterval(timer);
+}
+
+// Write a change-log entry from within a SqliteFactStore write (called by the store after INSERT).
+// Exported so SqliteFactStore can use it without depending on this module at import time.
+export function logSqliteChange(
+ db: import("better-sqlite3").Database,
+ factId: string,
+ eventType: ChangeEvent["type"],
+): void {
+ try {
+ db.prepare(
+ "INSERT INTO kndl_changes(fact_id, event_type) VALUES (?, ?)",
+ ).run(factId, eventType);
+ } catch {
+ // table doesn't exist yet — fine, poller will create it on attach
+ }
+}
+
+// ── Subscription registry ─────────────────────────────────────────────────────
+
+export interface SubscribeFilter {
+ subject?: string;
+ predicate?: string;
+ tenant?: string;
+}
+
+export interface Subscription {
+ id: string;
+ filter: SubscribeFilter;
+}
+
+export class SubscriptionRegistry {
+ private subs = new Map();
+
+ add(filter: SubscribeFilter): string {
+ const id = randomUUID();
+ this.subs.set(id, { id, filter });
+ return id;
+ }
+
+ remove(id: string): boolean {
+ return this.subs.delete(id);
+ }
+
+ list(): Subscription[] {
+ return [...this.subs.values()];
+ }
+
+ matches(fact: Partial<{ subject: string; predicate: string; tenant: string }>): Subscription[] {
+ return [...this.subs.values()].filter((s) => {
+ if (s.filter.subject && s.filter.subject !== fact.subject) return false;
+ if (s.filter.predicate && s.filter.predicate !== fact.predicate) return false;
+ if (s.filter.tenant && s.filter.tenant !== fact.tenant) return false;
+ return true;
+ });
+ }
+
+ get size(): number { return this.subs.size; }
+}
diff --git a/packages/kndl-memory/src/remote/anthropic.ts b/packages/kndl-memory/src/remote/anthropic.ts
new file mode 100644
index 0000000..8bf1871
--- /dev/null
+++ b/packages/kndl-memory/src/remote/anthropic.ts
@@ -0,0 +1,172 @@
+// remote/anthropic.ts — thin REST wrapper around the Anthropic Memory Stores beta API.
+//
+// Uses fetch() directly rather than the @anthropic-ai/sdk so that API surface
+// changes don't require a hard dependency bump. Gate all calls on ANTHROPIC_API_KEY.
+//
+// NOTE (Phase 5 — open question Q7):
+// The Memory Stores API is in beta. We have NOT yet verified whether
+// it supports a watermark / "since" cursor for incremental pulls.
+// Until verified:
+// - We always paginate through the full list and detect new items by
+// comparing to the last_cursor stored in remotes.json.
+// - watch_memory_store is NOT shipped in v2.0 (would require the watcher
+// loop to poll efficiently; polling the full list every N seconds is
+// too expensive at reasonable intervals).
+// Once the watermark call is confirmed, upgrade listItems to use it and
+// re-enable watch_memory_store.
+//
+// Endpoints assumed (adjust if the beta API differs):
+// GET /v1/memory-stores/{store_id}/items[?after=&limit=]
+// GET /v1/memory-stores/{store_id}/items/{item_id}
+// POST /v1/memory-stores/{store_id}/items (used by push, v2.1)
+//
+// Beta header: anthropic-beta: memory-stores-2025-08-01
+// (Pin this to a date you've tested against; update when the API stabilises.)
+
+import type { MemoryStoreClient, MemoryStoreItem, MemoryStoreListResult, ListItemsOptions } from "./types.js";
+
+const BASE_URL = "https://api.anthropic.com";
+const BETA_HEADER = "memory-stores-2025-08-01";
+const API_VERSION = "2023-06-01";
+
+/** Exponential backoff on 429 / 529 responses. */
+async function withRetry(
+ fn: () => Promise,
+ maxRetries = 4,
+ baseDelayMs = 1000,
+): Promise {
+ let lastErr: unknown;
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
+ try {
+ return await fn();
+ } catch (e) {
+ lastErr = e;
+ const status = (e as { status?: number }).status;
+ if (status !== 429 && status !== 529) throw e;
+ if (attempt === maxRetries) break;
+ const delay = baseDelayMs * Math.pow(2, attempt) + Math.random() * 200;
+ await new Promise((r) => setTimeout(r, delay));
+ }
+ }
+ throw lastErr;
+}
+
+export class AnthropicMemoryClient implements MemoryStoreClient {
+ constructor(private readonly apiKey: string) {}
+
+ private async fetch(path: string, init?: RequestInit): Promise {
+ const url = `${BASE_URL}${path}`;
+ const resp = await withRetry(async () => {
+ const r = await fetch(url, {
+ ...init,
+ headers: {
+ "x-api-key": this.apiKey,
+ "anthropic-version": API_VERSION,
+ "anthropic-beta": BETA_HEADER,
+ "content-type": "application/json",
+ ...(init?.headers ?? {}),
+ },
+ });
+ if (!r.ok) {
+ const body = await r.text().catch(() => "");
+ const err: { status: number; message: string } = {
+ status: r.status,
+ message: `Anthropic API ${r.status}: ${body.slice(0, 200)}`,
+ };
+ throw Object.assign(new Error(err.message), { status: r.status });
+ }
+ return r;
+ });
+ return resp.json() as Promise;
+ }
+
+ async listItems(storeId: string, opts: ListItemsOptions = {}): Promise {
+ const params = new URLSearchParams();
+ if (opts.after) params.set("after", opts.after);
+ if (opts.limit) params.set("limit", String(opts.limit));
+ const qs = params.toString() ? `?${params}` : "";
+
+ // Expected response shape — adjust to actual API response envelope.
+ const raw = await this.fetch<{
+ data?: MemoryStoreItem[];
+ items?: MemoryStoreItem[];
+ next_cursor?: string;
+ has_more?: boolean;
+ }>(`/v1/memory-stores/${storeId}/items${qs}`);
+
+ const items = raw.data ?? raw.items ?? [];
+ return {
+ items,
+ next_cursor: raw.next_cursor,
+ has_more: raw.has_more ?? !!raw.next_cursor,
+ };
+ }
+
+ async getItem(storeId: string, itemId: string): Promise {
+ try {
+ return await this.fetch(
+ `/v1/memory-stores/${storeId}/items/${itemId}`,
+ );
+ } catch (e) {
+ if ((e as { status?: number }).status === 404) return null;
+ throw e;
+ }
+ }
+
+ async createItem(
+ storeId: string,
+ content: string,
+ metadata?: Record,
+ ): Promise {
+ return this.fetch(`/v1/memory-stores/${storeId}/items`, {
+ method: "POST",
+ body: JSON.stringify({ content, metadata }),
+ });
+ }
+}
+
+// ── Fake client for tests / CI ────────────────────────────────────────────────
+
+export class FakeMemoryStoreClient implements MemoryStoreClient {
+ private stores = new Map>();
+ private seq = 0;
+
+ seed(storeId: string, items: Array<{ content: string; metadata?: Record }>): void {
+ if (!this.stores.has(storeId)) this.stores.set(storeId, new Map());
+ const store = this.stores.get(storeId)!;
+ for (const item of items) {
+ const id = `fake_item_${++this.seq}`;
+ const now = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
+ store.set(id, { id, store_id: storeId, content: item.content, created_at: now, metadata: item.metadata });
+ }
+ }
+
+ async listItems(storeId: string, opts: ListItemsOptions = {}): Promise {
+ const store = this.stores.get(storeId);
+ const all = store ? [...store.values()] : [];
+ const startIdx = opts.after
+ ? all.findIndex((i) => i.id === opts.after) + 1
+ : 0;
+ const limit = opts.limit ?? 100;
+ const page = all.slice(startIdx, startIdx + limit);
+ const hasMore = startIdx + limit < all.length;
+ return {
+ items: page,
+ next_cursor: hasMore ? page[page.length - 1]?.id : undefined,
+ has_more: hasMore,
+ };
+ }
+
+ async getItem(storeId: string, itemId: string): Promise {
+ return this.stores.get(storeId)?.get(itemId) ?? null;
+ }
+
+ async createItem(storeId: string, content: string, metadata?: Record): Promise {
+ if (!this.stores.has(storeId)) this.stores.set(storeId, new Map());
+ const id = `fake_item_${++this.seq}`;
+ const now = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
+ const item: MemoryStoreItem = { id, store_id: storeId, content, created_at: now, metadata };
+ this.stores.get(storeId)!.set(id, item);
+ return item;
+ }
+}
diff --git a/packages/kndl-memory/src/remote/config.ts b/packages/kndl-memory/src/remote/config.ts
new file mode 100644
index 0000000..e6001a5
--- /dev/null
+++ b/packages/kndl-memory/src/remote/config.ts
@@ -0,0 +1,74 @@
+// remote/config.ts — ~/.kndl/remotes.json management.
+//
+// Also reads KNDL_REMOTE_STORES env var for one-shot config without a file:
+// KNDL_REMOTE_STORES="anthropic:store_abc123:personal,anthropic:store_xyz:work"
+
+import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+import { homedir } from "node:os";
+import type { RemoteConfig, RemotesFile } from "./types.js";
+
+export function remotesPath(): string {
+ return join(homedir(), ".kndl", "remotes.json");
+}
+
+export function loadRemoteConfigs(): RemoteConfig[] {
+ // 1. Environment variable override
+ const envVar = process.env.KNDL_REMOTE_STORES;
+ if (envVar) {
+ return envVar.split(",").map((entry) => {
+ const parts = entry.trim().split(":");
+ if (parts.length < 3 || parts[0] !== "anthropic") {
+ throw new Error(`Bad KNDL_REMOTE_STORES entry: ${entry}. Format: anthropic::`);
+ }
+ return {
+ label: parts[2],
+ provider: "anthropic" as const,
+ store_id: parts[1],
+ default_confidence: 0.85,
+ push: false,
+ };
+ });
+ }
+
+ // 2. File
+ const p = remotesPath();
+ if (!existsSync(p)) return [];
+ try {
+ const data = JSON.parse(readFileSync(p, "utf8")) as RemotesFile;
+ return data.remotes ?? [];
+ } catch (e) {
+ process.stderr.write(`[kndl] Warning: could not parse ${p}: ${(e as Error).message}\n`);
+ return [];
+ }
+}
+
+export function saveRemoteConfigs(remotes: RemoteConfig[]): void {
+ const p = remotesPath();
+ mkdirSync(join(homedir(), ".kndl"), { recursive: true });
+ const data: RemotesFile = { remotes };
+ writeFileSync(p, JSON.stringify(data, null, 2));
+}
+
+export function addRemote(config: RemoteConfig): void {
+ const remotes = loadRemoteConfigs();
+ const idx = remotes.findIndex((r) => r.label === config.label);
+ if (idx >= 0) {
+ remotes[idx] = config;
+ } else {
+ remotes.push(config);
+ }
+ saveRemoteConfigs(remotes);
+}
+
+export function removeRemote(label: string): boolean {
+ const remotes = loadRemoteConfigs();
+ const filtered = remotes.filter((r) => r.label !== label);
+ if (filtered.length === remotes.length) return false;
+ saveRemoteConfigs(filtered);
+ return true;
+}
+
+export function getRemote(label: string): RemoteConfig | undefined {
+ return loadRemoteConfigs().find((r) => r.label === label);
+}
diff --git a/packages/kndl-memory/src/remote/sync.ts b/packages/kndl-memory/src/remote/sync.ts
new file mode 100644
index 0000000..bb7e1df
--- /dev/null
+++ b/packages/kndl-memory/src/remote/sync.ts
@@ -0,0 +1,127 @@
+// remote/sync.ts — pull driver for Anthropic Memory Store → local FactStore.
+//
+// Translation: each Memory Store item becomes one Fact.
+// Idempotency: same item id → same source URI → existing fact detected by
+// querying source field → no-op if content hash unchanged,
+// supersede if content changed.
+// Conflict: after each pull batch, run contradictions() to surface any
+// conflicts between pulled facts and local facts on the same
+// subject/predicate.
+// Push: explicitly OUT OF SCOPE for v2.0 (plan §12 Q8). The push()
+// function is a stub that throws; it will be filled in v2.1.
+
+import { createHash } from "node:crypto";
+import type { FactStore, Fact } from "../types.js";
+import { nowIso } from "../core.js";
+import type { MemoryStoreClient, RemoteConfig, SyncResult } from "./types.js";
+
+// ── Translation ────────────────────────────────────────────────────────────────
+
+function itemSourceUri(storeId: string, itemId: string): string {
+ return `claude-memory://${storeId}/${itemId}`;
+}
+
+function contentHash(content: string): string {
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
+}
+
+function itemToFactId(storeId: string, itemId: string): string {
+ // Stable fact id tied to the Memory Store item id.
+ const slug = `${storeId}-${itemId}`.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 80);
+ return `fact:claude-store-${slug}`;
+}
+
+// ── Existing-fact lookup ───────────────────────────────────────────────────────
+// We find whether a Memory Store item has already been pulled by looking for
+// a fact whose source == claude-memory://{storeId}/{itemId}.
+// Because the FactStore interface has no index on source, we load all and filter.
+// For large stores this is fine — remote pull is infrequent and bounded by API rate.
+
+async function findBySource(store: FactStore, sourceUri: string): Promise {
+ const result = await store.query();
+ return result.facts.find((f) => f.source === sourceUri) ?? null;
+}
+
+// ── Pull driver ────────────────────────────────────────────────────────────────
+
+export async function pull(
+ client: MemoryStoreClient,
+ store: FactStore,
+ config: RemoteConfig,
+): Promise {
+ const syncedAt = nowIso();
+ let pulled = 0, skipped = 0, superseded = 0;
+ let cursor = config.last_cursor;
+
+ // Paginate through all items (or all new items after the last cursor).
+ // NOTE: If the API supports a true "since watermark" (not pagination cursor),
+ // replace cursor logic with since=config.last_synced_at when that is verified.
+ let hasMore = true;
+ while (hasMore) {
+ const page = await client.listItems(config.store_id, { after: cursor, limit: 100 });
+
+ for (const item of page.items) {
+ const sourceUri = itemSourceUri(config.store_id, item.id);
+ const existing = await findBySource(store, sourceUri);
+
+ if (existing) {
+ // Check if content changed (hash stored in tags or statement prefix).
+ const existingHash = existing.tags?.find((t) => t.startsWith("content-hash:"))?.slice(13);
+ const newHash = contentHash(item.content);
+ if (existingHash === newHash) {
+ skipped++;
+ continue;
+ }
+ // Content changed — supersede the old fact.
+ await store.supersedeFact(existing["@id"], {
+ statement: item.content,
+ confidence: config.default_confidence,
+ source: sourceUri,
+ validFrom: item.updated_at ?? item.created_at,
+ tenant: config.label,
+ tags: ["from-anthropic-memory", config.store_id, `content-hash:${newHash}`],
+ });
+ superseded++;
+ } else {
+ // New item — assert as a fact.
+ await store.assertFact({
+ statement: item.content,
+ confidence: config.default_confidence,
+ source: sourceUri,
+ validFrom: item.created_at,
+ observedAt: item.created_at,
+ tenant: config.label,
+ tags: ["from-anthropic-memory", config.store_id, `content-hash:${contentHash(item.content)}`],
+ });
+ pulled++;
+ }
+ }
+
+ if (page.next_cursor) cursor = page.next_cursor;
+ hasMore = page.has_more;
+ }
+
+ // Update watermark on config (caller must persist this).
+ config.last_synced_at = syncedAt;
+ if (cursor) config.last_cursor = cursor;
+
+ // Run contradictions to surface any conflicts that pulling introduced.
+ const contradictResult = await store.contradictions({ tenant: config.label });
+ const contradictions = contradictResult.count;
+
+ return { store_id: config.store_id, label: config.label, pulled, skipped, superseded, contradictions, synced_at: syncedAt };
+}
+
+// ── Push stub (v2.1) ──────────────────────────────────────────────────────────
+
+export async function push(
+ _client: MemoryStoreClient,
+ _store: FactStore,
+ _config: RemoteConfig,
+): Promise {
+ throw new Error(
+ "push() is not implemented in v2.0. " +
+ "Push (local → Anthropic Memory Store) will ship in v2.1. " +
+ "To share local facts, export with `kndl export` and paste into the Memory Store manually.",
+ );
+}
diff --git a/packages/kndl-memory/src/remote/types.ts b/packages/kndl-memory/src/remote/types.ts
new file mode 100644
index 0000000..957283e
--- /dev/null
+++ b/packages/kndl-memory/src/remote/types.ts
@@ -0,0 +1,59 @@
+// remote/types.ts — interfaces for the Anthropic Memory Stores remote layer.
+
+/** One item in an Anthropic Memory Store. */
+export interface MemoryStoreItem {
+ id: string;
+ store_id: string;
+ content: string;
+ created_at: string; // ISO datetime
+ updated_at?: string; // ISO datetime
+ metadata?: Record;
+}
+
+/** Paginated list response. */
+export interface MemoryStoreListResult {
+ items: MemoryStoreItem[];
+ /** Cursor for next page; undefined when there are no more pages. */
+ next_cursor?: string;
+ has_more: boolean;
+}
+
+export interface ListItemsOptions {
+ /** Cursor returned from a previous list call (for pagination). */
+ after?: string;
+ limit?: number;
+}
+
+/** Abstract interface for a Memory Store client — real or fake. */
+export interface MemoryStoreClient {
+ listItems(storeId: string, opts?: ListItemsOptions): Promise;
+ getItem(storeId: string, itemId: string): Promise;
+ createItem(storeId: string, content: string, metadata?: Record): Promise;
+}
+
+/** A configured remote store registered locally. */
+export interface RemoteConfig {
+ label: string;
+ provider: "anthropic";
+ store_id: string;
+ default_confidence: number; // default 0.85
+ last_synced_at?: string; // ISO datetime watermark
+ last_cursor?: string; // pagination cursor from last pull
+ push: boolean; // false in v2.0
+ push_tag?: string;
+}
+
+export interface RemotesFile {
+ remotes: RemoteConfig[];
+}
+
+/** Result of a sync pull. */
+export interface SyncResult {
+ store_id: string;
+ label: string;
+ pulled: number; // new facts written
+ skipped: number; // items already up-to-date
+ superseded: number; // facts updated because item content changed
+ contradictions: number; // conflicts detected after pull
+ synced_at: string; // ISO datetime
+}
diff --git a/packages/kndl-memory/src/server.ts b/packages/kndl-memory/src/server.ts
new file mode 100644
index 0000000..0ee13c7
--- /dev/null
+++ b/packages/kndl-memory/src/server.ts
@@ -0,0 +1,397 @@
+#!/usr/bin/env node
+// kndl-memory-mcp — MCP server for KNDL JSON-LD facts.
+//
+// Tools:
+// assert_fact — write a new fact
+// query_facts — read active facts with effective confidence at as_of
+// contradictions — find disagreeing active facts
+// supersede_fact — write a fact replacing an older one (preserves history)
+// as_of — bitemporal time-travel query
+// provenance_chain — walk derivedFrom + supersedes backward
+// subscribe — register for notifications when matching facts change
+// unsubscribe — cancel a subscription
+// list_subscriptions — inspect active subscriptions
+// sync_memory_store — pull from an Anthropic Memory Store (gated on ANTHROPIC_API_KEY)
+// list_memory_stores — list configured remote stores and last-sync timestamps
+// watch_memory_store — NOT AVAILABLE in v2.0 (pending watermark API verification)
+//
+// Resources:
+// kndl://fact/{id} — live snapshot; clients re-read on notifications/resources/updated
+//
+// Transport:
+// stdio (default) — for Claude Desktop, Goose (stdio), Anthropic Memory
+// --http — StreamableHTTPServerTransport, port $PORT (default 8000), /mcp
+//
+// Env:
+// KNDL_STORAGE — store URL (default fs:./memory). See stores/index.ts.
+// KNDL_MEMORY_DIR — legacy alias for fs:
+// PORT — HTTP port (default 8000, --http only)
+
+import { randomUUID } from "node:crypto";
+import { Server } from "@modelcontextprotocol/sdk/server/index.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import {
+ CallToolRequestSchema,
+ ListToolsRequestSchema,
+ ReadResourceRequestSchema,
+ ListResourceTemplatesRequestSchema,
+} from "@modelcontextprotocol/sdk/types.js";
+import { z } from "zod";
+
+import { makeStore } from "./stores/index";
+import { NotifyingStore, SubscriptionRegistry, attachFsWatcher } from "./notify";
+import { AnthropicMemoryClient } from "./remote/anthropic";
+import { pull } from "./remote/sync";
+import { loadRemoteConfigs, addRemote, saveRemoteConfigs } from "./remote/config";
+import type { FactInput } from "./types";
+
+// ── Store setup ──────────────────────────────────────────────────────────────
+
+const innerStore = makeStore();
+const store = new NotifyingStore(innerStore);
+const subscriptions = new SubscriptionRegistry();
+
+// Attach cross-process FS watcher if the underlying store is FsFactStore
+import("./stores/fs").then(({ FsFactStore }) => {
+ if (innerStore instanceof FsFactStore) {
+ attachFsWatcher(innerStore.factsDir, (ev) => {
+ store.emitter.emit("change", ev);
+ }).catch(() => {});
+ }
+}).catch(() => {});
+
+// ── Broadcast on writes ───────────────────────────────────────────────────────
+// When a fact changes, send notifications/resources/updated to every
+// active session that has a matching subscription.
+
+const activeSessions = new Set();
+
+store.emitter.on("change", async (ev: { factId: string; type: string }) => {
+ const fact = await store.show(ev.factId).catch(() => null);
+ const matching = fact
+ ? subscriptions.matches({
+ subject: fact.subject,
+ predicate: fact.predicate,
+ tenant: fact.tenant,
+ })
+ : subscriptions.list(); // broadcast to all if fact not found
+
+ if (matching.length === 0) return;
+
+ const uri = `kndl://fact/${ev.factId}`;
+ for (const session of activeSessions) {
+ try {
+ await (session as unknown as {
+ sendResourceUpdated(p: { uri: string }): Promise;
+ }).sendResourceUpdated({ uri });
+ } catch {
+ // session may have closed — ignore
+ }
+ }
+});
+
+// ── Zod schemas ───────────────────────────────────────────────────────────────
+
+const AssertSchema = z.object({
+ statement: z.string().describe("Plain-language assertion"),
+ confidence: z.number().min(0).max(1).describe("Epistemic certainty 0–1"),
+ source: z.string().describe("URI of asserting entity; use human:// for user input"),
+ subject: z.string().optional().describe("Entity URI (structured triple form)"),
+ predicate: z.string().optional().describe("Property name (structured triple form)"),
+ object: z.unknown().optional().describe("Value or object URI (structured triple form)"),
+ decay: z.string().optional().describe("Decay spec e.g. '0.5/30d' (halves every 30 days)"),
+ valid_from: z.string().optional().describe("ISO datetime when fact became true in the world"),
+ valid_until: z.string().optional().describe("ISO datetime when fact expires"),
+ observed_at: z.string().optional().describe("ISO datetime when directly observed"),
+ classification: z.string().optional().describe("PII | PHI | PCI | INTERNAL | ..."),
+ consent: z.string().optional().describe("@id of consent scope (required if PHI)"),
+ tenant: z.string().optional().describe("Opaque tenant identifier"),
+ derived_from: z.array(z.string()).optional().describe("@ids of source facts"),
+ negated: z.boolean().optional().describe("True = this assertion is known-false"),
+});
+
+const QuerySchema = z.object({
+ subject: z.string().optional(),
+ predicate: z.string().optional(),
+ as_of: z.string().optional().describe("ISO datetime or 'now'"),
+ min_confidence: z.number().min(0).max(1).optional(),
+ tenant: z.string().optional(),
+ allow_phi: z.boolean().optional(),
+});
+
+const ContradictionsSchema = z.object({
+ subject: z.string().optional(),
+ predicate: z.string().optional(),
+});
+
+const SupersedeSchema = AssertSchema.extend({ old_id: z.string() });
+
+const AsOfSchema = z.object({
+ as_of: z.string().describe("ISO datetime — what did memory believe at this time"),
+ subject: z.string().optional(),
+ predicate: z.string().optional(),
+});
+
+const ProvenanceSchema = z.object({
+ id: z.string().describe("@id of the fact to trace"),
+ max_depth: z.number().int().positive().optional(),
+});
+
+const SubscribeSchema = z.object({
+ subject: z.string().optional().describe("Only notify for this subject"),
+ predicate: z.string().optional().describe("Only notify for this predicate"),
+ tenant: z.string().optional().describe("Only notify for this tenant"),
+});
+
+const UnsubscribeSchema = z.object({
+ id: z.string().describe("Subscription id returned by subscribe"),
+});
+
+const SyncMemoryStoreSchema = z.object({
+ label: z.string().describe("Remote label as registered with `kndl remote add`"),
+ direction: z.enum(["pull"]).default("pull").describe("Only 'pull' supported in v2.0"),
+ since: z.string().optional().describe("ISO datetime — pull only items created after this (if supported by the API)"),
+});
+
+// ── Helpers ───────────────────────────────────────────────────────────────────
+
+function toFactInput(args: z.infer): FactInput {
+ return {
+ statement: args.statement,
+ confidence: args.confidence,
+ source: args.source,
+ subject: args.subject,
+ predicate: args.predicate,
+ object: args.object,
+ decay: args.decay,
+ validFrom: args.valid_from,
+ validUntil: args.valid_until,
+ observedAt: args.observed_at,
+ classification: args.classification,
+ consent: args.consent,
+ tenant: args.tenant,
+ derivedFrom: args.derived_from,
+ negated: args.negated,
+ };
+}
+
+function zodToJson(schema: z.ZodTypeAny): Record {
+ const def = (schema as unknown as { _def: { typeName: string; [k: string]: unknown } })._def;
+ if (def.typeName === "ZodObject") {
+ const shape = (def.shape as () => Record)();
+ const properties: Record = {};
+ const required: string[] = [];
+ for (const [k, v] of Object.entries(shape)) {
+ properties[k] = zodToJson(v);
+ if (!(v as unknown as { isOptional(): boolean }).isOptional()) required.push(k);
+ }
+ return { type: "object", properties, required };
+ }
+ if (def.typeName === "ZodOptional") return zodToJson(def.innerType as z.ZodTypeAny);
+ if (def.typeName === "ZodString") return { type: "string" };
+ if (def.typeName === "ZodNumber") return { type: "number" };
+ if (def.typeName === "ZodBoolean") return { type: "boolean" };
+ if (def.typeName === "ZodArray") return { type: "array", items: zodToJson(def.type as z.ZodTypeAny) };
+ if (def.typeName === "ZodUnknown") return {};
+ return {};
+}
+
+function ok(result: unknown) {
+ return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
+}
+
+function err(e: unknown) {
+ return { isError: true, content: [{ type: "text" as const, text: `error: ${(e as Error).message}` }] };
+}
+
+// ── Server setup ──────────────────────────────────────────────────────────────
+
+const server = new Server(
+ { name: "kndl-memory", version: "2.0.0-alpha.3" },
+ { capabilities: { tools: {}, resources: {} } },
+);
+
+// ── Tools ─────────────────────────────────────────────────────────────────────
+
+const TOOLS = [
+ { name: "assert_fact", schema: AssertSchema, description: "Write a new immutable fact. Include source, confidence, validFrom. Add decay for time-sensitive data ('0.5/30d' halves every 30 days)." },
+ { name: "query_facts", schema: QuerySchema, description: "Read active (non-superseded) facts with effective confidence at as_of time. Filter by subject/predicate. Defaults to now." },
+ { name: "contradictions", schema: ContradictionsSchema, description: "Find disagreeing active facts about the same subject/predicate, ranked by recency, confidence, and chain length." },
+ { name: "supersede_fact", schema: SupersedeSchema, description: "Write a new fact replacing an older one. Preserves history — old fact hidden from queries but available for as_of time-travel." },
+ { name: "as_of", schema: AsOfSchema, description: "Bitemporal time-travel: what did memory believe at the given timestamp." },
+ { name: "provenance_chain", schema: ProvenanceSchema, description: "Walk derivedFrom + supersedes backward to surface the full audit trail of a fact." },
+ { name: "subscribe", schema: SubscribeSchema, description: "Register for notifications when facts matching the filter are written. Returns a subscription id. Re-read kndl://fact/{id} on notifications/resources/updated." },
+ { name: "unsubscribe", schema: UnsubscribeSchema, description: "Cancel a subscription by id." },
+ { name: "list_subscriptions", schema: z.object({}), description: "List active subscriptions and session count." },
+ { name: "sync_memory_store", schema: SyncMemoryStoreSchema, description: "Pull facts from a configured Anthropic Memory Store into the local fact store. Requires ANTHROPIC_API_KEY. Register stores with `kndl remote add`." },
+ { name: "list_memory_stores", schema: z.object({}), description: "List configured remote Anthropic Memory Stores and their last-sync timestamps." },
+] as const;
+
+server.setRequestHandler(ListToolsRequestSchema, async () => ({
+ tools: TOOLS.map((t) => ({
+ name: t.name,
+ description: t.description,
+ inputSchema: zodToJson(t.schema),
+ })),
+}));
+
+server.setRequestHandler(CallToolRequestSchema, async (req) => {
+ const { name, arguments: args } = req.params;
+ try {
+ switch (name) {
+ case "assert_fact": {
+ const a = AssertSchema.parse(args);
+ return ok(await store.assertFact(toFactInput(a)));
+ }
+ case "query_facts": {
+ const a = QuerySchema.parse(args);
+ return ok(await store.query({
+ subject: a.subject, predicate: a.predicate,
+ asOf: a.as_of, minConfidence: a.min_confidence,
+ tenant: a.tenant, allowPhi: a.allow_phi,
+ }));
+ }
+ case "contradictions": {
+ const a = ContradictionsSchema.parse(args);
+ return ok(await store.contradictions({ subject: a.subject, predicate: a.predicate }));
+ }
+ case "supersede_fact": {
+ const a = SupersedeSchema.parse(args);
+ const { old_id, ...rest } = a;
+ return ok(await store.supersedeFact(old_id, toFactInput(rest as z.infer)));
+ }
+ case "as_of": {
+ const a = AsOfSchema.parse(args);
+ return ok(await store.query({ subject: a.subject, predicate: a.predicate, asOf: a.as_of }));
+ }
+ case "provenance_chain": {
+ const a = ProvenanceSchema.parse(args);
+ return ok(await store.provenanceChain(a.id, a.max_depth));
+ }
+ case "subscribe": {
+ const a = SubscribeSchema.parse(args);
+ const id = subscriptions.add({ subject: a.subject, predicate: a.predicate, tenant: a.tenant });
+ return ok({ subscription_id: id, filter: a, message: `Subscribed. Re-read kndl://fact/ on notifications/resources/updated.` });
+ }
+ case "unsubscribe": {
+ const a = UnsubscribeSchema.parse(args);
+ const removed = subscriptions.remove(a.id);
+ return ok({ removed, id: a.id });
+ }
+ case "list_subscriptions": {
+ return ok({ count: subscriptions.size, subscriptions: subscriptions.list(), active_sessions: activeSessions.size });
+ }
+ case "sync_memory_store": {
+ const apiKey = process.env.ANTHROPIC_API_KEY;
+ if (!apiKey) throw new Error("ANTHROPIC_API_KEY is not set. Set it to use remote sync.");
+ const a = SyncMemoryStoreSchema.parse(args);
+ const remotes = loadRemoteConfigs();
+ const config = remotes.find((r) => r.label === a.label);
+ if (!config) throw new Error(`Remote '${a.label}' not found. Run \`kndl remote add\` first.`);
+ const client = new AnthropicMemoryClient(apiKey);
+ const result = await pull(client, store, config);
+ // Persist updated watermark
+ const idx = remotes.findIndex((r) => r.label === a.label);
+ if (idx >= 0) remotes[idx] = config;
+ saveRemoteConfigs(remotes);
+ return ok(result);
+ }
+ case "list_memory_stores": {
+ const remotes = loadRemoteConfigs();
+ return ok({ count: remotes.length, remotes: remotes.map((r) => ({
+ label: r.label, provider: r.provider, store_id: r.store_id,
+ last_synced_at: r.last_synced_at ?? null,
+ }))});
+ }
+ default:
+ throw new Error(`unknown tool: ${name}`);
+ }
+ } catch (e) {
+ return err(e);
+ }
+});
+
+// ── Resources ─────────────────────────────────────────────────────────────────
+
+server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
+ resourceTemplates: [
+ {
+ uriTemplate: "kndl://fact/{id}",
+ name: "Live fact snapshot",
+ description: "Current state of a fact — fields + effective_confidence. Re-read on notifications/resources/updated.",
+ mimeType: "application/json",
+ },
+ ],
+}));
+
+server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
+ const uri = req.params.uri;
+ if (uri.startsWith("kndl://fact/")) {
+ const id = decodeURIComponent(uri.slice("kndl://fact/".length));
+ const fact = await store.show(id);
+ if (!fact) {
+ return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ error: "not found", id }) }] };
+ }
+ const result = await store.query({ subject: fact.subject });
+ const live = result.facts.find((f) => f["@id"] === id);
+ return {
+ contents: [{
+ uri,
+ mimeType: "application/json",
+ text: JSON.stringify(live ?? fact, null, 2),
+ }],
+ };
+ }
+ throw new Error(`unknown resource: ${uri}`);
+});
+
+// ── Entry point ───────────────────────────────────────────────────────────────
+
+const isHttp = process.argv.includes("--http");
+const PORT = parseInt(process.env.PORT ?? "8000", 10);
+
+if (isHttp) {
+ const express = (await import("express")).default;
+ const { StreamableHTTPServerTransport } = await import(
+ "@modelcontextprotocol/sdk/server/streamableHttp.js"
+ );
+
+ const app = express();
+ app.use(express.json());
+
+ const transports = new Map>();
+
+ app.all("/mcp", async (req: import("express").Request, res: import("express").Response) => {
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
+
+ if (req.method === "POST" && !sessionId) {
+ // Use let + definite assignment so the closure can capture `transport`
+ // without TypeScript treating it as a circular initializer.
+ let transport!: InstanceType;
+ transport = new StreamableHTTPServerTransport({
+ sessionIdGenerator: () => randomUUID(),
+ onsessioninitialized: (id: string) => { transports.set(id, transport); },
+ });
+ transport.onclose = () => {
+ if (transport.sessionId) transports.delete(transport.sessionId);
+ activeSessions.delete(server);
+ };
+ await server.connect(transport);
+ activeSessions.add(server);
+ await transport.handleRequest(req, res, req.body);
+ } else if (sessionId && transports.has(sessionId)) {
+ await transports.get(sessionId)!.handleRequest(req, res, req.body);
+ } else {
+ res.status(400).json({ error: "bad session" });
+ }
+ });
+
+ app.listen(PORT, () => {
+ process.stderr.write(`[kndl-memory] HTTP MCP server on http://localhost:${PORT}/mcp\n`);
+ });
+} else {
+ const transport = new StdioServerTransport();
+ await server.connect(transport);
+ activeSessions.add(server);
+ process.stderr.write(`[kndl-memory] stdio MCP server ready\n`);
+}
diff --git a/packages/kndl-memory/src/stores/duckdb.ts b/packages/kndl-memory/src/stores/duckdb.ts
new file mode 100644
index 0000000..a72fb58
--- /dev/null
+++ b/packages/kndl-memory/src/stores/duckdb.ts
@@ -0,0 +1,120 @@
+// stores/duckdb.ts — DuckDbFactStore: columnar store for analytical workloads.
+//
+// Requires: @duckdb/node-api (npm install @duckdb/node-api)
+// Fast for AVG(effective_confidence) GROUP BY predicate, bulk imports, etc.
+
+import { resolve } from "node:path";
+import type { Fact, FactInput, FactStore, QueryOptions, QueryResult,
+ ContradictionsResult, ProvenanceResult, AssertResult, SupersedeResult } from "../types.js";
+import {
+ buildFact, applyQuery, findContradictions, buildProvenanceChain,
+} from "../core.js";
+
+const CONTEXT_URL = "https://kndl.artdaw.com/context/v1.jsonld";
+
+const DDL = `
+CREATE TABLE IF NOT EXISTS facts (
+ id VARCHAR PRIMARY KEY,
+ fact_json VARCHAR NOT NULL,
+ subject VARCHAR,
+ predicate VARCHAR,
+ confidence DOUBLE NOT NULL,
+ recorded_at VARCHAR NOT NULL,
+ supersedes VARCHAR,
+ tenant VARCHAR,
+ classification VARCHAR
+);
+`;
+
+export class DuckDbFactStore implements FactStore {
+ private conn!: import("@duckdb/node-api").DuckDBConnection;
+ private ready: Promise;
+
+ constructor(dbPath: string) {
+ this.ready = this._init(dbPath === ":memory:" ? ":memory:" : resolve(dbPath));
+ }
+
+ private async _init(path: string): Promise {
+ let mod: typeof import("@duckdb/node-api");
+ try {
+ mod = await import("@duckdb/node-api");
+ } catch {
+ throw new Error("DuckDB not installed: npm install @duckdb/node-api");
+ }
+ const instance = await mod.DuckDBInstance.create(path);
+ this.conn = await instance.connect();
+ await this.conn.run(DDL);
+ }
+
+ private async loadAll(): Promise {
+ await this.ready;
+ const result = await this.conn.run("SELECT fact_json FROM facts");
+ const rows = await result.getRows();
+ return rows.map((r) => JSON.parse(r[0] as string) as Fact);
+ }
+
+ async assertFact(input: FactInput, supersedesId?: string): Promise {
+ await this.ready;
+ const fact = buildFact(input, CONTEXT_URL, supersedesId);
+ const stmt = await this.conn.prepare(
+ `INSERT INTO facts (id, fact_json, subject, predicate, confidence,
+ recorded_at, supersedes, tenant, classification)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ );
+ await stmt.run(
+ fact["@id"],
+ JSON.stringify(fact),
+ fact.subject ?? null,
+ fact.predicate ?? null,
+ fact.confidence,
+ fact.recordedAt,
+ fact.supersedes ?? null,
+ fact.tenant ?? null,
+ fact.classification ?? null,
+ );
+ return { id: fact["@id"], fact };
+ }
+
+ async supersedeFact(oldId: string, input: FactInput): Promise {
+ const { id, fact } = await this.assertFact(input, oldId);
+ return { id, fact, supersedes: oldId };
+ }
+
+ async query(opts?: QueryOptions): Promise {
+ return applyQuery(await this.loadAll(), opts ?? {});
+ }
+
+ async contradictions(opts?: { subject?: string; predicate?: string }): Promise {
+ return findContradictions(await this.loadAll(), opts ?? {});
+ }
+
+ async provenanceChain(rootId: string, maxDepth?: number): Promise {
+ const facts = await this.loadAll();
+ const byId = new Map(facts.map((f) => [f["@id"], f]));
+ return buildProvenanceChain(byId, rootId, maxDepth);
+ }
+
+ async list(subject?: string): Promise {
+ await this.ready;
+ const sql = subject ? "SELECT id FROM facts WHERE subject = ?" : "SELECT id FROM facts";
+ const result = subject
+ ? await (await this.conn.prepare(sql)).run(subject)
+ : await this.conn.run(sql);
+ const rows = await result.getRows();
+ return rows.map((r) => r[0] as string);
+ }
+
+ async show(id: string): Promise {
+ await this.ready;
+ const stmt = await this.conn.prepare("SELECT fact_json FROM facts WHERE id = ?");
+ const result = await stmt.run(id);
+ const rows = await result.getRows();
+ return rows.length ? JSON.parse(rows[0][0] as string) as Fact : null;
+ }
+
+ async close(): Promise {
+ await this.ready;
+ // DuckDB Node API connection cleanup
+ this.conn.close?.();
+ }
+}
diff --git a/packages/kndl-memory/src/stores/fs.ts b/packages/kndl-memory/src/stores/fs.ts
new file mode 100644
index 0000000..67049ef
--- /dev/null
+++ b/packages/kndl-memory/src/stores/fs.ts
@@ -0,0 +1,81 @@
+// stores/fs.ts — FsFactStore: one JSON-LD file per fact.
+//
+// Storage: {memoryDir}/facts/*.fact.json
+// Source of truth when running inside Anthropic Memory (filesystem mount).
+// Synchronous FS calls wrapped in Promises so it satisfies the async FactStore interface.
+
+import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
+import { join, resolve } from "node:path";
+import type { Fact, FactInput, FactStore, QueryOptions, QueryResult,
+ ContradictionsResult, ProvenanceResult, AssertResult, SupersedeResult } from "../types.js";
+import {
+ buildFact, factFilename, applyQuery, findContradictions, buildProvenanceChain,
+ supersededIds,
+} from "../core.js";
+
+const CONTEXT_REL = "../context/v1.jsonld";
+
+export class FsFactStore implements FactStore {
+ readonly memoryDir: string;
+ readonly factsDir: string;
+
+ constructor(memoryDir: string) {
+ this.memoryDir = resolve(memoryDir);
+ this.factsDir = join(this.memoryDir, "facts");
+ }
+
+ private ensureDirs(): void {
+ mkdirSync(this.factsDir, { recursive: true });
+ }
+
+ private loadAll(): Fact[] {
+ if (!existsSync(this.factsDir)) return [];
+ const out: Fact[] = [];
+ for (const f of readdirSync(this.factsDir).filter((f) => f.endsWith(".fact.json"))) {
+ try {
+ out.push(JSON.parse(readFileSync(join(this.factsDir, f), "utf8")));
+ } catch (e) {
+ process.stderr.write(`warning: skipping ${f}: ${(e as Error).message}\n`);
+ }
+ }
+ return out;
+ }
+
+ async assertFact(input: FactInput, supersedesId?: string): Promise {
+ this.ensureDirs();
+ const fact = buildFact(input, CONTEXT_REL, supersedesId);
+ const path = join(this.factsDir, factFilename(fact["@id"]));
+ if (existsSync(path)) throw new Error(`refusing to overwrite ${path}; facts are immutable`);
+ writeFileSync(path, JSON.stringify(fact, null, 2));
+ return { id: fact["@id"], fact };
+ }
+
+ async supersedeFact(oldId: string, input: FactInput): Promise {
+ const { id, fact } = await this.assertFact(input, oldId);
+ return { id, fact, supersedes: oldId };
+ }
+
+ async query(opts?: QueryOptions): Promise {
+ return applyQuery(this.loadAll(), opts ?? {});
+ }
+
+ async contradictions(opts?: { subject?: string; predicate?: string }): Promise {
+ return findContradictions(this.loadAll(), opts ?? {});
+ }
+
+ async provenanceChain(rootId: string, maxDepth?: number): Promise {
+ const facts = this.loadAll();
+ const byId = new Map(facts.map((f) => [f["@id"], f]));
+ return buildProvenanceChain(byId, rootId, maxDepth);
+ }
+
+ async list(subject?: string): Promise {
+ const facts = this.loadAll();
+ return (subject ? facts.filter((f) => f.subject === subject) : facts)
+ .map((f) => f["@id"]);
+ }
+
+ async show(id: string): Promise {
+ return this.loadAll().find((f) => f["@id"] === id) ?? null;
+ }
+}
diff --git a/packages/kndl-memory/src/stores/index.ts b/packages/kndl-memory/src/stores/index.ts
new file mode 100644
index 0000000..226b126
--- /dev/null
+++ b/packages/kndl-memory/src/stores/index.ts
@@ -0,0 +1,56 @@
+// stores/index.ts — makeStore factory.
+//
+// Dispatches on KNDL_STORAGE env var (or explicit url argument).
+// Falls back to KNDL_MEMORY_DIR for backwards compatibility with v1.
+//
+// Formats:
+// fs:./memory filesystem (Anthropic Memory mount)
+// sqlite:./kndl.db SQLite, single-file persistent (DEFAULT)
+// sqlite::memory: SQLite in-memory (tests)
+// duckdb:./kndl.duckdb DuckDB, analytical workloads
+// supabase:?key= Supabase, multi-tenant cloud
+
+// FsFactStore and SqliteFactStore are statically imported so tsup bundles them
+// into the main chunk — avoids ERR_MODULE_NOT_FOUND from hashed chunk filenames.
+// DuckDb and Supabase remain lazily required (optional packages; fail gracefully
+// when the npm package is not installed).
+import { createRequire } from "node:module";
+import { FsFactStore } from "./fs.js";
+import { SqliteFactStore } from "./sqlite.js";
+import type { FactStore } from "../types.js";
+
+const _require = createRequire(import.meta.url);
+
+export function makeStore(url?: string): FactStore {
+ // Resolve the storage URL: explicit arg > env var > legacy KNDL_MEMORY_DIR > default
+ const raw = url
+ ?? process.env.KNDL_STORAGE
+ ?? (process.env.KNDL_MEMORY_DIR ? `fs:${process.env.KNDL_MEMORY_DIR}` : null)
+ ?? "fs:./memory";
+
+ if (raw.startsWith("fs:")) return new FsFactStore(raw.slice(3));
+ if (raw.startsWith("sqlite:")) return new SqliteFactStore(raw.slice(7));
+
+ if (raw.startsWith("duckdb:")) {
+ // Optional: requires `npm install @duckdb/node-api`
+ const { DuckDbFactStore } = _require("./duckdb.js") as typeof import("./duckdb.js");
+ return new DuckDbFactStore(raw.slice(7));
+ }
+
+ if (raw.startsWith("supabase:")) {
+ // Optional: requires `npm install @supabase/supabase-js`
+ const rest = raw.slice(9);
+ const qIdx = rest.lastIndexOf("?key=");
+ if (qIdx === -1) throw new Error("supabase: URL must include ?key=");
+ const supabaseUrl = rest.slice(0, qIdx);
+ const supabaseKey = rest.slice(qIdx + 5);
+ const { SupabaseFactStore } = _require("./supabase.js") as typeof import("./supabase.js");
+ return new SupabaseFactStore(supabaseUrl, supabaseKey);
+ }
+
+ throw new Error(`Unknown KNDL_STORAGE scheme: ${raw}\nSupported: fs:, sqlite:, duckdb:, supabase:`);
+}
+
+export type { FactStore } from "../types.js";
+export { FsFactStore } from "./fs.js";
+export { SqliteFactStore } from "./sqlite.js";
diff --git a/packages/kndl-memory/src/stores/sqlite.ts b/packages/kndl-memory/src/stores/sqlite.ts
new file mode 100644
index 0000000..d78db0e
--- /dev/null
+++ b/packages/kndl-memory/src/stores/sqlite.ts
@@ -0,0 +1,116 @@
+// stores/sqlite.ts — SqliteFactStore: single-file persistent store (DEFAULT).
+//
+// Schema: one row per fact. Key columns indexed for fast queries.
+// The full fact JSON is stored in fact_json for lossless round-trip.
+// WAL journal mode enabled for concurrent read access.
+//
+// Requires: better-sqlite3
+
+import { resolve } from "node:path";
+import { createRequire } from "node:module";
+
+const require = createRequire(import.meta.url);
+import type { Fact, FactInput, FactStore, QueryOptions, QueryResult,
+ ContradictionsResult, ProvenanceResult, AssertResult, SupersedeResult } from "../types.js";
+import {
+ buildFact, applyQuery, findContradictions, buildProvenanceChain,
+} from "../core.js";
+
+const CONTEXT_URL = "https://kndl.artdaw.com/context/v1.jsonld";
+
+const DDL = `
+CREATE TABLE IF NOT EXISTS facts (
+ id TEXT PRIMARY KEY,
+ fact_json TEXT NOT NULL,
+ subject TEXT,
+ predicate TEXT,
+ confidence REAL NOT NULL,
+ recorded_at TEXT NOT NULL,
+ supersedes TEXT,
+ tenant TEXT,
+ classification TEXT
+);
+CREATE INDEX IF NOT EXISTS idx_facts_subject ON facts(subject);
+CREATE INDEX IF NOT EXISTS idx_facts_predicate ON facts(predicate);
+CREATE INDEX IF NOT EXISTS idx_facts_confidence ON facts(confidence);
+CREATE INDEX IF NOT EXISTS idx_facts_recorded_at ON facts(recorded_at);
+CREATE INDEX IF NOT EXISTS idx_facts_supersedes ON facts(supersedes);
+CREATE INDEX IF NOT EXISTS idx_facts_tenant ON facts(tenant);
+`;
+
+export class SqliteFactStore implements FactStore {
+ private db: import("better-sqlite3").Database;
+
+ constructor(dbPath: string) {
+ const Database = require("better-sqlite3") as typeof import("better-sqlite3");
+ const path = dbPath === ":memory:" ? dbPath : resolve(dbPath);
+ this.db = new Database(path);
+ this.db.pragma("journal_mode = WAL");
+ this.db.pragma("busy_timeout = 5000");
+ this.db.pragma("foreign_keys = ON");
+ for (const stmt of DDL.trim().split(";").map((s) => s.trim()).filter(Boolean)) {
+ this.db.exec(stmt);
+ }
+ }
+
+ private loadAll(): Fact[] {
+ return (this.db.prepare("SELECT fact_json FROM facts").all() as { fact_json: string }[])
+ .map((r) => JSON.parse(r.fact_json) as Fact);
+ }
+
+ async assertFact(input: FactInput, supersedesId?: string): Promise {
+ const fact = buildFact(input, CONTEXT_URL, supersedesId);
+ this.db.prepare(
+ `INSERT INTO facts (id, fact_json, subject, predicate, confidence,
+ recorded_at, supersedes, tenant, classification)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ ).run(
+ fact["@id"],
+ JSON.stringify(fact),
+ fact.subject ?? null,
+ fact.predicate ?? null,
+ fact.confidence,
+ fact.recordedAt,
+ fact.supersedes ?? null,
+ fact.tenant ?? null,
+ fact.classification ?? null,
+ );
+ return { id: fact["@id"], fact };
+ }
+
+ async supersedeFact(oldId: string, input: FactInput): Promise {
+ const { id, fact } = await this.assertFact(input, oldId);
+ return { id, fact, supersedes: oldId };
+ }
+
+ async query(opts?: QueryOptions): Promise {
+ return applyQuery(this.loadAll(), opts ?? {});
+ }
+
+ async contradictions(opts?: { subject?: string; predicate?: string }): Promise {
+ return findContradictions(this.loadAll(), opts ?? {});
+ }
+
+ async provenanceChain(rootId: string, maxDepth?: number): Promise {
+ const facts = this.loadAll();
+ const byId = new Map(facts.map((f) => [f["@id"], f]));
+ return buildProvenanceChain(byId, rootId, maxDepth);
+ }
+
+ async list(subject?: string): Promise {
+ if (subject) {
+ return (this.db.prepare("SELECT id FROM facts WHERE subject = ?").all(subject) as { id: string }[])
+ .map((r) => r.id);
+ }
+ return (this.db.prepare("SELECT id FROM facts").all() as { id: string }[]).map((r) => r.id);
+ }
+
+ async show(id: string): Promise {
+ const row = this.db.prepare("SELECT fact_json FROM facts WHERE id = ?").get(id) as { fact_json: string } | undefined;
+ return row ? JSON.parse(row.fact_json) as Fact : null;
+ }
+
+ async close(): Promise {
+ this.db.close();
+ }
+}
diff --git a/packages/kndl-memory/src/stores/supabase.ts b/packages/kndl-memory/src/stores/supabase.ts
new file mode 100644
index 0000000..e2ddb80
--- /dev/null
+++ b/packages/kndl-memory/src/stores/supabase.ts
@@ -0,0 +1,117 @@
+// stores/supabase.ts — SupabaseFactStore: multi-tenant cloud store.
+//
+// Requires: @supabase/supabase-js (npm install @supabase/supabase-js)
+// RLS enforces tenant isolation. Realtime change feed available via Supabase.
+//
+// Prerequisites — run once in your Supabase project:
+//
+// CREATE TABLE facts (
+// id TEXT PRIMARY KEY,
+// fact_json JSONB NOT NULL,
+// subject TEXT,
+// predicate TEXT,
+// confidence FLOAT8 NOT NULL,
+// recorded_at TIMESTAMPTZ NOT NULL,
+// supersedes TEXT,
+// tenant TEXT,
+// classification TEXT
+// );
+// CREATE INDEX ON facts(subject);
+// CREATE INDEX ON facts(predicate);
+// CREATE INDEX ON facts(confidence);
+// CREATE INDEX ON facts(recorded_at);
+// CREATE INDEX ON facts(supersedes);
+// CREATE INDEX ON facts(tenant);
+// ALTER TABLE facts ENABLE ROW LEVEL SECURITY;
+// -- Add your own RLS policies for tenant isolation.
+
+import type { Fact, FactInput, FactStore, QueryOptions, QueryResult,
+ ContradictionsResult, ProvenanceResult, AssertResult, SupersedeResult } from "../types.js";
+import {
+ buildFact, applyQuery, findContradictions, buildProvenanceChain,
+} from "../core.js";
+
+const CONTEXT_URL = "https://kndl.artdaw.com/context/v1.jsonld";
+
+export class SupabaseFactStore implements FactStore {
+ private client!: import("@supabase/supabase-js").SupabaseClient;
+ private ready: Promise;
+
+ constructor(supabaseUrl: string, supabaseKey: string) {
+ this.ready = this._init(supabaseUrl, supabaseKey);
+ }
+
+ private async _init(url: string, key: string): Promise {
+ let mod: typeof import("@supabase/supabase-js");
+ try {
+ mod = await import("@supabase/supabase-js");
+ } catch {
+ throw new Error("Supabase not installed: npm install @supabase/supabase-js");
+ }
+ this.client = mod.createClient(url, key);
+ }
+
+ private async loadAll(): Promise {
+ await this.ready;
+ const { data, error } = await this.client.from("facts").select("fact_json");
+ if (error) throw new Error(`Supabase loadAll: ${error.message}`);
+ return (data ?? []).map((r: { fact_json: unknown }) => r.fact_json as Fact);
+ }
+
+ async assertFact(input: FactInput, supersedesId?: string): Promise {
+ await this.ready;
+ const fact = buildFact(input, CONTEXT_URL, supersedesId);
+ const { error } = await this.client.from("facts").insert({
+ id: fact["@id"],
+ fact_json: fact,
+ subject: fact.subject ?? null,
+ predicate: fact.predicate ?? null,
+ confidence: fact.confidence,
+ recorded_at: fact.recordedAt,
+ supersedes: fact.supersedes ?? null,
+ tenant: fact.tenant ?? null,
+ classification: fact.classification ?? null,
+ });
+ if (error) throw new Error(`Supabase assertFact: ${error.message}`);
+ return { id: fact["@id"], fact };
+ }
+
+ async supersedeFact(oldId: string, input: FactInput): Promise {
+ const { id, fact } = await this.assertFact(input, oldId);
+ return { id, fact, supersedes: oldId };
+ }
+
+ async query(opts?: QueryOptions): Promise {
+ return applyQuery(await this.loadAll(), opts ?? {});
+ }
+
+ async contradictions(opts?: { subject?: string; predicate?: string }): Promise {
+ return findContradictions(await this.loadAll(), opts ?? {});
+ }
+
+ async provenanceChain(rootId: string, maxDepth?: number): Promise {
+ const facts = await this.loadAll();
+ const byId = new Map(facts.map((f) => [f["@id"], f]));
+ return buildProvenanceChain(byId, rootId, maxDepth);
+ }
+
+ async list(subject?: string): Promise {
+ await this.ready;
+ let q = this.client.from("facts").select("id");
+ if (subject) q = q.eq("subject", subject);
+ const { data, error } = await q;
+ if (error) throw new Error(`Supabase list: ${error.message}`);
+ return (data ?? []).map((r: { id: string }) => r.id);
+ }
+
+ async show(id: string): Promise {
+ await this.ready;
+ const { data, error } = await this.client
+ .from("facts")
+ .select("fact_json")
+ .eq("id", id)
+ .maybeSingle();
+ if (error) throw new Error(`Supabase show: ${error.message}`);
+ return data ? (data.fact_json as Fact) : null;
+ }
+}
diff --git a/packages/kndl-memory/src/types.ts b/packages/kndl-memory/src/types.ts
new file mode 100644
index 0000000..1654102
--- /dev/null
+++ b/packages/kndl-memory/src/types.ts
@@ -0,0 +1,118 @@
+// types.ts — shared interfaces for the KNDL fact store.
+
+export interface Fact {
+ "@context"?: string;
+ "@id": string;
+ "@type": string;
+ statement: string;
+ subject?: string;
+ predicate?: string;
+ object?: unknown;
+ confidence: number;
+ decay?: string;
+ source: string;
+ validFrom: string;
+ validUntil?: string;
+ observedAt?: string;
+ recordedAt: string;
+ supersedes?: string;
+ derivedFrom?: string[];
+ inference?: string;
+ negated?: boolean;
+ classification?: string;
+ consent?: string;
+ retention?: string;
+ tenant?: string;
+ signature?: unknown;
+ weight?: number;
+ tags?: string[];
+}
+
+export interface FactInput {
+ statement: string;
+ confidence: number;
+ source: string;
+ subject?: string;
+ predicate?: string;
+ object?: unknown;
+ decay?: string;
+ validFrom?: string;
+ validUntil?: string;
+ observedAt?: string;
+ classification?: string;
+ consent?: string;
+ tenant?: string;
+ derivedFrom?: string[];
+ negated?: boolean;
+ tags?: string[];
+}
+
+export interface QueryOptions {
+ subject?: string;
+ predicate?: string;
+ asOf?: string;
+ minConfidence?: number;
+ tenant?: string;
+ allowPhi?: boolean;
+}
+
+export interface QueryResultFact extends Fact {
+ effective_confidence: number;
+}
+
+export interface QueryResult {
+ as_of: string;
+ count: number;
+ facts: QueryResultFact[];
+}
+
+export interface ContradictionEntry {
+ subject: string | undefined;
+ predicate: string | undefined;
+ preferred: { id: string; object: unknown; negated: boolean; effective_confidence: number };
+ conflicts_with: { id: string; object: unknown; negated: boolean; effective_confidence: number }[];
+}
+
+export interface ProvenanceNode {
+ id: string;
+ statement?: string;
+ source?: string;
+ confidence?: number;
+ recordedAt?: string;
+ derivedFrom?: string[];
+ supersedes?: string;
+ missing?: boolean;
+}
+
+export interface AssertResult {
+ id: string;
+ fact: Fact;
+}
+
+export interface SupersedeResult extends AssertResult {
+ supersedes: string;
+}
+
+export interface ContradictionsResult {
+ count: number;
+ conflicts: ContradictionEntry[];
+}
+
+export interface ProvenanceResult {
+ root: string;
+ depth: number;
+ chain: ProvenanceNode[];
+}
+
+// FactStore — the async interface every storage backend implements.
+export interface FactStore {
+ assertFact(input: FactInput, supersedesId?: string): Promise;
+ supersedeFact(oldId: string, input: FactInput): Promise;
+ query(opts?: QueryOptions): Promise;
+ contradictions(opts?: { subject?: string; predicate?: string }): Promise;
+ provenanceChain(rootId: string, maxDepth?: number): Promise;
+ list(subject?: string): Promise;
+ show(id: string): Promise;
+ // close() called on shutdown; optional so FS store (stateless) need not implement.
+ close?(): Promise;
+}
diff --git a/packages/kndl-memory/tests/remote.test.ts b/packages/kndl-memory/tests/remote.test.ts
new file mode 100644
index 0000000..611b499
--- /dev/null
+++ b/packages/kndl-memory/tests/remote.test.ts
@@ -0,0 +1,142 @@
+// tests/remote.test.ts — loopback tests for remote sync using FakeMemoryStoreClient.
+// No real API calls; no ANTHROPIC_API_KEY required.
+
+import { test, describe } from "node:test";
+import assert from "node:assert/strict";
+import { mkdtempSync, rmSync } from "node:fs";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+
+import { FakeMemoryStoreClient } from "../src/remote/anthropic.js";
+import { pull } from "../src/remote/sync.js";
+import { FsFactStore } from "../src/stores/fs.js";
+import { SqliteFactStore } from "../src/stores/sqlite.js";
+import type { RemoteConfig } from "../src/remote/types.js";
+
+function makeConfig(storeId = "store_test"): RemoteConfig {
+ return {
+ label: "test",
+ provider: "anthropic",
+ store_id: storeId,
+ default_confidence: 0.85,
+ push: false,
+ };
+}
+
+describe("remote sync — FsFactStore backend", async () => {
+ const dir = mkdtempSync(join(tmpdir(), "kndl-remote-fs-test-"));
+ const store = new FsFactStore(dir);
+ const cleanup = () => rmSync(dir, { recursive: true, force: true });
+
+ const client = new FakeMemoryStoreClient();
+ client.seed("store_test", [
+ { content: "Alice is a senior engineer on the payments team" },
+ { content: "The Q1 goal is to reduce p99 latency to under 200ms" },
+ { content: "PHI record: patient 9001 has type 2 diabetes", metadata: { classification: "PHI" } },
+ ]);
+
+ test("pull: writes all 3 items as facts", async () => {
+ const config = makeConfig();
+ const result = await pull(client, store, config);
+ assert.equal(result.pulled, 3, `expected 3 pulled, got ${result.pulled}`);
+ assert.equal(result.skipped, 0);
+ assert.equal(result.superseded, 0);
+ assert.equal(result.synced_at.endsWith("Z"), true);
+ });
+
+ test("pull: facts have correct source URI", async () => {
+ const facts = (await store.query()).facts;
+ assert.equal(facts.length, 3);
+ for (const f of facts) {
+ assert.ok(f.source.startsWith("claude-memory://store_test/fake_item_"), `unexpected source: ${f.source}`);
+ assert.ok(f.tags?.includes("from-anthropic-memory"), "missing from-anthropic-memory tag");
+ assert.ok(f.tags?.some((t) => t.startsWith("content-hash:")), "missing content-hash tag");
+ }
+ });
+
+ test("pull: idempotent — second pull skips all facts", async () => {
+ const config = makeConfig();
+ config.last_cursor = undefined; // reset to re-scan all items
+ const result = await pull(client, store, config);
+ assert.equal(result.pulled, 0, "should skip all already-pulled facts");
+ assert.equal(result.skipped, 3, "should report 3 skipped");
+ assert.equal(result.superseded, 0);
+ });
+
+ test("pull: supersedes when item content changes", async () => {
+ // Mutate the fake client item directly to simulate an update
+ const items = (await client.listItems("store_test")).items;
+ const first = items[0];
+ // Re-seed with changed content for first item
+ (client as unknown as { stores: Map> })
+ .stores.get("store_test")!
+ .set(first.id, { ...first, content: "Alice is now a STAFF engineer on the payments team", updated_at: new Date().toISOString() });
+
+ const config = makeConfig();
+ const result = await pull(client, store, config);
+ assert.equal(result.superseded, 1, "expected 1 superseded fact");
+ assert.equal(result.pulled, 0);
+
+ // Active query should show the new statement
+ const active = await store.query({ tenant: "test" });
+ const aliceFact = active.facts.find((f) => f.statement.includes("STAFF"));
+ assert.ok(aliceFact, "superseded fact should appear in active results");
+ });
+
+ test("push: throws not-implemented error in v2.0", async () => {
+ const { push } = await import("../src/remote/sync.js");
+ await assert.rejects(
+ () => push(client, store, makeConfig()),
+ /not implemented in v2\.0/,
+ );
+ });
+
+ // Cleanup after all tests in this suite
+ test("_cleanup", () => { cleanup(); });
+});
+
+describe("remote sync — SqliteFactStore backend", async () => {
+ const store = new SqliteFactStore(":memory:");
+ const client = new FakeMemoryStoreClient();
+ client.seed("store_sqlite", [
+ { content: "Customer 9281 credit score is 740" },
+ { content: "Customer 9281 is employed at ACME Corp", metadata: { subject: "customer:9281" } },
+ ]);
+
+ test("pull into SQLite: writes 2 facts", async () => {
+ const config = makeConfig("store_sqlite");
+ const result = await pull(client, store, config);
+ assert.equal(result.pulled, 2);
+ assert.equal(result.skipped, 0);
+ });
+
+ test("SQLite: facts queryable after pull", async () => {
+ const result = await store.query({ tenant: "test" });
+ assert.equal(result.count, 2);
+ for (const f of result.facts) {
+ assert.ok(f.confidence === 0.85, "default_confidence should be 0.85");
+ }
+ });
+
+ test("_cleanup", async () => { await store.close?.(); });
+});
+
+describe("FakeMemoryStoreClient", () => {
+ test("seed + listItems pagination", async () => {
+ const client = new FakeMemoryStoreClient();
+ client.seed("s1", Array.from({ length: 5 }, (_, i) => ({ content: `item ${i}` })));
+ const page1 = await client.listItems("s1", { limit: 3 });
+ assert.equal(page1.items.length, 3);
+ assert.equal(page1.has_more, true);
+ assert.ok(page1.next_cursor);
+ const page2 = await client.listItems("s1", { after: page1.next_cursor, limit: 3 });
+ assert.equal(page2.items.length, 2);
+ assert.equal(page2.has_more, false);
+ });
+
+ test("getItem returns null for unknown id", async () => {
+ const client = new FakeMemoryStoreClient();
+ const item = await client.getItem("s1", "nonexistent");
+ assert.equal(item, null);
+ });
+});
diff --git a/packages/kndl-memory/tests/stores.test.ts b/packages/kndl-memory/tests/stores.test.ts
new file mode 100644
index 0000000..2ce7d78
--- /dev/null
+++ b/packages/kndl-memory/tests/stores.test.ts
@@ -0,0 +1,239 @@
+// tests/stores.test.ts — backend conformance tests on the loan-decision corpus.
+//
+// Run: tsx --test tests/stores.test.ts
+//
+// Tests FsFactStore and SqliteFactStore with identical assertions.
+// DuckDbFactStore and SupabaseFactStore require optional deps; skipped when absent.
+
+import { test, describe, before, after } from "node:test";
+import assert from "node:assert/strict";
+import { mkdtempSync, rmSync, readFileSync, readdirSync } from "node:fs";
+import { join, resolve } from "node:path";
+import { tmpdir } from "node:os";
+
+import type { FactStore, FactInput } from "../src/types.js";
+import { FsFactStore } from "../src/stores/fs.js";
+import { SqliteFactStore } from "../src/stores/sqlite.js";
+import { effectiveConfidence, nowIso } from "../src/core.js";
+
+// ── Loan-decision corpus ──────────────────────────────────────────────────────
+
+const CORPUS_DIR = resolve(
+ new URL(".", import.meta.url).pathname,
+ "../../../skills/kndl-memory/examples/loan-decision",
+);
+
+interface CorpusFact {
+ "@id": string;
+ statement: string;
+ subject?: string;
+ predicate?: string;
+ object?: unknown;
+ confidence: number;
+ decay?: string;
+ source: string;
+ validFrom: string;
+ recordedAt: string;
+ supersedes?: string;
+}
+
+function loadCorpus(): CorpusFact[] {
+ const files = readdirSync(CORPUS_DIR).filter((f) => f.endsWith(".fact.json"));
+ return files.map((f) => JSON.parse(readFileSync(join(CORPUS_DIR, f), "utf8")));
+}
+
+// Convert corpus fact (already written) into FactInput for assertFact
+function toInput(f: CorpusFact): FactInput {
+ return {
+ statement: f.statement,
+ confidence: f.confidence,
+ source: f.source,
+ subject: f.subject,
+ predicate: f.predicate,
+ object: f.object,
+ decay: f.decay,
+ validFrom: f.validFrom,
+ };
+}
+
+// ── Shared conformance suite ──────────────────────────────────────────────────
+
+async function runConformance(store: FactStore, label: string): Promise {
+ const corpus = loadCorpus();
+
+ await test(`${label}: assertFact — writes all ${corpus.length} corpus facts`, async () => {
+ for (const f of corpus) {
+ const r = await store.assertFact(toInput(f));
+ assert.ok(r.id.startsWith("fact:"), `id should start with fact: — got ${r.id}`);
+ assert.equal(r.fact.confidence, f.confidence);
+ assert.equal(r.fact.source, f.source);
+ }
+ });
+
+ await test(`${label}: list — returns IDs for all written facts`, async () => {
+ const ids = await store.list();
+ assert.equal(ids.length, corpus.length, `expected ${corpus.length} facts`);
+ for (const id of ids) assert.ok(id.startsWith("fact:"));
+ });
+
+ await test(`${label}: list — filters by subject`, async () => {
+ const ids = await store.list("customer:9281");
+ assert.ok(ids.length >= 4, `expected ≥4 facts for customer:9281, got ${ids.length}`);
+ });
+
+ await test(`${label}: query — returns active facts with effective_confidence`, async () => {
+ const result = await store.query();
+ assert.ok(result.count > 0, "expected at least one active fact");
+ assert.equal(result.facts.length, result.count);
+ for (const f of result.facts) {
+ assert.ok(typeof f.effective_confidence === "number");
+ assert.ok(f.effective_confidence >= 0 && f.effective_confidence <= 1);
+ }
+ // Facts are sorted descending by effective_confidence
+ for (let i = 1; i < result.facts.length; i++) {
+ assert.ok(
+ result.facts[i - 1].effective_confidence >= result.facts[i].effective_confidence,
+ "facts should be sorted by effective_confidence desc",
+ );
+ }
+ });
+
+ await test(`${label}: query — filters by subject`, async () => {
+ const result = await store.query({ subject: "customer:9281" });
+ assert.ok(result.count >= 1);
+ for (const f of result.facts) assert.equal(f.subject, "customer:9281");
+ });
+
+ await test(`${label}: query — filters by predicate`, async () => {
+ const result = await store.query({ predicate: "creditScore" });
+ assert.ok(result.count >= 1);
+ for (const f of result.facts) assert.equal(f.predicate, "creditScore");
+ });
+
+ await test(`${label}: contradictions — detects conflicting creditScore facts`, async () => {
+ const result = await store.contradictions({ predicate: "creditScore" });
+ // The corpus has 3 creditScore facts with different objects (720, 680, etc.)
+ // — expect at least one contradiction group
+ assert.ok(result.count >= 1, `expected ≥1 contradiction, got ${result.count}`);
+ assert.ok(result.conflicts.length >= 1);
+ const group = result.conflicts[0];
+ assert.ok(group.preferred.effective_confidence >= 0);
+ assert.ok(group.conflicts_with.length >= 1);
+ });
+
+ await test(`${label}: show — returns a fact by ID`, async () => {
+ const ids = await store.list();
+ const f = await store.show(ids[0]);
+ assert.ok(f !== null, "expected a fact");
+ assert.equal(f!["@id"], ids[0]);
+ });
+
+ await test(`${label}: show — returns null for unknown ID`, async () => {
+ const f = await store.show("fact:does-not-exist");
+ assert.equal(f, null);
+ });
+
+ await test(`${label}: supersedeFact — hides old fact, shows new one in active query`, async () => {
+ // Write a fact then supersede it
+ const original = await store.assertFact({
+ statement: "Test supersession — original",
+ confidence: 0.8,
+ source: "test://supersede",
+ subject: "test:supersede",
+ predicate: "value",
+ object: "original",
+ });
+
+ const replacement = await store.supersedeFact(original.id, {
+ statement: "Test supersession — replacement",
+ confidence: 0.9,
+ source: "test://supersede",
+ subject: "test:supersede",
+ predicate: "value",
+ object: "replacement",
+ });
+
+ assert.equal(replacement.supersedes, original.id);
+
+ // Active query should NOT include the original
+ const active = await store.query({ subject: "test:supersede" });
+ const activeIds = active.facts.map((f) => f["@id"]);
+ assert.ok(!activeIds.includes(original.id), "original should be superseded (hidden)");
+ assert.ok(activeIds.includes(replacement.id), "replacement should be active");
+
+ // as_of before replacement should return original
+ const asOf = await store.query({
+ subject: "test:supersede",
+ asOf: "2020-01-01T00:00:00Z",
+ });
+ // recordedAt of both is ~now, so as_of 2020 returns nothing
+ assert.equal(asOf.count, 0, "no facts existed in 2020");
+ });
+
+ await test(`${label}: provenanceChain — walks supersession chain`, async () => {
+ // Find a fact that supersedes another
+ const ids = await store.list();
+ for (const id of ids) {
+ const f = await store.show(id);
+ if (f?.supersedes) {
+ const chain = await store.provenanceChain(id);
+ assert.ok(chain.chain.length >= 2, "should walk back to superseded fact");
+ assert.equal(chain.root, id);
+ const ids_in_chain = chain.chain.map((n) => n.id);
+ assert.ok(ids_in_chain.includes(f.supersedes!), "superseded fact should be in chain");
+ return;
+ }
+ }
+ // No supersession in the base corpus (we added one above) — skip silently
+ });
+}
+
+// ── Test runners ──────────────────────────────────────────────────────────────
+
+describe("FsFactStore", async () => {
+ const dir = mkdtempSync(join(tmpdir(), "kndl-fs-test-"));
+ const store = new FsFactStore(dir);
+ after(() => rmSync(dir, { recursive: true, force: true }));
+ await runConformance(store, "FsFactStore");
+});
+
+describe("SqliteFactStore", async () => {
+ const store = new SqliteFactStore(":memory:");
+ after(async () => store.close?.());
+ await runConformance(store, "SqliteFactStore");
+});
+
+// ── Decay math unit tests ─────────────────────────────────────────────────────
+
+describe("decay math", () => {
+ test("effectiveConfidence: no decay returns base confidence", () => {
+ const fact = {
+ "@id": "fact:x", "@type": "Fact",
+ statement: "x", confidence: 0.9, source: "test",
+ validFrom: "2026-01-01T00:00:00Z", recordedAt: "2026-01-01T00:00:00Z",
+ };
+ assert.equal(effectiveConfidence(fact, nowIso()), 0.9);
+ });
+
+ test("effectiveConfidence: 0.5/24h after 24h = 0.45", () => {
+ const validFrom = new Date(Date.now() - 24 * 3600 * 1000).toISOString();
+ const fact = {
+ "@id": "fact:x", "@type": "Fact",
+ statement: "x", confidence: 0.9, source: "test",
+ validFrom, recordedAt: validFrom,
+ decay: "0.5/24h",
+ };
+ const eff = effectiveConfidence(fact, nowIso());
+ assert.ok(Math.abs(eff - 0.45) < 0.01, `expected ~0.45, got ${eff}`);
+ });
+
+ test("effectiveConfidence: future validFrom returns base confidence", () => {
+ const fact = {
+ "@id": "fact:x", "@type": "Fact",
+ statement: "x", confidence: 0.8, source: "test",
+ validFrom: "2099-01-01T00:00:00Z", recordedAt: "2026-01-01T00:00:00Z",
+ decay: "0.5/1d",
+ };
+ assert.equal(effectiveConfidence(fact, nowIso()), 0.8);
+ });
+});
diff --git a/packages/kndl-memory/tsconfig.json b/packages/kndl-memory/tsconfig.json
new file mode 100644
index 0000000..1daa78c
--- /dev/null
+++ b/packages/kndl-memory/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "esModuleInterop": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "declaration": true,
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "include": ["src/**/*"]
+}
diff --git a/packages/mcp-server/.python-version b/packages/mcp-server/.python-version
deleted file mode 100644
index e4fba21..0000000
--- a/packages/mcp-server/.python-version
+++ /dev/null
@@ -1 +0,0 @@
-3.12
diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md
deleted file mode 100644
index 9b175cc..0000000
--- a/packages/mcp-server/README.md
+++ /dev/null
@@ -1,125 +0,0 @@
-# kndl-mcp
-
-MCP server that gives AI agents a persistent, confidence-aware knowledge graph.
-
-Connect it to Claude Desktop and your agent can remember facts, build relationship graphs, and reason over structured knowledge — all through natural conversation.
-
-**Version:** 1.0.0
-
-## Quickstart with Claude Desktop
-
-**1. Install**
-
-```bash
-pip install kndl-mcp
-# or with uv (recommended):
-uv add kndl-mcp
-```
-
-**2. Add to Claude Desktop config**
-
-File: `~/Library/Application Support/Claude/claude_desktop_config.json`
-
-```json
-{
- "mcpServers": {
- "kndl": {
- "command": "uvx",
- "args": ["kndl-mcp"]
- }
- }
-}
-```
-
-Or, if running from source:
-
-```json
-{
- "mcpServers": {
- "kndl": {
- "command": "uv",
- "args": [
- "run", "--project", "/absolute/path/to/kndl/packages/mcp-server",
- "python", "-m", "kndl_mcp"
- ]
- }
- }
-}
-```
-
-**3. Restart Claude Desktop** — you'll see a 🔌 icon when the server connects.
-
-**Try it:** Ask Claude to *"Remember that Alice is a senior engineer on the payments team with confidence 0.95."* It will call `kndl_add_node` and the fact persists in the graph.
-
-## Persistent storage
-
-By default the graph is in-memory and resets on restart. To keep it across sessions, add a `.env` file next to where you run the server:
-
-```bash
-DATABASE_URL=sqlite:///./kndl.db # local SQLite file
-DATABASE_URL=postgresql://user:pw@host/db # PostgreSQL
-```
-
-## Run standalone
-
-```bash
-# stdio — for Claude Desktop
-kndl-mcp
-python -m kndl_mcp
-
-# Streamable HTTP on port 8000 — for custom integrations
-python -m kndl_mcp --http
-```
-
-## Tools
-
-| Tool | Description |
-|------|-------------|
-| `kndl_add_node` | Add a typed node with fields, confidence, source, validity, decay, and extended meta (`recorded`, `observed`, `negated`, `deadline`, `classification`, `retention`, `uncertainty`) |
-| `kndl_get_node` | Fetch a node with all its edges |
-| `kndl_update_node` | Update fields or meta on an existing node |
-| `kndl_remove_node` | Delete a node and all connected edges |
-| `kndl_add_edge` | Add a typed edge between two nodes — `direction` controls `forward` / `reverse` / `undirected` |
-| `kndl_query_nodes` | Filter nodes by type, confidence threshold, or field values |
-| `kndl_neighborhood` | Get N-hop subgraph around a node (max 5 hops) |
-| `kndl_add_intent` | Register a trigger-action reactive rule |
-| `kndl_parse` | Parse a KNDL document (including `process` blocks) and merge it into the graph |
-| `kndl_merge_graphs` | Merge a second KNDL document (higher confidence wins on conflict) |
-| `kndl_serialize` | Export the full graph as KNDL text |
-| `kndl_graph_stats` | Node / edge / intent / process counts and type distribution |
-| `kndl_get_types` | List compiled type definitions in the graph |
-| `kndl_reset` | Clear the entire graph |
-
-## Resources
-
-| URI | Description |
-|-----|-------------|
-| `kndl://spec/version` | Current KNDL spec version |
-| `kndl://spec/grammar` | Full EBNF grammar |
-| `kndl://spec/language` | Full language specification |
-| `kndl://graph/types` | JSON snapshot of type declarations in the live graph |
-| `kndl://graph/summary` | Live node / edge / intent / process count summary |
-
-## Response format
-
-All tools return `{"status": "ok", ...}` on success or `{"status": "error", "message": "..."}` on failure.
-
-Node dicts use keys: `id`, `type`, `fields`, `meta`
-Edge dicts use keys: `id`, `source`, `target`, `type`, `direction`, `meta`
-Intent dicts use keys: `id`, `type`, `trigger` (`{kind, data}`), `actions`, `meta`
-Stats dict includes: `node_count`, `edge_count`, `intent_count`, `process_count`, `type_distribution`
-
-## Development
-
-```bash
-uv sync --all-extras
-uv run pytest tests/ -v # 80 integration tests
-uv run ruff check src tests
-uv run mypy src
-```
-
-Tests call tool functions directly, bypassing the MCP transport layer. Each test class resets the global graph via `kndl_reset()` through an `autouse` fixture.
-
-## License
-
-MIT
diff --git a/packages/mcp-server/pyproject.toml b/packages/mcp-server/pyproject.toml
deleted file mode 100644
index 6223cea..0000000
--- a/packages/mcp-server/pyproject.toml
+++ /dev/null
@@ -1,48 +0,0 @@
-[build-system]
-requires = ["hatchling"]
-build-backend = "hatchling.build"
-
-[project]
-name = "kndl-mcp"
-version = "1.0.0"
-description = "KNDL MCP Server — Model Context Protocol server for KNDL knowledge graphs"
-readme = "README.md"
-requires-python = ">=3.12"
-license = { text = "MIT" }
-dependencies = [
- "kndl",
- "mcp[cli]>=1.2.0",
- "python-dotenv>=1.0",
-]
-
-[tool.uv.sources]
-kndl = { path = "../python", editable = true }
-
-[project.optional-dependencies]
-postgres = [
- "psycopg2-binary>=2.9",
-]
-dev = [
- "pytest>=8.0",
- "pytest-asyncio>=0.23",
- "pytest-cov>=4.0",
- "ruff>=0.4",
- "mypy>=1.9",
- "python-dotenv>=1.0",
-]
-
-[project.scripts]
-kndl-mcp = "kndl_mcp.server:main"
-
-[tool.hatch.build.targets.wheel]
-packages = ["src/kndl_mcp"]
-
-[tool.mypy]
-python_version = "3.12"
-ignore_missing_imports = true
-
-[tool.pytest.ini_options]
-testpaths = ["tests"]
-
-[tool.ruff]
-line-length = 100
diff --git a/packages/mcp-server/src/kndl_mcp/__init__.py b/packages/mcp-server/src/kndl_mcp/__init__.py
deleted file mode 100644
index 645e13e..0000000
--- a/packages/mcp-server/src/kndl_mcp/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-"""KNDL MCP Server — Model Context Protocol server for KNDL knowledge graphs."""
-
-__version__ = "1.0.0"
diff --git a/packages/mcp-server/src/kndl_mcp/__main__.py b/packages/mcp-server/src/kndl_mcp/__main__.py
deleted file mode 100644
index 7dced84..0000000
--- a/packages/mcp-server/src/kndl_mcp/__main__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from kndl_mcp.server import main
-
-main()
diff --git a/packages/mcp-server/src/kndl_mcp/_meta.py b/packages/mcp-server/src/kndl_mcp/_meta.py
deleted file mode 100644
index a760c07..0000000
--- a/packages/mcp-server/src/kndl_mcp/_meta.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Shared utilities for the KNDL MCP server."""
-
-from __future__ import annotations
-
-import re
-
-_DURATION_RE = re.compile(r"^(\d+(?:\.\d+)?)(ns|us|mo|ms|s|m|h|d|w|y)$")
-_DURATION_MULT: dict[str, float] = {
- "ns": 1e-9, "us": 1e-6, "ms": 0.001,
- "s": 1.0, "m": 60.0, "h": 3600.0, "d": 86400.0, "w": 604800.0,
- "mo": 2592000.0, "y": 31536000.0,
-}
-
-
-def _duration_to_seconds(duration_str: str) -> float | None:
- m = _DURATION_RE.match(str(duration_str).strip())
- if not m:
- return None
- return float(m.group(1)) * _DURATION_MULT[m.group(2)]
diff --git a/packages/mcp-server/src/kndl_mcp/server.py b/packages/mcp-server/src/kndl_mcp/server.py
deleted file mode 100644
index 4927b02..0000000
--- a/packages/mcp-server/src/kndl_mcp/server.py
+++ /dev/null
@@ -1,555 +0,0 @@
-"""
-KNDL MCP Server — Model Context Protocol server for KNDL knowledge graphs.
-
-Exposes KNDL operations as MCP tools that AI agents can invoke.
-
-Run:
- python -m kndl_mcp # stdio transport (Claude Desktop)
- python -m kndl_mcp --http # streamable HTTP (port 8000)
-"""
-
-from __future__ import annotations
-
-import sys
-from pathlib import Path
-from typing import Any
-
-from mcp.server.fastmcp import FastMCP
-
-import kndl
-from kndl.graph import KNDLGraph, GraphNode, GraphEdge, GraphIntent, KNDLMeta
-from kndl.storage import create_storage
-
-from ._meta import _duration_to_seconds
-
-# Resolve spec files relative to this file's location inside the monorepo.
-# server.py → kndl_mcp/ → src/ → mcp-server/ → packages/ →
-_REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent.parent
-_SPEC_GRAMMAR = _REPO_ROOT / "spec" / "grammar" / "kndl.ebnf"
-_SPEC_FULL = _REPO_ROOT / "spec" / "SPECIFICATION.md"
-
-
-# ── Server setup ──────────────────────────────────────────────────────────────
-
-mcp = FastMCP(
- "kndl-server",
- instructions="""KNDL Knowledge Graph Server.
-
-Manages an in-memory KNDL (Knowledge Node Description Language) knowledge graph.
-
-Use it to:
-1. Parse KNDL source into a structured graph
-2. Add/update/remove nodes and edges with confidence scores
-3. Query nodes by type, confidence threshold, and field values
-4. Explore node neighborhoods (N-hop traversals)
-5. Serialize the graph back to KNDL text
-6. Add intents (reactive trigger-action rules)
-
-All nodes support meta-annotations: confidence (0.0–1.0), source URIs,
-temporal validity ranges, and confidence decay rates.
-""",
-)
-
-# Initialise storage once at import time (reads DATABASE_URL / .env).
-# Returns None when DATABASE_URL is unset → pure in-memory mode.
-_storage = create_storage()
-_graph = KNDLGraph.from_storage(_storage) if _storage is not None else KNDLGraph()
-
-
-def _get_graph() -> KNDLGraph:
- return _graph
-
-
-def _reset_graph() -> None:
- global _graph
- if _storage is not None:
- _storage.clear()
- _graph = KNDLGraph(storage=_storage)
- else:
- _graph = KNDLGraph()
-
-
-# ── Tools ─────────────────────────────────────────────────────────────────────
-
-@mcp.tool()
-def kndl_parse(source: str) -> dict[str, Any]:
- """
- Parse KNDL source text and merge it into the knowledge graph.
- Returns the resulting graph as JSON.
- """
- try:
- new_graph = kndl.compile(source)
- g = _get_graph()
- for node in new_graph.nodes.values():
- g.add_node(node)
- for edge in new_graph.edges.values():
- g.add_edge(edge)
- for intent in new_graph.intents.values():
- g.add_intent(intent)
- g.types.update(new_graph.types)
- g.processes.update(new_graph.processes)
- return {"status": "ok", "graph": g.to_dict()}
- except (kndl.ParseError, kndl.LexerError) as e:
- return {"status": "error", "message": str(e)}
-
-
-@mcp.tool()
-def kndl_add_node(
- node_id: str,
- type_name: str,
- fields: dict[str, Any] | None = None,
- confidence: float = 1.0,
- source: str = "",
- valid_start: str | None = None,
- valid_end: str | None = None,
- decay_rate: float | None = None,
- decay_duration: str | None = None,
- tags: list[str] | None = None,
- recorded: str | None = None,
- observed: str | None = None,
- negated: bool = False,
- deadline: str | None = None,
- classification: str | None = None,
- retention: str | None = None,
- uncertainty: dict[str, Any] | None = None,
-) -> dict[str, Any]:
- """
- Add a node to the knowledge graph.
-
- Args:
- node_id: Unique identifier (e.g. "sensor_t001")
- type_name: Node type (e.g. "Temperature")
- fields: Key-value data fields
- confidence: Certainty score 0.0–1.0
- source: URI of asserting entity (e.g. "agent://claude-sonnet-4.6")
- valid_start: Temporal validity start (ISO datetime)
- valid_end: Temporal validity end (ISO datetime or omit for open-ended)
- decay_rate: Confidence decay rate (e.g. 0.95)
- decay_duration: Duration per decay period (e.g. "1h", "30m", "1mo")
- tags: Free-form labels
- recorded: ISO datetime when fact was recorded
- observed: ISO datetime when fact was observed
- negated: Whether this fact is a negation
- deadline: ISO datetime deadline
- classification: Security classification label
- retention: Retention policy string
- uncertainty: Structured uncertainty model, e.g. {"_type": "gaussian", "mean": 0.5, "std": 0.1} (§9)
- """
- meta = KNDLMeta(
- confidence=confidence,
- source=source,
- valid_start=valid_start,
- valid_end=valid_end,
- decay_rate=decay_rate,
- decay_duration_seconds=_duration_to_seconds(decay_duration) if decay_duration else None,
- tags=tags or [],
- recorded=recorded,
- observed=observed,
- negated=negated,
- deadline=deadline,
- classification=classification,
- retention=retention,
- uncertainty=uncertainty,
- )
- node = GraphNode(id=node_id, type_name=type_name, fields=fields or {}, meta=meta)
- _get_graph().add_node(node)
- return {"status": "ok", "node": node.to_dict()}
-
-
-@mcp.tool()
-def kndl_add_edge(
- source_id: str,
- target_id: str,
- edge_type: str = "relates_to",
- direction: str = "forward",
- confidence: float = 1.0,
- source_uri: str = "",
- fields: dict[str, Any] | None = None,
-) -> dict[str, Any]:
- """
- Add an edge between two nodes.
-
- Args:
- source_id: ID of source node
- target_id: ID of target node
- edge_type: Semantic relationship (e.g. "located_in", "caused_by")
- direction: Edge direction — "forward" (-[T]->), "bidirectional" (<-[T]->),
- "reverse" (<-[T]-), or "undirected" (-[T]-)
- confidence: Certainty 0.0–1.0
- source_uri: URI of asserting entity
- fields: Additional data on the edge
- """
- meta = KNDLMeta(confidence=confidence, source=source_uri)
- edge = GraphEdge(
- source_id=source_id,
- target_id=target_id,
- edge_type=edge_type,
- direction=direction,
- fields=fields or {},
- meta=meta,
- )
- _get_graph().add_edge(edge)
- return {"status": "ok", "edge": edge.to_dict()}
-
-
-@mcp.tool()
-def kndl_query_nodes(
- type_name: str | None = None,
- min_confidence: float = 0.0,
- field_filters: dict[str, Any] | None = None,
- apply_decay: bool = True,
-) -> dict[str, Any]:
- """
- Query nodes by type, confidence, and field values.
-
- Args:
- type_name: Filter by node type (e.g. "Temperature")
- min_confidence: Minimum confidence threshold 0.0–1.0
- field_filters: Exact-match field filters (e.g. {"unit": "°C"})
- apply_decay: Apply confidence decay based on elapsed time
- """
- nodes = _get_graph().query_nodes(
- type_name=type_name,
- min_confidence=min_confidence,
- field_filters=field_filters,
- apply_decay=apply_decay,
- )
- return {"status": "ok", "count": len(nodes), "nodes": [n.to_dict() for n in nodes]}
-
-
-@mcp.tool()
-def kndl_get_node(node_id: str) -> dict[str, Any]:
- """
- Get a specific node and its connected edges.
-
- Args:
- node_id: The node's unique identifier
- """
- g = _get_graph()
- node = g.get_node(node_id)
- if not node:
- return {"status": "error", "message": f"Node '{node_id}' not found"}
- result = node.to_dict()
- result["outgoing_edges"] = [e.to_dict() for e in g.get_outgoing_edges(node_id)]
- result["incoming_edges"] = [e.to_dict() for e in g.get_incoming_edges(node_id)]
- result["effective_confidence"] = node.meta.effective_confidence()
- return {"status": "ok", "node": result}
-
-
-@mcp.tool()
-def kndl_update_node(
- node_id: str,
- fields: dict[str, Any] | None = None,
- confidence: float | None = None,
- source: str | None = None,
- valid_start: str | None = None,
- valid_end: str | None = None,
-) -> dict[str, Any]:
- """
- Update an existing node's fields and meta-annotations.
-
- Args:
- node_id: ID of node to update
- fields: Fields to merge (partial update)
- confidence: New confidence score
- source: New source URI
- valid_start: New validity start
- valid_end: New validity end
- """
- meta_updates: dict[str, Any] = {}
- if confidence is not None:
- meta_updates["confidence"] = confidence
- if source is not None:
- meta_updates["source"] = source
- if valid_start is not None:
- meta_updates["valid_start"] = valid_start
- if valid_end is not None:
- meta_updates["valid_end"] = valid_end
-
- node = _get_graph().update_node(node_id, fields=fields, meta_updates=meta_updates or None)
- if not node:
- return {"status": "error", "message": f"Node '{node_id}' not found"}
- return {"status": "ok", "node": node.to_dict()}
-
-
-@mcp.tool()
-def kndl_remove_node(node_id: str) -> dict[str, Any]:
- """Remove a node and all its connected edges from the graph."""
- if _get_graph().remove_node(node_id):
- return {"status": "ok", "message": f"Node '{node_id}' removed"}
- return {"status": "error", "message": f"Node '{node_id}' not found"}
-
-
-@mcp.tool()
-def kndl_neighborhood(node_id: str, hops: int = 1) -> dict[str, Any]:
- """
- Get the N-hop neighborhood around a node.
-
- Args:
- node_id: Center node ID
- hops: Number of hops to traverse (1–5)
- """
- g = _get_graph()
- if not g.get_node(node_id):
- return {"status": "error", "message": f"Node '{node_id}' not found"}
- return {"status": "ok", **g.query_neighborhood(node_id, hops=max(1, min(hops, 5)))}
-
-
-@mcp.tool()
-def kndl_serialize() -> dict[str, Any]:
- """Serialize the current knowledge graph to KNDL text format."""
- g = _get_graph()
- return {
- "status": "ok",
- "kndl_text": kndl.serialize(g),
- "stats": {
- "node_count": len(g.nodes),
- "edge_count": len(g.edges),
- "intent_count": len(g.intents),
- "type_count": len(g.types),
- "process_count": len(g.processes),
- },
- }
-
-
-@mcp.tool()
-def kndl_graph_stats() -> dict[str, Any]:
- """Get summary statistics about the current knowledge graph."""
- g = _get_graph()
- type_counts: dict[str, int] = {}
- confidences: list[float] = []
- for node in g.nodes.values():
- type_counts[node.type_name] = type_counts.get(node.type_name, 0) + 1
- confidences.append(node.meta.effective_confidence())
- avg = sum(confidences) / len(confidences) if confidences else 0.0
- return {
- "status": "ok",
- "stats": {
- "node_count": len(g.nodes),
- "edge_count": len(g.edges),
- "intent_count": len(g.intents),
- "type_count": len(g.types),
- "process_count": len(g.processes),
- "type_distribution": type_counts,
- "average_confidence": round(avg, 4),
- },
- }
-
-
-@mcp.tool()
-def kndl_add_intent(
- intent_id: str,
- type_name: str = "Action",
- trigger_kind: str = "expression",
- trigger_data: str = "",
- actions: list[dict[str, Any]] | None = None,
- priority: float = 0.5,
- cooldown: str | None = None,
-) -> dict[str, Any]:
- """
- Add a reactive intent (trigger-action rule) to the graph.
-
- Args:
- intent_id: Unique identifier
- type_name: Intent type (e.g. "Action", "ScheduledAction")
- trigger_kind: "expression", "query", or "cron"
- trigger_data: Trigger expression, query name, or cron string
- actions: List of action dicts with keys: type, node_type, fields
- priority: Execution priority 0.0–1.0
- cooldown: Cooldown duration (e.g. "15m", "1h")
- """
- meta = KNDLMeta(
- priority=priority,
- cooldown_seconds=_duration_to_seconds(cooldown) if cooldown else None,
- )
- intent = GraphIntent(
- id=intent_id,
- type_name=type_name,
- trigger_kind=trigger_kind,
- trigger_data=trigger_data,
- actions=actions or [],
- meta=meta,
- )
- _get_graph().add_intent(intent)
- return {"status": "ok", "intent": intent.to_dict()}
-
-
-@mcp.tool()
-def kndl_merge_graphs(source: str) -> dict[str, Any]:
- """
- Parse KNDL source text and merge it into the existing graph.
- For existing nodes: merges fields and takes higher confidence.
-
- Args:
- source: KNDL source text to parse and merge
- """
- try:
- new_graph = kndl.compile(source)
- g = _get_graph()
- merged = new_nodes = new_edges = 0
-
- for node in new_graph.nodes.values():
- existing = g.get_node(node.id)
- if existing:
- existing.fields.update(node.fields)
- if node.meta.confidence > existing.meta.confidence:
- existing.meta.confidence = node.meta.confidence
- if node.meta.source:
- existing.meta.source = node.meta.source
- existing.meta.derived_from.extend(node.meta.derived_from)
- merged += 1
- else:
- g.add_node(node)
- new_nodes += 1
-
- for edge in new_graph.edges.values():
- g.add_edge(edge)
- new_edges += 1
-
- for intent in new_graph.intents.values():
- g.add_intent(intent)
- g.types.update(new_graph.types)
- g.processes.update(new_graph.processes)
-
- return {
- "status": "ok",
- "merged_nodes": merged,
- "new_nodes": new_nodes,
- "new_edges": new_edges,
- "total_nodes": len(g.nodes),
- "total_edges": len(g.edges),
- }
- except (kndl.ParseError, kndl.LexerError) as e:
- return {"status": "error", "message": str(e)}
-
-
-@mcp.tool()
-def kndl_get_types(type_name: str | None = None) -> dict[str, Any]:
- """
- Return the compiled type schema declared in the current graph.
-
- Args:
- type_name: If provided, return only that type definition.
- If omitted, return all declared types.
-
- Each type entry contains:
- - name: type identifier
- - fields: mapping of field name → declared type (e.g. {"value": "Float"})
- - constraints: list of where-clause constraint strings (may be empty)
- """
- types = _get_graph().types
- if type_name is not None:
- if type_name not in types:
- return {"status": "error", "message": f"Type '{type_name}' not found"}
- return {"status": "ok", "type": types[type_name]}
- return {"status": "ok", "count": len(types), "types": types}
-
-
-@mcp.tool()
-def kndl_reset() -> dict[str, Any]:
- """Reset the knowledge graph to an empty state. Deletes all data."""
- _reset_graph()
- return {"status": "ok", "message": "Graph reset to empty state"}
-
-
-# ── Resources ─────────────────────────────────────────────────────────────────
-
-@mcp.resource("kndl://spec/version")
-def spec_version() -> str:
- return f"KNDL Specification v{kndl.__version__}"
-
-
-@mcp.resource("kndl://spec/grammar")
-def spec_grammar() -> str:
- """Full EBNF grammar for the KNDL language."""
- if _SPEC_GRAMMAR.exists():
- return _SPEC_GRAMMAR.read_text(encoding="utf-8")
- return "# EBNF grammar file not found (expected at spec/grammar/kndl.ebnf)"
-
-
-@mcp.resource("kndl://spec/language")
-def spec_language() -> str:
- """Full KNDL language specification (Markdown)."""
- if _SPEC_FULL.exists():
- return _SPEC_FULL.read_text(encoding="utf-8")
- return "# Specification file not found (expected at spec/SPECIFICATION.md)"
-
-
-@mcp.resource("kndl://graph/types")
-def graph_types() -> str:
- """Type schema declared in the current graph (JSON)."""
- import json
- g = _get_graph()
- return json.dumps(
- {"count": len(g.types), "types": g.types},
- indent=2,
- )
-
-
-@mcp.resource("kndl://graph/summary")
-def graph_summary() -> str:
- g = _get_graph()
- type_counts: dict[str, int] = {}
- for n in g.nodes.values():
- type_counts[n.type_name] = type_counts.get(n.type_name, 0) + 1
- lines = [
- "KNDL Knowledge Graph Summary",
- f" Nodes: {len(g.nodes)}",
- f" Edges: {len(g.edges)}",
- f" Intents: {len(g.intents)}",
- f" Types: {len(g.types)}",
- ]
- if type_counts:
- lines.append("\nNode types:")
- for t, c in sorted(type_counts.items()):
- lines.append(f" {t}: {c}")
- return "\n".join(lines)
-
-
-# ── Prompts ───────────────────────────────────────────────────────────────────
-
-@mcp.prompt()
-def create_knowledge_node(
- topic: str,
- confidence: str = "0.8",
- source: str = "agent://claude",
-) -> str:
- return f"""Create a KNDL node to represent knowledge about: {topic}
-
-Use this format:
-node @ :: {{
- =
- ~confidence {confidence}
- ~source "{source}"
- ~valid .. *
-}}
-
-Make the node ID descriptive, choose an appropriate type, include
-relevant fields, and set confidence based on how certain the information is."""
-
-
-@mcp.prompt()
-def analyze_graph() -> str:
- return """Analyze the current KNDL knowledge graph:
-
-1. Use kndl_graph_stats to get an overview
-2. Use kndl_query_nodes to find nodes with low confidence
-3. Identify nodes that might need updating (check valid dates)
-4. Look for disconnected nodes that should have edges
-5. Suggest intents that could automate actions based on graph state
-
-Provide a structured analysis with recommendations."""
-
-
-# ── Entry point ───────────────────────────────────────────────────────────────
-
-def main() -> None:
- from typing import Literal
- transport: Literal["stdio", "streamable-http"] = (
- "streamable-http" if "--http" in sys.argv else "stdio"
- )
- mcp.run(transport=transport)
-
-
-if __name__ == "__main__":
- main()
diff --git a/packages/mcp-server/tests/__init__.py b/packages/mcp-server/tests/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/packages/mcp-server/tests/test_tools.py b/packages/mcp-server/tests/test_tools.py
deleted file mode 100644
index 63af72f..0000000
--- a/packages/mcp-server/tests/test_tools.py
+++ /dev/null
@@ -1,627 +0,0 @@
-"""
-Integration tests for the KNDL MCP server tools.
-
-Tests call the tool functions directly (bypassing the MCP protocol layer)
-to verify that graph state is correctly maintained and that all 13 tools
-produce the expected JSON payloads.
-
-Each test class resets the in-memory graph via kndl_reset() to ensure
-isolation.
-"""
-
-import sys
-import os
-import pytest
-
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python", "src"))
-
-from kndl_mcp.server import (
- kndl_parse,
- kndl_add_node,
- kndl_add_edge,
- kndl_query_nodes,
- kndl_get_node,
- kndl_update_node,
- kndl_remove_node,
- kndl_neighborhood,
- kndl_serialize,
- kndl_graph_stats,
- kndl_add_intent,
- kndl_merge_graphs,
- kndl_reset,
- kndl_get_types,
- spec_grammar,
- spec_language,
- spec_version,
- graph_types,
-)
-
-
-@pytest.fixture(autouse=True)
-def clean_graph():
- """Reset the in-memory graph before each test."""
- kndl_reset()
- yield
- kndl_reset()
-
-
-# ── kndl_reset ────────────────────────────────────────────────────────────────
-
-class TestReset:
- def test_reset_returns_ok(self):
- result = kndl_reset()
- assert result["status"] == "ok"
-
- def test_reset_clears_nodes(self):
- kndl_add_node("n1", "Foo")
- kndl_reset()
- result = kndl_query_nodes()
- assert result["count"] == 0
-
- def test_reset_clears_edges(self):
- kndl_add_node("a", "Foo")
- kndl_add_node("b", "Bar")
- kndl_add_edge("a", "b")
- kndl_reset()
- stats = kndl_graph_stats()
- assert stats["stats"]["edge_count"] == 0
-
-
-# ── kndl_add_node ─────────────────────────────────────────────────────────────
-
-class TestAddNode:
- def test_add_simple_node(self):
- result = kndl_add_node("sensor_01", "Temperature")
- assert result["status"] == "ok"
- assert result["node"]["id"] == "sensor_01"
- assert result["node"]["type"] == "Temperature"
-
- def test_add_node_with_fields(self):
- result = kndl_add_node("s", "Sensor", fields={"value": 22.5, "unit": "°C"})
- assert result["status"] == "ok"
- assert result["node"]["fields"]["value"] == 22.5
- assert result["node"]["fields"]["unit"] == "°C"
-
- def test_add_node_with_confidence(self):
- result = kndl_add_node("s", "Sensor", confidence=0.87)
- assert result["node"]["meta"]["confidence"] == 0.87
-
- def test_add_node_with_source(self):
- result = kndl_add_node("s", "Sensor", source="agent://test")
- assert result["node"]["meta"]["source"] == "agent://test"
-
- def test_add_node_with_valid_range(self):
- result = kndl_add_node(
- "s", "Event",
- valid_start="2026-01-01T00:00Z",
- valid_end="2026-12-31T23:59Z",
- )
- assert result["node"]["meta"]["valid_start"] == "2026-01-01T00:00Z"
- assert result["node"]["meta"]["valid_end"] == "2026-12-31T23:59Z"
-
- def test_add_node_with_decay(self):
- result = kndl_add_node("s", "Sensor", decay_rate=0.95, decay_duration="1h")
- assert result["node"]["meta"]["decay_rate"] == 0.95
- assert result["node"]["meta"]["decay_duration_seconds"] == 3600.0
-
- def test_add_node_with_tags(self):
- result = kndl_add_node("s", "Sensor", tags=["iot", "outdoor"])
- assert "iot" in result["node"]["meta"]["tags"]
- assert "outdoor" in result["node"]["meta"]["tags"]
-
- def test_add_node_persists_in_graph(self):
- kndl_add_node("n1", "Widget")
- result = kndl_query_nodes(type_name="Widget")
- assert result["count"] == 1
-
-
-# ── kndl_add_edge ─────────────────────────────────────────────────────────────
-
-class TestAddEdge:
- def test_add_simple_edge(self):
- kndl_add_node("a", "T")
- kndl_add_node("b", "T")
- result = kndl_add_edge("a", "b")
- assert result["status"] == "ok"
- assert result["edge"]["source"] == "a"
- assert result["edge"]["target"] == "b"
-
- def test_add_edge_with_type(self):
- kndl_add_node("a", "T")
- kndl_add_node("b", "T")
- result = kndl_add_edge("a", "b", edge_type="located_in")
- assert result["edge"]["type"] == "located_in"
-
- def test_add_edge_with_confidence(self):
- kndl_add_node("a", "T")
- kndl_add_node("b", "T")
- result = kndl_add_edge("a", "b", confidence=0.75)
- assert result["edge"]["meta"]["confidence"] == 0.75
-
- def test_add_edge_with_source_uri(self):
- kndl_add_node("a", "T")
- kndl_add_node("b", "T")
- result = kndl_add_edge("a", "b", source_uri="agent://test")
- assert result["edge"]["meta"]["source"] == "agent://test"
-
- def test_edge_appears_in_stats(self):
- kndl_add_node("a", "T")
- kndl_add_node("b", "T")
- kndl_add_edge("a", "b")
- stats = kndl_graph_stats()
- assert stats["stats"]["edge_count"] == 1
-
-
-# ── kndl_parse ────────────────────────────────────────────────────────────────
-
-class TestParse:
- SENSOR_SRC = """
-node @sensor_01 :: Temperature {
- value = 22.5
- unit = "°C"
- location -> @berlin
- ~confidence 0.94
- ~source "sensor://t-001"
-}
-"""
-
- def test_parse_valid_kndl(self):
- result = kndl_parse(self.SENSOR_SRC)
- assert result["status"] == "ok"
- assert result["graph"]["summary"]["node_count"] >= 1
-
- def test_parse_creates_node(self):
- kndl_parse(self.SENSOR_SRC)
- result = kndl_query_nodes(type_name="Temperature")
- assert result["count"] == 1
- assert result["nodes"][0]["id"] == "sensor_01"
-
- def test_parse_invalid_kndl_returns_error(self):
- result = kndl_parse("!!! not valid kndl !!!")
- assert result["status"] == "error"
- assert "message" in result
-
- def test_parse_merges_into_existing_graph(self):
- kndl_add_node("existing", "Widget")
- kndl_parse(self.SENSOR_SRC)
- stats = kndl_graph_stats()
- assert stats["stats"]["node_count"] >= 2
-
- def test_parse_empty_source(self):
- result = kndl_parse("")
- assert result["status"] == "ok"
-
-
-# ── kndl_query_nodes ──────────────────────────────────────────────────────────
-
-class TestQueryNodes:
- def setup_method(self):
- kndl_reset()
- kndl_add_node("t1", "Temperature", fields={"value": 22.5}, confidence=0.9)
- kndl_add_node("t2", "Temperature", fields={"value": 35.0}, confidence=0.4)
- kndl_add_node("r1", "Room", fields={"name": "Lab"}, confidence=0.8)
-
- def test_query_all(self):
- result = kndl_query_nodes()
- assert result["count"] == 3
-
- def test_query_by_type(self):
- result = kndl_query_nodes(type_name="Temperature")
- assert result["count"] == 2
-
- def test_query_by_min_confidence(self):
- result = kndl_query_nodes(min_confidence=0.8)
- ids = [n["id"] for n in result["nodes"]]
- assert "t1" in ids
- assert "t2" not in ids
-
- def test_query_by_field_filter(self):
- result = kndl_query_nodes(field_filters={"name": "Lab"})
- assert result["count"] == 1
- assert result["nodes"][0]["id"] == "r1"
-
- def test_query_returns_ok(self):
- result = kndl_query_nodes()
- assert result["status"] == "ok"
-
-
-# ── kndl_get_node ─────────────────────────────────────────────────────────────
-
-class TestGetNode:
- def setup_method(self):
- kndl_reset()
- kndl_add_node("a", "T", confidence=0.9)
- kndl_add_node("b", "T", confidence=0.8)
- kndl_add_edge("a", "b", edge_type="links")
-
- def test_get_existing_node(self):
- result = kndl_get_node("a")
- assert result["status"] == "ok"
- assert result["node"]["id"] == "a"
-
- def test_get_node_includes_outgoing_edges(self):
- result = kndl_get_node("a")
- outgoing = result["node"]["outgoing_edges"]
- assert len(outgoing) == 1
- assert outgoing[0]["target"] == "b"
-
- def test_get_node_includes_incoming_edges(self):
- result = kndl_get_node("b")
- incoming = result["node"]["incoming_edges"]
- assert len(incoming) == 1
- assert incoming[0]["source"] == "a"
-
- def test_get_nonexistent_node_returns_error(self):
- result = kndl_get_node("does_not_exist")
- assert result["status"] == "error"
-
- def test_get_node_includes_effective_confidence(self):
- result = kndl_get_node("a")
- assert "effective_confidence" in result["node"]
-
-
-# ── kndl_update_node ──────────────────────────────────────────────────────────
-
-class TestUpdateNode:
- def setup_method(self):
- kndl_reset()
- kndl_add_node("n1", "Sensor", fields={"value": 10.0}, confidence=0.5)
-
- def test_update_fields(self):
- result = kndl_update_node("n1", fields={"value": 99.0})
- assert result["status"] == "ok"
- get = kndl_get_node("n1")
- assert get["node"]["fields"]["value"] == 99.0
-
- def test_update_confidence(self):
- kndl_update_node("n1", confidence=0.99)
- get = kndl_get_node("n1")
- assert get["node"]["meta"]["confidence"] == 0.99
-
- def test_update_source(self):
- kndl_update_node("n1", source="agent://updated")
- get = kndl_get_node("n1")
- assert get["node"]["meta"]["source"] == "agent://updated"
-
- def test_update_nonexistent_node_returns_error(self):
- result = kndl_update_node("ghost", fields={"x": 1})
- assert result["status"] == "error"
-
-
-# ── kndl_remove_node ──────────────────────────────────────────────────────────
-
-class TestRemoveNode:
- def setup_method(self):
- kndl_reset()
- kndl_add_node("a", "T")
- kndl_add_node("b", "T")
- kndl_add_edge("a", "b")
-
- def test_remove_existing_node(self):
- result = kndl_remove_node("a")
- assert result["status"] == "ok"
- assert kndl_get_node("a")["status"] == "error"
-
- def test_remove_node_cleans_edges(self):
- kndl_remove_node("a")
- stats = kndl_graph_stats()
- assert stats["stats"]["edge_count"] == 0
-
- def test_remove_nonexistent_node_returns_error(self):
- result = kndl_remove_node("ghost")
- assert result["status"] == "error"
-
-
-# ── kndl_neighborhood ─────────────────────────────────────────────────────────
-
-class TestNeighborhood:
- def setup_method(self):
- kndl_reset()
- kndl_add_node("a", "T")
- kndl_add_node("b", "T")
- kndl_add_node("c", "T")
- kndl_add_edge("a", "b")
- kndl_add_edge("b", "c")
-
- def test_one_hop(self):
- result = kndl_neighborhood("a", hops=1)
- assert result["status"] == "ok"
- ids = {n["id"] for n in result["nodes"]}
- assert "a" in ids
- assert "b" in ids
- assert "c" not in ids
-
- def test_two_hop(self):
- result = kndl_neighborhood("a", hops=2)
- ids = {n["id"] for n in result["nodes"]}
- assert "c" in ids
-
- def test_nonexistent_center_returns_error(self):
- result = kndl_neighborhood("ghost", hops=1)
- assert result["status"] == "error"
-
-
-# ── kndl_serialize ────────────────────────────────────────────────────────────
-
-class TestSerialize:
- def test_serialize_empty_graph(self):
- result = kndl_serialize()
- assert result["status"] == "ok"
- assert "kndl_text" in result
-
- def test_serialize_contains_nodes(self):
- kndl_add_node("n1", "Temperature", confidence=0.9)
- result = kndl_serialize()
- assert "n1" in result["kndl_text"]
- assert "Temperature" in result["kndl_text"]
-
- def test_serialize_stats_match_graph(self):
- kndl_add_node("a", "T")
- kndl_add_node("b", "T")
- kndl_add_edge("a", "b")
- result = kndl_serialize()
- assert result["stats"]["node_count"] == 2
- assert result["stats"]["edge_count"] == 1
-
-
-# ── kndl_graph_stats ──────────────────────────────────────────────────────────
-
-class TestGraphStats:
- def test_empty_graph_stats(self):
- result = kndl_graph_stats()
- assert result["status"] == "ok"
- assert result["stats"]["node_count"] == 0
- assert result["stats"]["edge_count"] == 0
- assert result["stats"]["average_confidence"] == 0.0
-
- def test_type_distribution(self):
- kndl_add_node("a", "Temp")
- kndl_add_node("b", "Temp")
- kndl_add_node("c", "Room")
- result = kndl_graph_stats()
- dist = result["stats"]["type_distribution"]
- assert dist["Temp"] == 2
- assert dist["Room"] == 1
-
- def test_average_confidence(self):
- kndl_add_node("a", "T", confidence=0.8)
- kndl_add_node("b", "T", confidence=0.6)
- result = kndl_graph_stats()
- assert abs(result["stats"]["average_confidence"] - 0.7) < 0.001
-
-
-# ── kndl_add_intent ───────────────────────────────────────────────────────────
-
-class TestAddIntent:
- def test_add_simple_intent(self):
- result = kndl_add_intent("alert_01", type_name="Action")
- assert result["status"] == "ok"
- assert result["intent"]["id"] == "alert_01"
-
- def test_add_intent_with_cooldown(self):
- result = kndl_add_intent("i1", cooldown="15m")
- assert result["intent"]["meta"]["cooldown_seconds"] == 900.0
-
- def test_add_intent_with_priority(self):
- result = kndl_add_intent("i1", priority=0.95)
- assert result["intent"]["meta"]["priority"] == 0.95
-
- def test_add_intent_with_cron_trigger(self):
- result = kndl_add_intent("i1", trigger_kind="cron", trigger_data="0 0 * * *")
- assert result["intent"]["trigger"]["kind"] == "cron"
- assert result["intent"]["trigger"]["data"] == "0 0 * * *"
-
- def test_intent_appears_in_stats(self):
- kndl_add_intent("i1")
- stats = kndl_graph_stats()
- assert stats["stats"]["intent_count"] == 1
-
-
-# ── kndl_merge_graphs ─────────────────────────────────────────────────────────
-
-class TestMergeGraphs:
- def test_merge_adds_new_nodes(self):
- result = kndl_merge_graphs("""
-node @alice :: Person { name = "Alice" ~confidence 0.9 }
-""")
- assert result["status"] == "ok"
- assert result["new_nodes"] == 1
-
- def test_merge_updates_existing_node(self):
- kndl_add_node("alice", "Person", fields={"name": "Alice"}, confidence=0.5)
- kndl_merge_graphs("""
-node @alice :: Person { name = "Alice Updated" ~confidence 0.9 }
-""")
- get = kndl_get_node("alice")
- assert get["node"]["meta"]["confidence"] == 0.9
-
- def test_merge_invalid_source_returns_error(self):
- result = kndl_merge_graphs("!!! invalid !!!")
- assert result["status"] == "error"
-
- def test_merge_accumulates_edges(self):
- kndl_add_node("a", "T")
- kndl_add_node("b", "T")
- kndl_merge_graphs("edge @a -[links]-> @b")
- stats = kndl_graph_stats()
- assert stats["stats"]["edge_count"] == 1
-
- def test_merge_preserves_existing_nodes(self):
- kndl_add_node("existing", "Widget")
- kndl_merge_graphs("node @new_node :: Gadget { val = 1 }")
- stats = kndl_graph_stats()
- assert stats["stats"]["node_count"] == 2
-
-
-# ── kndl_get_types ────────────────────────────────────────────────────────────
-
-class TestGetTypes:
- def test_empty_graph_returns_empty(self):
- result = kndl_get_types()
- assert result["status"] == "ok"
- assert result["count"] == 0
- assert result["types"] == {}
-
- def test_parse_type_decl_populates_types(self):
- kndl_parse("""
-type SmartRoom {
- temp : Float
- unit : String
-}
-""")
- result = kndl_get_types()
- assert result["status"] == "ok"
- assert result["count"] == 1
- assert "SmartRoom" in result["types"]
- t = result["types"]["SmartRoom"]
- assert t["fields"]["temp"] == "Float"
- assert t["fields"]["unit"] == "String"
-
- def test_get_single_type_by_name(self):
- kndl_parse("type Protocol = \"knx\" | \"bacnet\"")
- result = kndl_get_types(type_name="Protocol")
- assert result["status"] == "ok"
- assert result["type"]["name"] == "Protocol"
-
- def test_get_missing_type_returns_error(self):
- result = kndl_get_types(type_name="DoesNotExist")
- assert result["status"] == "error"
-
- def test_multiple_types(self):
- kndl_parse("""
-type Foo { x : Int }
-type Bar { y : String }
-""")
- result = kndl_get_types()
- assert result["count"] == 2
- assert "Foo" in result["types"]
- assert "Bar" in result["types"]
-
-
-# ── Schema resources ──────────────────────────────────────────────────────────
-
-class TestSchemaResources:
- def test_spec_version_contains_version(self):
- text = spec_version()
- assert "KNDL" in text
- assert "v" in text
-
- def test_spec_grammar_returns_ebnf(self):
- text = spec_grammar()
- assert "program" in text
- assert "node_decl" in text
-
- def test_spec_language_returns_markdown(self):
- text = spec_language()
- assert len(text) > 500
- assert "KNDL" in text
-
- def test_graph_types_resource_empty(self):
- import json
- text = graph_types()
- data = json.loads(text)
- assert data["count"] == 0
- assert data["types"] == {}
-
- def test_graph_types_resource_after_parse(self):
- import json
- kndl_parse("type Sensor { value : Float }")
- text = graph_types()
- data = json.loads(text)
- assert data["count"] == 1
- assert "Sensor" in data["types"]
-
-
-# ── Extended meta and uncertainty ─────────────────────────────────────────────
-
-class TestV02Features:
- def test_duration_mo_in_add_node(self):
- """CalDuration 'mo' works in decay_duration."""
- result = kndl_add_node("n", "T", decay_rate=0.9, decay_duration="1mo")
- assert result["status"] == "ok"
- assert result["node"]["meta"]["decay_duration_seconds"] == pytest.approx(2592000.0)
-
- def test_duration_y_in_add_node(self):
- """CalDuration 'y' works in decay_duration."""
- result = kndl_add_node("n", "T", decay_rate=0.5, decay_duration="1y")
- assert result["node"]["meta"]["decay_duration_seconds"] == pytest.approx(31536000.0)
-
- def test_duration_ns_in_add_node(self):
- """Duration 'ns' works in decay_duration."""
- result = kndl_add_node("n", "T", decay_rate=0.99, decay_duration="500ns")
- assert result["node"]["meta"]["decay_duration_seconds"] == pytest.approx(5e-7)
-
- def test_add_node_v02_meta_recorded(self):
- """recorded meta field is stored and returned."""
- result = kndl_add_node("n", "T", recorded="2026-04-23T10:00Z")
- assert result["status"] == "ok"
- assert result["node"]["meta"]["recorded"] == "2026-04-23T10:00Z"
-
- def test_add_node_v02_meta_negated(self):
- """negated meta field is stored and returned."""
- result = kndl_add_node("n", "T", negated=True)
- assert result["node"]["meta"]["negated"] is True
-
- def test_add_node_v02_meta_classification(self):
- """classification meta field is stored and returned."""
- result = kndl_add_node("n", "T", classification="confidential")
- assert result["node"]["meta"]["classification"] == "confidential"
-
- def test_add_node_v02_meta_uncertainty(self):
- """uncertainty meta field is stored and returned."""
- u = {"_type": "gaussian", "mean": 0.5, "std": 0.1}
- result = kndl_add_node("n", "T", uncertainty=u)
- assert result["node"]["meta"]["uncertainty"]["_type"] == "gaussian"
-
- def test_add_edge_undirected_direction(self):
- """kndl_add_edge supports direction='undirected'."""
- kndl_add_node("a", "T")
- kndl_add_node("b", "T")
- result = kndl_add_edge("a", "b", edge_type="peer", direction="undirected")
- assert result["status"] == "ok"
- assert result["edge"]["direction"] == "undirected"
-
- def test_add_edge_bidirectional_direction(self):
- """kndl_add_edge supports direction='bidirectional'."""
- kndl_add_node("a", "T")
- kndl_add_node("b", "T")
- result = kndl_add_edge("a", "b", direction="bidirectional")
- assert result["edge"]["direction"] == "bidirectional"
-
- def test_parse_process_propagates_to_graph(self):
- """kndl_parse imports process declarations into the graph."""
- result = kndl_parse("""
-process @order :: OrderProcess {
- state PENDING {}
- state DONE {}
- on complete in PENDING -> DONE
-}
-""")
- assert result["status"] == "ok"
- from kndl_mcp.server import _get_graph
- g = _get_graph()
- assert "order" in g.processes
-
- def test_merge_process_propagates(self):
- """kndl_merge_graphs imports process declarations."""
- result = kndl_merge_graphs("""
-process @flow :: MyFlow {
- state A {}
- state B {}
- on go in A -> B
-}
-""")
- assert result["status"] == "ok"
- from kndl_mcp.server import _get_graph
- assert "flow" in _get_graph().processes
-
- def test_graph_stats_includes_process_count(self):
- """kndl_graph_stats reports process_count."""
- stats = kndl_graph_stats()
- assert "process_count" in stats["stats"]
-
- def test_serialize_stats_includes_process_count(self):
- """kndl_serialize stats include process_count."""
- result = kndl_serialize()
- assert "process_count" in result["stats"]
diff --git a/packages/mcp-server/uv.lock b/packages/mcp-server/uv.lock
deleted file mode 100644
index 7a06f5b..0000000
--- a/packages/mcp-server/uv.lock
+++ /dev/null
@@ -1,1075 +0,0 @@
-version = 1
-revision = 3
-requires-python = ">=3.12"
-resolution-markers = [
- "python_full_version >= '3.15'",
- "python_full_version < '3.15'",
-]
-
-[[package]]
-name = "annotated-doc"
-version = "0.0.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
-]
-
-[[package]]
-name = "annotated-types"
-version = "0.7.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
-]
-
-[[package]]
-name = "anyio"
-version = "4.13.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "idna" },
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
-]
-
-[[package]]
-name = "attrs"
-version = "26.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
-]
-
-[[package]]
-name = "certifi"
-version = "2026.2.25"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
-]
-
-[[package]]
-name = "cffi"
-version = "2.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pycparser", marker = "implementation_name != 'PyPy'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
- { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
- { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
- { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
- { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
- { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
- { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
- { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
- { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
- { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
- { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
- { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
- { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
- { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
- { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
- { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
- { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
- { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
- { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
- { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
- { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
- { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
- { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
- { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
- { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
- { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
- { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
- { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
- { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
- { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
- { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
- { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
- { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
- { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
- { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
- { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
- { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
- { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
- { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
- { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
- { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
- { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
- { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
- { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
- { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
- { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
-]
-
-[[package]]
-name = "click"
-version = "8.3.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
-]
-
-[[package]]
-name = "colorama"
-version = "0.4.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
-]
-
-[[package]]
-name = "coverage"
-version = "7.13.5"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" },
- { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" },
- { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" },
- { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" },
- { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" },
- { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" },
- { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" },
- { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" },
- { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" },
- { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" },
- { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" },
- { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" },
- { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" },
- { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" },
- { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" },
- { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" },
- { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" },
- { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" },
- { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" },
- { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" },
- { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" },
- { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" },
- { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" },
- { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" },
- { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" },
- { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" },
- { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" },
- { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" },
- { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" },
- { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" },
- { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" },
- { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" },
- { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" },
- { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" },
- { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" },
- { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" },
- { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" },
- { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" },
- { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" },
- { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" },
- { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" },
- { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" },
- { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" },
- { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" },
- { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" },
- { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" },
- { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" },
- { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" },
- { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" },
- { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" },
- { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" },
- { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" },
- { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" },
- { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" },
- { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" },
- { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" },
- { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" },
- { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" },
- { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" },
- { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" },
- { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" },
- { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" },
- { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" },
- { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" },
- { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" },
- { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" },
- { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" },
- { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" },
- { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" },
- { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" },
- { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" },
- { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" },
- { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" },
- { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" },
- { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" },
- { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" },
-]
-
-[[package]]
-name = "cryptography"
-version = "46.0.7"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
- { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
- { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
- { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
- { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
- { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
- { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
- { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
- { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
- { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
- { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
- { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
- { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
- { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
- { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
- { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
- { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
- { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
- { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
- { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
- { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
- { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
- { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
- { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
- { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
- { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
- { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
- { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
- { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
- { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
- { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
- { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
- { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
- { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
- { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
- { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
- { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
- { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
- { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
- { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
- { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
- { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
-]
-
-[[package]]
-name = "h11"
-version = "0.16.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
-]
-
-[[package]]
-name = "httpcore"
-version = "1.0.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "h11" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
-]
-
-[[package]]
-name = "httpx"
-version = "0.28.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "certifi" },
- { name = "httpcore" },
- { name = "idna" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
-]
-
-[[package]]
-name = "httpx-sse"
-version = "0.4.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
-]
-
-[[package]]
-name = "idna"
-version = "3.11"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
-]
-
-[[package]]
-name = "iniconfig"
-version = "2.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
-]
-
-[[package]]
-name = "jsonschema"
-version = "4.26.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "attrs" },
- { name = "jsonschema-specifications" },
- { name = "referencing" },
- { name = "rpds-py" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
-]
-
-[[package]]
-name = "jsonschema-specifications"
-version = "2025.9.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "referencing" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
-]
-
-[[package]]
-name = "kndl"
-version = "0.1.0"
-source = { editable = "../python" }
-
-[package.metadata]
-requires-dist = [
- { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.9" },
- { name = "psycopg2-binary", marker = "extra == 'postgres'", specifier = ">=2.9" },
- { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
- { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" },
- { name = "python-dotenv", marker = "extra == 'dev'", specifier = ">=1.0" },
- { name = "python-dotenv", marker = "extra == 'dotenv'", specifier = ">=1.0" },
- { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4" },
-]
-provides-extras = ["postgres", "dotenv", "dev"]
-
-[[package]]
-name = "kndl-mcp"
-version = "0.2.0"
-source = { editable = "." }
-dependencies = [
- { name = "kndl" },
- { name = "mcp", extra = ["cli"] },
- { name = "python-dotenv" },
-]
-
-[package.optional-dependencies]
-dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "pytest-asyncio" },
- { name = "pytest-cov" },
- { name = "python-dotenv" },
- { name = "ruff" },
-]
-postgres = [
- { name = "psycopg2-binary" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "kndl", editable = "../python" },
- { name = "mcp", extras = ["cli"], specifier = ">=1.2.0" },
- { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.9" },
- { name = "psycopg2-binary", marker = "extra == 'postgres'", specifier = ">=2.9" },
- { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
- { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" },
- { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" },
- { name = "python-dotenv", specifier = ">=1.0" },
- { name = "python-dotenv", marker = "extra == 'dev'", specifier = ">=1.0" },
- { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4" },
-]
-provides-extras = ["postgres", "dev"]
-
-[[package]]
-name = "librt"
-version = "0.9.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" },
- { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" },
- { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" },
- { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" },
- { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" },
- { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" },
- { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" },
- { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" },
- { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" },
- { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" },
- { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" },
- { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" },
- { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" },
- { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" },
- { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" },
- { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" },
- { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" },
- { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" },
- { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" },
- { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" },
- { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" },
- { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" },
- { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" },
- { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" },
- { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" },
- { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" },
- { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" },
- { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" },
- { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" },
- { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" },
- { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" },
- { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" },
- { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" },
- { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" },
- { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" },
- { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" },
- { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" },
- { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" },
- { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" },
- { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" },
- { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" },
- { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" },
- { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" },
- { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" },
- { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" },
- { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" },
- { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" },
- { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" },
- { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" },
- { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" },
- { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" },
- { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" },
-]
-
-[[package]]
-name = "markdown-it-py"
-version = "4.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "mdurl" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
-]
-
-[[package]]
-name = "mcp"
-version = "1.27.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "httpx" },
- { name = "httpx-sse" },
- { name = "jsonschema" },
- { name = "pydantic" },
- { name = "pydantic-settings" },
- { name = "pyjwt", extra = ["crypto"] },
- { name = "python-multipart" },
- { name = "pywin32", marker = "sys_platform == 'win32'" },
- { name = "sse-starlette" },
- { name = "starlette" },
- { name = "typing-extensions" },
- { name = "typing-inspection" },
- { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" },
-]
-
-[package.optional-dependencies]
-cli = [
- { name = "python-dotenv" },
- { name = "typer" },
-]
-
-[[package]]
-name = "mdurl"
-version = "0.1.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
-]
-
-[[package]]
-name = "mypy"
-version = "1.20.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
- { name = "mypy-extensions" },
- { name = "pathspec" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" },
- { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" },
- { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" },
- { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" },
- { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" },
- { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" },
- { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" },
- { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" },
- { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" },
- { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" },
- { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" },
- { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" },
- { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" },
- { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" },
- { url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" },
- { url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" },
- { url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" },
- { url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" },
- { url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" },
- { url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" },
- { url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" },
- { url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" },
- { url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" },
- { url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" },
- { url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" },
- { url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" },
- { url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" },
- { url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" },
- { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" },
-]
-
-[[package]]
-name = "mypy-extensions"
-version = "1.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
-]
-
-[[package]]
-name = "packaging"
-version = "26.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
-]
-
-[[package]]
-name = "pathspec"
-version = "1.0.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
-]
-
-[[package]]
-name = "pluggy"
-version = "1.6.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
-]
-
-[[package]]
-name = "psycopg2-binary"
-version = "2.9.11"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" },
- { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" },
- { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" },
- { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" },
- { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" },
- { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" },
- { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" },
- { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" },
- { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" },
- { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" },
- { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" },
- { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
- { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" },
- { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
- { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
- { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
- { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
- { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
- { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
- { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
- { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
- { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
- { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
- { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
- { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
- { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
- { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
- { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
- { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
- { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
- { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
- { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
- { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
-]
-
-[[package]]
-name = "pycparser"
-version = "3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
-]
-
-[[package]]
-name = "pydantic"
-version = "2.12.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "annotated-types" },
- { name = "pydantic-core" },
- { name = "typing-extensions" },
- { name = "typing-inspection" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
-]
-
-[[package]]
-name = "pydantic-core"
-version = "2.41.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
- { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
- { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
- { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
- { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
- { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
- { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
- { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
- { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
- { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
- { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
- { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
- { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
- { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
- { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
- { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
- { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
- { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
- { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
- { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
- { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
- { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
- { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
- { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
- { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
- { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
- { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
- { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
- { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
- { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
- { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
- { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
- { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
- { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
- { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
- { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
- { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
- { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
- { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
- { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
- { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
- { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
- { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
- { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
- { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
- { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
- { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
- { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
- { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
- { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
- { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
- { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
- { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
- { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
- { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
- { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
- { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
- { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
- { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
- { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
-]
-
-[[package]]
-name = "pydantic-settings"
-version = "2.13.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pydantic" },
- { name = "python-dotenv" },
- { name = "typing-inspection" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" },
-]
-
-[[package]]
-name = "pygments"
-version = "2.20.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
-]
-
-[[package]]
-name = "pyjwt"
-version = "2.12.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
-]
-
-[package.optional-dependencies]
-crypto = [
- { name = "cryptography" },
-]
-
-[[package]]
-name = "pytest"
-version = "9.0.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "iniconfig" },
- { name = "packaging" },
- { name = "pluggy" },
- { name = "pygments" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
-]
-
-[[package]]
-name = "pytest-asyncio"
-version = "1.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pytest" },
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
-]
-
-[[package]]
-name = "pytest-cov"
-version = "7.1.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "coverage" },
- { name = "pluggy" },
- { name = "pytest" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
-]
-
-[[package]]
-name = "python-dotenv"
-version = "1.2.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
-]
-
-[[package]]
-name = "python-multipart"
-version = "0.0.26"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
-]
-
-[[package]]
-name = "pywin32"
-version = "311"
-source = { registry = "https://pypi.org/simple" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
- { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
- { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
- { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
- { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
- { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
- { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
- { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
- { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
-]
-
-[[package]]
-name = "referencing"
-version = "0.37.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "attrs" },
- { name = "rpds-py" },
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
-]
-
-[[package]]
-name = "rich"
-version = "14.3.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "markdown-it-py" },
- { name = "pygments" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" },
-]
-
-[[package]]
-name = "rpds-py"
-version = "0.30.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
- { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
- { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
- { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
- { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
- { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
- { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
- { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
- { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
- { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
- { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
- { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
- { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
- { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
- { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
- { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
- { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
- { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
- { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
- { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
- { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
- { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
- { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
- { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
- { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
- { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
- { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
- { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
- { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
- { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
- { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
- { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
- { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
- { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
- { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
- { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
- { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
- { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
- { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
- { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
- { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
- { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
- { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
- { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
- { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
- { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
- { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
- { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
- { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
- { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
- { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
- { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
- { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
- { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
- { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
- { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
- { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
- { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
- { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
- { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
- { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
- { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
- { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
- { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
- { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
- { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
- { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
- { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
- { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
- { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
- { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
- { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
- { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
-]
-
-[[package]]
-name = "ruff"
-version = "0.15.11"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" },
- { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" },
- { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" },
- { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" },
- { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" },
- { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" },
- { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" },
- { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" },
- { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" },
- { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" },
- { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" },
- { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" },
- { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" },
- { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" },
- { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" },
- { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" },
- { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" },
-]
-
-[[package]]
-name = "shellingham"
-version = "1.5.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
-]
-
-[[package]]
-name = "sse-starlette"
-version = "3.3.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "starlette" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" },
-]
-
-[[package]]
-name = "starlette"
-version = "1.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
-]
-
-[[package]]
-name = "typer"
-version = "0.24.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "annotated-doc" },
- { name = "click" },
- { name = "rich" },
- { name = "shellingham" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" },
-]
-
-[[package]]
-name = "typing-extensions"
-version = "4.15.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
-]
-
-[[package]]
-name = "typing-inspection"
-version = "0.4.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
-]
-
-[[package]]
-name = "uvicorn"
-version = "0.44.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "h11" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" },
-]
diff --git a/packages/python/.python-version b/packages/python/.python-version
deleted file mode 100644
index e4fba21..0000000
--- a/packages/python/.python-version
+++ /dev/null
@@ -1 +0,0 @@
-3.12
diff --git a/packages/python/README.md b/packages/python/README.md
deleted file mode 100644
index cc79c02..0000000
--- a/packages/python/README.md
+++ /dev/null
@@ -1,236 +0,0 @@
-# kndl
-
-Python library for **KNDL** — Knowledge Node Description Language.
-
-Build, query, and persist confidence-aware knowledge graphs in Python. The reference implementation of the KNDL specification.
-
-[](https://pypi.org/project/kndl/)
-[](https://pypi.org/project/kndl/)
-
-## Install
-
-```bash
-pip install kndl
-# or with uv:
-uv add kndl
-```
-
-## Quickstart
-
-```python
-import kndl
-
-graph = kndl.compile("""
-node @alice :: Person {
- name = "Alice"
- role = "Engineer"
- ~confidence 0.95
- ~source "agent://hr"
-}
-
-node @acme :: Company {
- name = "Acme Corp"
-}
-
-edge @alice -[works_at]-> @acme {
- ~confidence 1.0
-}
-""")
-
-# Query by type and confidence
-engineers = graph.query_nodes(type_name="Person", min_confidence=0.9)
-
-# Get neighbours
-subgraph = graph.query_neighborhood("alice", hops=2)
-
-# Round-trip to KNDL text
-print(kndl.serialize(graph))
-
-# Export to JSON
-import json
-print(json.dumps(graph.to_dict(), indent=2))
-```
-
-## Persistent storage
-
-Set `DATABASE_URL` in your environment or a `.env` file:
-
-```bash
-DATABASE_URL=sqlite:///./kndl.db # local file, zero extra deps
-DATABASE_URL=postgresql://user:pw@host/db # postgres (pip install 'kndl[postgres]')
-```
-
-```python
-from kndl.storage import create_storage
-from kndl.graph import KNDLGraph
-
-storage = create_storage() # reads DATABASE_URL from env / .env
-graph = KNDLGraph.from_storage(storage) if storage else KNDLGraph()
-
-# All mutations are now auto-persisted
-graph.add_node(...)
-graph.remove_node("alice")
-```
-
-## Confidence decay
-
-Facts can lose confidence over time automatically:
-
-```kndl
-node @reading :: Sensor {
- value = 22.5
- ~confidence 0.99
- ~valid 2026-01-01T00:00Z .. *
- ~decay 0.95 / 1h # drops 5% every hour
-}
-```
-
-```python
-node = graph.get_node("reading")
-print(node.meta.effective_confidence()) # current confidence after decay
-```
-
-## Features
-
-### Parameterised types
-
-Types accept type parameters, enabling domain-generic schemas that stay strongly typed at instantiation:
-
-```kndl
-type Observation where C <: Code {
- code : C
- value : Float
- subject : Patient
-}
-
-node @obs_001 :: Observation> {
- code = "8310-5"
- value = 38.2
- subject -> @patient_p001
- ~confidence 0.96
-}
-```
-
-### Processes and state machines
-
-```kndl
-process @grasp_sm :: StateMachine {
- states = ["idle", "approaching", "grasping", "lifting"]
- initial = "idle"
- @idle -> @approaching { trigger = "pickup_cmd" }
- @approaching -> @grasping { trigger = @joint.angle > 30 }
-}
-```
-
-### Uncertainty distributions
-
-```kndl
-node @temp :: Temperature<°C> {
- value = 22.5
- ~confidence 0.94
- ~uncertainty Gaussian { mean = 22.5 stddev = 0.3 }
-}
-```
-
-Supported distributions: `Gaussian`, `Interval`, `Categorical`, `Histogram`.
-
-### Multi-hop query patterns
-
-```kndl
-query supply_chain {
- match ?supplier -[supplies*2..4]-> ?product
- where ?supplier.~confidence > 0.8
- return { supplier: ?supplier, product: ?product }
-}
-```
-
-### Undirected edges
-
-```kndl
-edge @room_204 -[adjacent_to]- @room_205
-```
-
-## API reference
-
-### Top-level functions
-
-| Function | Returns | Description |
-|----------|---------|-------------|
-| `kndl.compile(source)` | `KNDLGraph` | Parse and compile KNDL source to a graph |
-| `kndl.parse(source)` | `Program` | Parse to AST only |
-| `kndl.serialize(graph)` | `str` | Export graph as KNDL text |
-| `kndl.tokenize(source)` | `list[Token]` | Tokenize source text |
-
-### KNDLGraph
-
-| Method | Description |
-|--------|-------------|
-| `add_node(node)` | Add a `GraphNode` |
-| `get_node(node_id)` | Fetch a node or `None` |
-| `update_node(node_id, fields, meta_updates)` | Partial update |
-| `remove_node(node_id)` | Remove node and all its edges |
-| `add_edge(edge)` | Add a `GraphEdge` |
-| `remove_edge(edge_id)` | Remove an edge |
-| `get_outgoing_edges(node_id, edge_type?)` | Edges leaving a node |
-| `get_incoming_edges(node_id, edge_type?)` | Edges entering a node |
-| `add_intent(intent)` | Register a reactive `GraphIntent` |
-| `remove_intent(intent_id)` | Remove an intent |
-| `query_nodes(type_name?, min_confidence?, field_filters?, apply_decay?)` | Filter nodes |
-| `query_neighborhood(node_id, hops?)` | N-hop subgraph as dict |
-| `to_dict()` | JSON-serialisable snapshot |
-| `from_dict(data)` | Reconstruct from dict (classmethod) |
-| `from_storage(storage)` | Warm graph from a storage backend (classmethod) |
-
-### KNDLMeta fields
-
-| Field | Type | Description |
-|-------|------|-------------|
-| `confidence` | `float` | Trust level 0.0–1.0 (default 1.0) |
-| `source` | `str` | Provenance URI |
-| `valid_start` / `valid_end` | `str \| None` | ISO datetime validity window |
-| `decay_rate` / `decay_duration_seconds` | `float \| None` | Exponential decay parameters |
-| `tags` | `list[str]` | Arbitrary labels |
-| `priority` | `float` | For intent scheduling (default 0.5) |
-| `cooldown_seconds` | `float \| None` | Minimum time between intent firings |
-| `supersedes` | `str \| None` | ID of fact this replaces |
-| `recorded` | `str \| None` | When this fact was recorded in the system |
-| `observed` | `str \| None` | When the event was actually observed |
-| `negated` | `bool` | Assert that this fact is false |
-| `deadline` | `str \| None` | Time by which action must complete |
-| `classification` | `str \| None` | Security classification label |
-| `retention` | `str \| None` | How long to retain this record |
-| `uncertainty` | `dict \| None` | Full probability distribution |
-
-`meta.effective_confidence(at_time?)` applies decay and returns the current value.
-
-## How it works
-
-```
-source text
- → Lexer (lexer.py) → list[Token]
- → Parser (parser.py) → Program (AST)
- → Compiler (compiler.py) → KNDLGraph
- → Serializer (serializer.py) → KNDL text
-```
-
-## Development
-
-```bash
-uv sync --all-extras
-uv run pytest -v # 245 tests
-uv run pytest --cov=src/kndl --cov-report=term-missing
-uv run ruff check src tests
-uv run mypy src
-```
-
-| Test file | Tests | What it covers |
-|-----------|-------|----------------|
-| `test_kndl.py` | 52 | Lexer, Parser, Compiler, Graph, Serializer |
-| `test_kndl_extended.py` | 65 | Edge cases, integration, roundtrip |
-| `test_storage.py` | 24 | SQLite, PostgreSQL, factory, persistence |
-| `test_processes.py` | 72 | Processes, decimal, group-by, reverse edges |
-| `test_advanced_types.py` | 32 | Parameterised types, multi-hop, undirected edges, uncertainty, goto |
-
-## License
-
-MIT
diff --git a/packages/python/pyproject.toml b/packages/python/pyproject.toml
deleted file mode 100644
index 3b84c66..0000000
--- a/packages/python/pyproject.toml
+++ /dev/null
@@ -1,50 +0,0 @@
-[build-system]
-requires = ["hatchling"]
-build-backend = "hatchling.build"
-
-[project]
-name = "kndl"
-version = "1.0.0"
-description = "KNDL — Knowledge Node Description Language for AI agents"
-readme = "README.md"
-requires-python = ">=3.12"
-license = { text = "MIT" }
-keywords = ["knowledge-graph", "ai", "agents", "language", "semantic"]
-classifiers = [
- "Development Status :: 3 - Alpha",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3.12",
- "License :: OSI Approved :: MIT License",
-]
-dependencies = []
-
-[project.optional-dependencies]
-postgres = [
- "psycopg2-binary>=2.9",
-]
-dotenv = [
- "python-dotenv>=1.0",
-]
-dev = [
- "pytest>=8.0",
- "pytest-cov>=4.0",
- "ruff>=0.4",
- "mypy>=1.9",
- "python-dotenv>=1.0",
-]
-
-[tool.hatch.build.targets.wheel]
-packages = ["src/kndl"]
-
-[tool.pytest.ini_options]
-testpaths = ["tests"]
-
-[tool.ruff]
-src = ["src"]
-line-length = 100
-
-[tool.mypy]
-python_version = "3.11"
-strict = true
-ignore_missing_imports = true
diff --git a/packages/python/src/kndl/__init__.py b/packages/python/src/kndl/__init__.py
deleted file mode 100644
index 9766572..0000000
--- a/packages/python/src/kndl/__init__.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""
-KNDL — Knowledge Node Description Language
-
-A semantic-first, confidence-aware, graph-structured language
-for AI agent knowledge representation.
-
-Usage:
- import kndl
-
- # Parse KNDL source
- program = kndl.parse(source_text)
-
- # Compile to runtime graph
- graph = kndl.compile(source_text)
-
- # Query nodes
- nodes = graph.query_nodes(type_name="Temperature", min_confidence=0.8)
-
- # Serialize back to KNDL text
- text = kndl.serialize(graph)
-
- # Convert to JSON-compatible dict
- data = graph.to_dict()
-"""
-
-__version__ = "1.0.0"
-
-from .lexer import Lexer, Token, LexerError
-from .parser import Parser, ParseError
-from .ast_nodes import Program
-from .graph import KNDLGraph, GraphNode, GraphEdge, GraphIntent, KNDLMeta
-from .compiler import Compiler
-from .serializer import Serializer
-
-__all__ = [
- # Public API
- "parse", "compile", "serialize", "tokenize",
- # Types
- "KNDLGraph", "GraphNode", "GraphEdge", "GraphIntent", "KNDLMeta",
- # Errors
- "LexerError", "ParseError",
- # AST (for advanced use)
- "Program",
-]
-
-
-def parse(source: str) -> Program:
- """Parse KNDL source text into an AST."""
- return Parser(source).parse()
-
-
-def compile(source: str) -> KNDLGraph: # noqa: A001
- """Parse and compile KNDL source text into a runtime graph."""
- return Compiler().compile(parse(source))
-
-
-def serialize(graph: KNDLGraph) -> str:
- """Serialize a KNDLGraph back to KNDL text format."""
- return Serializer().serialize(graph)
-
-
-def tokenize(source: str) -> list[Token]:
- """Tokenize KNDL source text into a list of tokens."""
- return list(Lexer(source).tokenize())
diff --git a/packages/python/src/kndl/ast_nodes.py b/packages/python/src/kndl/ast_nodes.py
deleted file mode 100644
index ca21be5..0000000
--- a/packages/python/src/kndl/ast_nodes.py
+++ /dev/null
@@ -1,341 +0,0 @@
-"""
-KNDL AST — Abstract Syntax Tree definitions.
-
-Defines the data structures produced by the parser.
-These represent the semantic structure of a KNDL program.
-"""
-
-from __future__ import annotations
-
-from dataclasses import dataclass, field
-from typing import Any, Optional
-
-
-# ── Base ──
-
-@dataclass
-class ASTNode:
- """Base class for all AST nodes."""
- line: int = 0
- col: int = 0
-
-
-# ── Expressions ──
-
-@dataclass
-class Literal(ASTNode):
- """A literal value: int, float, decimal, string, bool, null, duration, datetime."""
- value: Any = None
- kind: str = "string" # "int", "float", "decimal", "string", "bool", "null", "duration", "datetime"
-
-
-@dataclass
-class NodeRef(ASTNode):
- """A reference to a node: @name or @name.sub.path"""
- path: list[str] = field(default_factory=list)
-
- @property
- def name(self) -> str:
- return ".".join(self.path)
-
- @property
- def full_ref(self) -> str:
- return f"@{self.name}"
-
-
-@dataclass
-class VarBind(ASTNode):
- """A query variable binding: ?name"""
- name: str = ""
-
-
-@dataclass
-class FieldAccess(ASTNode):
- """Field access expression: expr.field"""
- target: Optional[ASTNode] = None
- field_name: str = ""
-
-
-@dataclass
-class IndexAccess(ASTNode):
- """Index access expression: expr[index]"""
- target: Optional[ASTNode] = None
- index: Optional[ASTNode] = None
-
-
-@dataclass
-class BinaryOp(ASTNode):
- """Binary operation: left op right"""
- left: Optional[ASTNode] = None
- op: str = ""
- right: Optional[ASTNode] = None
-
-
-@dataclass
-class UnaryOp(ASTNode):
- """Unary operation: op expr"""
- op: str = ""
- operand: Optional[ASTNode] = None
-
-
-@dataclass
-class FuncCall(ASTNode):
- """Function call: name(args)"""
- name: str = ""
- args: list[ASTNode] = field(default_factory=list)
-
-
-@dataclass
-class ArrayLiteral(ASTNode):
- """Array literal: [a, b, c]"""
- elements: list[ASTNode] = field(default_factory=list)
-
-
-@dataclass
-class MapLiteral(ASTNode):
- """Map literal: #{ k: v, ... } or { k: v, ... }"""
- pairs: list[tuple[ASTNode, ASTNode]] = field(default_factory=list)
-
-
-@dataclass
-class RangeExpr(ASTNode):
- """Range expression: start .. end"""
- start: Optional[ASTNode] = None
- end: Optional[ASTNode] = None
-
-
-@dataclass
-class DecayExpr(ASTNode):
- """Decay rate expression: rate / duration"""
- rate: Optional[ASTNode] = None
- duration: Optional[ASTNode] = None
-
-
-# ── Meta-Annotations ──
-
-@dataclass
-class MetaAnnotation(ASTNode):
- """A meta-annotation: ~key value"""
- key: str = ""
- value: Optional[ASTNode] = None # Can be Literal, RangeExpr, DecayExpr, etc.
-
-
-# ── Fields & Edges ──
-
-@dataclass
-class FieldAssignment(ASTNode):
- """Field assignment: name = value"""
- name: str = ""
- value: Optional[ASTNode] = None
-
-
-@dataclass
-class InlineEdge(ASTNode):
- """Inline edge within a node: field_name -> @target"""
- field_name: str = ""
- target: Optional[NodeRef] = None
-
-
-# ── Top-Level Declarations ──
-
-@dataclass
-class NodeDecl(ASTNode):
- """Node declaration."""
- ref: Optional[NodeRef] = None
- type_name: str = ""
- fields: list[FieldAssignment] = field(default_factory=list)
- edges: list[InlineEdge] = field(default_factory=list)
- meta: list[MetaAnnotation] = field(default_factory=list)
-
-
-@dataclass
-class EdgeDecl(ASTNode):
- """Edge declaration."""
- source: Optional[NodeRef] = None
- targets: list[NodeRef] = field(default_factory=list)
- edge_type: str = "relates_to"
- direction: str = "forward" # "forward", "bidirectional", "reverse"
- fields: list[FieldAssignment] = field(default_factory=list)
- meta: list[MetaAnnotation] = field(default_factory=list)
-
-
-@dataclass
-class FieldDecl(ASTNode):
- """Type field declaration: name : Type"""
- name: str = ""
- type_expr: Optional[TypeExpr] = None
-
-
-@dataclass
-class TypeExpr(ASTNode):
- """A type expression."""
- name: str = ""
- kind: str = "named" # "named", "intersection", "union", "optional", "literal", "struct", "parameterised"
- children: list[TypeExpr] = field(default_factory=list)
- fields: list[FieldDecl] = field(default_factory=list) # For struct types
- params: list[TypeExpr] = field(default_factory=list) # For parameterised types: Name
-
-
-@dataclass
-class ConstraintExpr(ASTNode):
- """A constraint in a where block."""
- expression: Optional[ASTNode] = None
-
-
-@dataclass
-class TypeDecl(ASTNode):
- """Type declaration."""
- name: str = ""
- type_expr: Optional[TypeExpr] = None
- fields: list[FieldDecl] = field(default_factory=list)
- constraints: list[ConstraintExpr] = field(default_factory=list)
-
-
-@dataclass
-class ContextDecl(ASTNode):
- """Context declaration."""
- ref: Optional[NodeRef] = None
- meta: list[MetaAnnotation] = field(default_factory=list)
- nodes: list[NodeDecl] = field(default_factory=list)
- edges: list[EdgeDecl] = field(default_factory=list)
- intents: list[IntentDecl] = field(default_factory=list)
- contexts: list[ContextDecl] = field(default_factory=list)
-
-
-# ── Queries ──
-
-@dataclass
-class EdgePattern(ASTNode):
- """Edge pattern in a query: -[type]-> target"""
- edge_type: str = ""
- target: Optional[ASTNode] = None # VarBind or NodeRef
- target_type: str = ""
- direction: str = "forward"
- hop_min: int = 1 # For multi-hop: -[T*2..5]->
- hop_max: int = 1 # -1 means unbounded (*)
-
-
-@dataclass
-class MatchClause(ASTNode):
- """Match clause in a query."""
- variable: Optional[VarBind] = None
- type_name: str = ""
- edge_pattern: Optional[EdgePattern] = None
- optional: bool = False
-
-
-@dataclass
-class AggField(ASTNode):
- """Aggregation field: name = func(expr)"""
- name: str = ""
- func: str = ""
- expr: Optional[ASTNode] = None
-
-
-@dataclass
-class ReturnClause(ASTNode):
- """Return clause in a query."""
- expression: Optional[ASTNode] = None
- with_edges: int = 0
- aggregations: list[AggField] = field(default_factory=list)
-
-
-@dataclass
-class QueryDecl(ASTNode):
- """Query declaration."""
- name: str = ""
- matches: list[MatchClause] = field(default_factory=list)
- where_expr: Optional[ASTNode] = None
- return_clause: Optional[ReturnClause] = None
- group_by: list[ASTNode] = field(default_factory=list)
-
-
-# ── Intents ──
-
-@dataclass
-class TriggerClause(ASTNode):
- """Trigger clause in an intent."""
- kind: str = "expression" # "expression", "query", "cron"
- expression: Optional[ASTNode] = None
- query: Optional[QueryDecl] = None
- cron_expr: str = ""
-
-
-@dataclass
-class EmitAction(ASTNode):
- """Emit action in an intent's do block."""
- node_decl: Optional[NodeDecl] = None
- action_type: str = "create" # "create", "update", "delete", "goto"
- target_ref: Optional[NodeRef] = None
- goto_state: str = "" # For action_type="goto" in process transitions
-
-
-@dataclass
-class IntentDecl(ASTNode):
- """Intent declaration."""
- ref: Optional[NodeRef] = None
- type_name: str = ""
- trigger: Optional[TriggerClause] = None
- actions: list[EmitAction] = field(default_factory=list)
- meta: list[MetaAnnotation] = field(default_factory=list)
-
-
-# ── Process declarations ──
-
-@dataclass
-class StateDecl(ASTNode):
- """State declaration within a process."""
- name: str = ""
- meta: list[MetaAnnotation] = field(default_factory=list)
-
-
-@dataclass
-class TransitionDecl(ASTNode):
- """Transition declaration within a process."""
- event: str = ""
- from_state: str = ""
- to_state: str = ""
- where_expr: Optional[ASTNode] = None
- actions: list[EmitAction] = field(default_factory=list)
- compensate_actions: list[EmitAction] = field(default_factory=list)
-
-
-@dataclass
-class ProcessDecl(ASTNode):
- """Process declaration."""
- ref: Optional[NodeRef] = None
- type_name: str = ""
- states: list[StateDecl] = field(default_factory=list)
- transitions: list[TransitionDecl] = field(default_factory=list)
- meta: list[MetaAnnotation] = field(default_factory=list)
-
-
-# ── Module System ──
-
-@dataclass
-class ImportDecl(ASTNode):
- """Import declaration."""
- names: list[str] = field(default_factory=list)
- source: str = ""
-
-
-@dataclass
-class ExportDecl(ASTNode):
- """Export declaration."""
- declaration: Optional[ASTNode] = None
-
-
-# ── Program ──
-
-@dataclass
-class Program(ASTNode):
- """Root AST node: a complete KNDL program."""
- imports: list[ImportDecl] = field(default_factory=list)
- exports: list[ExportDecl] = field(default_factory=list)
- types: list[TypeDecl] = field(default_factory=list)
- nodes: list[NodeDecl] = field(default_factory=list)
- edges: list[EdgeDecl] = field(default_factory=list)
- contexts: list[ContextDecl] = field(default_factory=list)
- intents: list[IntentDecl] = field(default_factory=list)
- queries: list[QueryDecl] = field(default_factory=list)
- processes: list[ProcessDecl] = field(default_factory=list)
diff --git a/packages/python/src/kndl/backends/__init__.py b/packages/python/src/kndl/backends/__init__.py
deleted file mode 100644
index 8ad578b..0000000
--- a/packages/python/src/kndl/backends/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# Storage backend implementations — imported lazily by kndl.storage.create_storage()
\ No newline at end of file
diff --git a/packages/python/src/kndl/backends/postgres_backend.py b/packages/python/src/kndl/backends/postgres_backend.py
deleted file mode 100644
index 6a51c8d..0000000
--- a/packages/python/src/kndl/backends/postgres_backend.py
+++ /dev/null
@@ -1,231 +0,0 @@
-"""
-PostgreSQL storage backend for KNDLGraph.
-
-Requires: pip install 'kndl[postgres]' (psycopg2-binary)
-
-Features:
- - JSONB columns for fields and meta — fast key-path queries, GIN-indexed
- - ON CONFLICT DO UPDATE (upsert) for all tables
- - Optional pgvector: add an `embedding VECTOR(1536)` column to kndl_nodes
- and query with cosine similarity if pgvector extension is installed
- - Automatic reconnect on connection drop (OperationalError)
-
-Schema is created automatically on first connect.
-"""
-
-from __future__ import annotations
-
-import json
-from typing import Any
-
-from kndl.graph import GraphEdge, GraphIntent, GraphNode
-
-try:
- import psycopg2
- import psycopg2.extras
- import psycopg2.extensions
-except ImportError as exc:
- raise ImportError(
- "PostgreSQL backend requires psycopg2.\n"
- "Install with: pip install 'kndl[postgres]'\n"
- " or: pip install psycopg2-binary"
- ) from exc
-
-
-_DDL = """
-CREATE TABLE IF NOT EXISTS kndl_nodes (
- id TEXT PRIMARY KEY,
- type_name TEXT NOT NULL,
- fields JSONB NOT NULL DEFAULT '{}',
- meta JSONB NOT NULL DEFAULT '{}'
-);
-CREATE TABLE IF NOT EXISTS kndl_edges (
- id TEXT PRIMARY KEY,
- source_id TEXT NOT NULL,
- target_id TEXT NOT NULL,
- edge_type TEXT NOT NULL DEFAULT 'relates_to',
- direction TEXT NOT NULL DEFAULT 'forward',
- fields JSONB NOT NULL DEFAULT '{}',
- meta JSONB NOT NULL DEFAULT '{}'
-);
-CREATE TABLE IF NOT EXISTS kndl_intents (
- id TEXT PRIMARY KEY,
- type_name TEXT NOT NULL,
- trigger_kind TEXT NOT NULL DEFAULT 'expression',
- trigger_data TEXT NOT NULL DEFAULT '',
- actions JSONB NOT NULL DEFAULT '[]',
- meta JSONB NOT NULL DEFAULT '{}'
-);
-"""
-
-# GIN indexes enable fast JSONB key/value lookups (e.g. meta @> '{"confidence": 0.9}')
-_INDEXES = [
- "CREATE INDEX IF NOT EXISTS idx_kndl_nodes_type ON kndl_nodes (type_name)",
- "CREATE INDEX IF NOT EXISTS idx_kndl_nodes_meta ON kndl_nodes USING GIN (meta)",
- "CREATE INDEX IF NOT EXISTS idx_kndl_nodes_fields ON kndl_nodes USING GIN (fields)",
- "CREATE INDEX IF NOT EXISTS idx_kndl_edges_source ON kndl_edges (source_id)",
- "CREATE INDEX IF NOT EXISTS idx_kndl_edges_target ON kndl_edges (target_id)",
- "CREATE INDEX IF NOT EXISTS idx_kndl_edges_type ON kndl_edges (edge_type)",
- "CREATE INDEX IF NOT EXISTS idx_kndl_intents_type ON kndl_intents (type_name)",
-]
-
-
-class PostgresStorage:
- def __init__(self, url: str) -> None:
- self._url = url
- self._conn = self._connect()
- self._setup()
-
- def _connect(self) -> psycopg2.extensions.connection:
- conn = psycopg2.connect(self._url, cursor_factory=psycopg2.extras.RealDictCursor)
- conn.autocommit = False
- return conn
-
- def _ensure_connection(self) -> None:
- """Reconnect automatically if the connection was dropped."""
- try:
- if self._conn.closed:
- self._conn = self._connect()
- return
- self._conn.cursor().execute("SELECT 1")
- except psycopg2.OperationalError:
- try:
- self._conn.close()
- except Exception:
- pass
- self._conn = self._connect()
-
- def _setup(self) -> None:
- with self._conn.cursor() as cur:
- for stmt in _DDL.strip().split(";"):
- stmt = stmt.strip()
- if stmt:
- cur.execute(stmt)
- for idx in _INDEXES:
- cur.execute(idx)
- self._conn.commit()
-
- # ── Load ──────────────────────────────────────────────────────────────────
-
- def load(self) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
- self._ensure_connection()
- with self._conn.cursor() as cur:
- cur.execute("SELECT id, type_name, fields, meta FROM kndl_nodes")
- nodes = [
- {"id": r["id"], "type": r["type_name"],
- "fields": r["fields"], "meta": r["meta"]}
- for r in cur.fetchall()
- ]
-
- cur.execute(
- "SELECT id, source_id, target_id, edge_type, direction, fields, meta"
- " FROM kndl_edges"
- )
- edges = [
- {"id": r["id"], "source": r["source_id"], "target": r["target_id"],
- "type": r["edge_type"], "direction": r["direction"],
- "fields": r["fields"], "meta": r["meta"]}
- for r in cur.fetchall()
- ]
-
- cur.execute(
- "SELECT id, type_name, trigger_kind, trigger_data, actions, meta"
- " FROM kndl_intents"
- )
- intents = [
- {"id": r["id"], "type": r["type_name"],
- "trigger_kind": r["trigger_kind"], "trigger_data": r["trigger_data"],
- "actions": r["actions"], "meta": r["meta"]}
- for r in cur.fetchall()
- ]
-
- return nodes, edges, intents
-
- # ── Upsert / delete ───────────────────────────────────────────────────────
-
- def upsert_node(self, node: GraphNode) -> None:
- self._ensure_connection()
- with self._conn.cursor() as cur:
- cur.execute(
- """
- INSERT INTO kndl_nodes (id, type_name, fields, meta)
- VALUES (%s, %s, %s, %s)
- ON CONFLICT (id) DO UPDATE
- SET type_name = EXCLUDED.type_name,
- fields = EXCLUDED.fields,
- meta = EXCLUDED.meta
- """,
- (node.id, node.type_name,
- json.dumps(node.fields), json.dumps(node.meta.to_dict())),
- )
- self._conn.commit()
-
- def delete_node(self, node_id: str) -> None:
- self._ensure_connection()
- with self._conn.cursor() as cur:
- cur.execute("DELETE FROM kndl_nodes WHERE id = %s", (node_id,))
- self._conn.commit()
-
- def upsert_edge(self, edge: GraphEdge) -> None:
- self._ensure_connection()
- with self._conn.cursor() as cur:
- cur.execute(
- """
- INSERT INTO kndl_edges
- (id, source_id, target_id, edge_type, direction, fields, meta)
- VALUES (%s, %s, %s, %s, %s, %s, %s)
- ON CONFLICT (id) DO UPDATE
- SET source_id = EXCLUDED.source_id,
- target_id = EXCLUDED.target_id,
- edge_type = EXCLUDED.edge_type,
- direction = EXCLUDED.direction,
- fields = EXCLUDED.fields,
- meta = EXCLUDED.meta
- """,
- (edge.id, edge.source_id, edge.target_id, edge.edge_type, edge.direction,
- json.dumps(edge.fields), json.dumps(edge.meta.to_dict())),
- )
- self._conn.commit()
-
- def delete_edge(self, edge_id: str) -> None:
- self._ensure_connection()
- with self._conn.cursor() as cur:
- cur.execute("DELETE FROM kndl_edges WHERE id = %s", (edge_id,))
- self._conn.commit()
-
- def upsert_intent(self, intent: GraphIntent) -> None:
- self._ensure_connection()
- with self._conn.cursor() as cur:
- cur.execute(
- """
- INSERT INTO kndl_intents
- (id, type_name, trigger_kind, trigger_data, actions, meta)
- VALUES (%s, %s, %s, %s, %s, %s)
- ON CONFLICT (id) DO UPDATE
- SET type_name = EXCLUDED.type_name,
- trigger_kind = EXCLUDED.trigger_kind,
- trigger_data = EXCLUDED.trigger_data,
- actions = EXCLUDED.actions,
- meta = EXCLUDED.meta
- """,
- (intent.id, intent.type_name, intent.trigger_kind, intent.trigger_data,
- json.dumps(intent.actions), json.dumps(intent.meta.to_dict())),
- )
- self._conn.commit()
-
- def delete_intent(self, intent_id: str) -> None:
- self._ensure_connection()
- with self._conn.cursor() as cur:
- cur.execute("DELETE FROM kndl_intents WHERE id = %s", (intent_id,))
- self._conn.commit()
-
- def clear(self) -> None:
- self._ensure_connection()
- with self._conn.cursor() as cur:
- cur.execute("DELETE FROM kndl_nodes")
- cur.execute("DELETE FROM kndl_edges")
- cur.execute("DELETE FROM kndl_intents")
- self._conn.commit()
-
- def close(self) -> None:
- self._conn.close()
diff --git a/packages/python/src/kndl/backends/sqlite_backend.py b/packages/python/src/kndl/backends/sqlite_backend.py
deleted file mode 100644
index 3fb00af..0000000
--- a/packages/python/src/kndl/backends/sqlite_backend.py
+++ /dev/null
@@ -1,167 +0,0 @@
-"""SQLite storage backend for KNDLGraph — zero extra dependencies (stdlib sqlite3)."""
-
-from __future__ import annotations
-
-import json
-import re
-import sqlite3
-from typing import Any
-
-from kndl.graph import GraphEdge, GraphIntent, GraphNode
-
-
-_DDL = """
-CREATE TABLE IF NOT EXISTS kndl_nodes (
- id TEXT PRIMARY KEY,
- type_name TEXT NOT NULL,
- fields TEXT NOT NULL DEFAULT '{}',
- meta TEXT NOT NULL DEFAULT '{}'
-);
-CREATE TABLE IF NOT EXISTS kndl_edges (
- id TEXT PRIMARY KEY,
- source_id TEXT NOT NULL,
- target_id TEXT NOT NULL,
- edge_type TEXT NOT NULL DEFAULT 'relates_to',
- direction TEXT NOT NULL DEFAULT 'forward',
- fields TEXT NOT NULL DEFAULT '{}',
- meta TEXT NOT NULL DEFAULT '{}'
-);
-CREATE TABLE IF NOT EXISTS kndl_intents (
- id TEXT PRIMARY KEY,
- type_name TEXT NOT NULL,
- trigger_kind TEXT NOT NULL DEFAULT 'expression',
- trigger_data TEXT NOT NULL DEFAULT '',
- actions TEXT NOT NULL DEFAULT '[]',
- meta TEXT NOT NULL DEFAULT '{}'
-);
-"""
-
-_INDEXES = [
- "CREATE INDEX IF NOT EXISTS idx_kndl_nodes_type ON kndl_nodes (type_name)",
- "CREATE INDEX IF NOT EXISTS idx_kndl_edges_source ON kndl_edges (source_id)",
- "CREATE INDEX IF NOT EXISTS idx_kndl_edges_target ON kndl_edges (target_id)",
- "CREATE INDEX IF NOT EXISTS idx_kndl_edges_type ON kndl_edges (edge_type)",
- "CREATE INDEX IF NOT EXISTS idx_kndl_intents_type ON kndl_intents (type_name)",
-]
-
-
-def _path_from_url(url: str) -> str:
- """sqlite:///./kndl.db → ./kndl.db, sqlite:///:memory: → :memory:"""
- m = re.match(r"sqlite:///(.+)", url)
- return m.group(1) if m else url
-
-
-class SQLiteStorage:
- def __init__(self, url: str) -> None:
- path = _path_from_url(url)
- self._conn = sqlite3.connect(path, check_same_thread=False)
- self._conn.row_factory = sqlite3.Row
- for stmt in _DDL.strip().split(";"):
- stmt = stmt.strip()
- if stmt:
- self._conn.execute(stmt)
- for idx in _INDEXES:
- self._conn.execute(idx)
- self._conn.commit()
-
- # ── Load ──────────────────────────────────────────────────────────────────
-
- def load(self) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
- cur = self._conn
-
- nodes = [
- {
- "id": r["id"],
- "type": r["type_name"],
- "fields": json.loads(r["fields"]),
- "meta": json.loads(r["meta"]),
- }
- for r in cur.execute(
- "SELECT id, type_name, fields, meta FROM kndl_nodes"
- )
- ]
-
- edges = [
- {
- "id": r["id"],
- "source": r["source_id"],
- "target": r["target_id"],
- "type": r["edge_type"],
- "direction": r["direction"],
- "fields": json.loads(r["fields"]),
- "meta": json.loads(r["meta"]),
- }
- for r in cur.execute(
- "SELECT id, source_id, target_id, edge_type, direction, fields, meta"
- " FROM kndl_edges"
- )
- ]
-
- intents = [
- {
- "id": r["id"],
- "type": r["type_name"],
- "trigger_kind": r["trigger_kind"],
- "trigger_data": r["trigger_data"],
- "actions": json.loads(r["actions"]),
- "meta": json.loads(r["meta"]),
- }
- for r in cur.execute(
- "SELECT id, type_name, trigger_kind, trigger_data, actions, meta"
- " FROM kndl_intents"
- )
- ]
-
- return nodes, edges, intents
-
- # ── Upsert / delete ───────────────────────────────────────────────────────
-
- def upsert_node(self, node: GraphNode) -> None:
- self._conn.execute(
- "INSERT OR REPLACE INTO kndl_nodes (id, type_name, fields, meta)"
- " VALUES (?, ?, ?, ?)",
- (node.id, node.type_name,
- json.dumps(node.fields), json.dumps(node.meta.to_dict())),
- )
- self._conn.commit()
-
- def delete_node(self, node_id: str) -> None:
- self._conn.execute("DELETE FROM kndl_nodes WHERE id = ?", (node_id,))
- self._conn.commit()
-
- def upsert_edge(self, edge: GraphEdge) -> None:
- self._conn.execute(
- "INSERT OR REPLACE INTO kndl_edges"
- " (id, source_id, target_id, edge_type, direction, fields, meta)"
- " VALUES (?, ?, ?, ?, ?, ?, ?)",
- (edge.id, edge.source_id, edge.target_id, edge.edge_type, edge.direction,
- json.dumps(edge.fields), json.dumps(edge.meta.to_dict())),
- )
- self._conn.commit()
-
- def delete_edge(self, edge_id: str) -> None:
- self._conn.execute("DELETE FROM kndl_edges WHERE id = ?", (edge_id,))
- self._conn.commit()
-
- def upsert_intent(self, intent: GraphIntent) -> None:
- self._conn.execute(
- "INSERT OR REPLACE INTO kndl_intents"
- " (id, type_name, trigger_kind, trigger_data, actions, meta)"
- " VALUES (?, ?, ?, ?, ?, ?)",
- (intent.id, intent.type_name, intent.trigger_kind, intent.trigger_data,
- json.dumps(intent.actions), json.dumps(intent.meta.to_dict())),
- )
- self._conn.commit()
-
- def delete_intent(self, intent_id: str) -> None:
- self._conn.execute("DELETE FROM kndl_intents WHERE id = ?", (intent_id,))
- self._conn.commit()
-
- def clear(self) -> None:
- self._conn.execute("DELETE FROM kndl_nodes")
- self._conn.execute("DELETE FROM kndl_edges")
- self._conn.execute("DELETE FROM kndl_intents")
- self._conn.commit()
-
- def close(self) -> None:
- self._conn.close()
\ No newline at end of file
diff --git a/packages/python/src/kndl/compiler.py b/packages/python/src/kndl/compiler.py
deleted file mode 100644
index f48800b..0000000
--- a/packages/python/src/kndl/compiler.py
+++ /dev/null
@@ -1,479 +0,0 @@
-"""
-KNDL Compiler — Transforms a parsed AST into a runtime KNDLGraph.
-
-Implements KNDL Specification v1.0.0, Section 4 (Core Constructs).
-Walks the Program AST produced by the Parser and populates a KNDLGraph
-with GraphNode, GraphEdge, and GraphIntent objects.
-"""
-
-from __future__ import annotations
-
-import re
-from dataclasses import dataclass, field
-from typing import Any
-
-from .ast_nodes import (
- ASTNode, Program, NodeDecl, EdgeDecl, TypeDecl, ContextDecl, IntentDecl,
- MetaAnnotation, ProcessDecl,
- Literal, NodeRef, ArrayLiteral, MapLiteral, RangeExpr, DecayExpr,
- BinaryOp, UnaryOp, FuncCall, FieldAccess, VarBind,
-)
-from .graph import KNDLGraph, GraphNode, GraphEdge, GraphIntent, KNDLMeta
-
-
-# ── Duration parsing ──────────────────────────────────────────────────────────
-
-_DURATION_RE = re.compile(r"^(\d+(?:\.\d+)?)(ns|us|mo|ms|s|m|h|d|w|y)$")
-_DURATION_MULT: dict[str, float] = {
- "ns": 1e-9, "us": 1e-6, "ms": 0.001,
- "s": 1.0, "m": 60.0, "h": 3600.0, "d": 86400.0, "w": 604800.0,
- "mo": 2592000.0, "y": 31536000.0,
-}
-
-
-def _duration_to_seconds(duration_str: str) -> float | None:
- """Convert a KNDL duration literal (e.g. '1h', '30m') to seconds."""
- m = _DURATION_RE.match(str(duration_str).strip())
- if not m:
- return None
- return float(m.group(1)) * _DURATION_MULT[m.group(2)]
-
-
-# ── Value evaluation ──────────────────────────────────────────────────────────
-
-def _eval_value(node: ASTNode | None) -> Any:
- """Evaluate an AST expression to a plain Python value."""
- if node is None:
- return None
-
- if isinstance(node, Literal):
- return node.value
-
- if isinstance(node, NodeRef):
- return node.full_ref
-
- if isinstance(node, ArrayLiteral):
- return [_eval_value(e) for e in node.elements]
-
- if isinstance(node, MapLiteral):
- return {_eval_value(k): _eval_value(v) for k, v in node.pairs}
-
- if isinstance(node, VarBind):
- return f"?{node.name}"
-
- if isinstance(node, FieldAccess):
- target = _eval_value(node.target)
- return f"{target}.{node.field_name}"
-
- if isinstance(node, FuncCall):
- args = [_eval_value(a) for a in node.args]
- return f"{node.name}({', '.join(str(a) for a in args)})"
-
- if isinstance(node, BinaryOp):
- left = _eval_value(node.left)
- right = _eval_value(node.right)
- return f"{left} {node.op} {right}"
-
- if isinstance(node, UnaryOp):
- operand = _eval_value(node.operand)
- return f"{node.op} {operand}"
-
- if isinstance(node, RangeExpr):
- start = _eval_value(node.start)
- end = _eval_value(node.end)
- return f"{start} .. {end}"
-
- if isinstance(node, DecayExpr):
- rate = _eval_value(node.rate)
- duration = _eval_value(node.duration)
- return f"{rate} / {duration}"
-
- return str(node)
-
-
-# ── Inherited meta context ────────────────────────────────────────────────────
-
-@dataclass
-class _MetaContext:
- """Inherited meta-annotation defaults from parent context."""
- confidence: float = 1.0
- source: str = ""
- access: str = ""
- extra: dict[str, Any] = field(default_factory=dict)
-
- def override(self, annotations: list[MetaAnnotation]) -> _MetaContext:
- """Return a new context with these annotations merged in."""
- ctx = _MetaContext(
- confidence=self.confidence,
- source=self.source,
- access=self.access,
- extra=dict(self.extra),
- )
- for ann in annotations:
- match ann.key:
- case "confidence":
- ctx.confidence = float(_eval_value(ann.value) or ctx.confidence)
- case "source":
- ctx.source = str(_eval_value(ann.value) or ctx.source)
- case "access":
- ctx.access = str(_eval_value(ann.value) or ctx.access)
- case _:
- ctx.extra[ann.key] = _eval_value(ann.value)
- return ctx
-
-
-# ── Meta extraction ───────────────────────────────────────────────────────────
-
-def _build_meta(annotations: list[MetaAnnotation], ctx: _MetaContext) -> KNDLMeta:
- """Convert AST meta-annotations + inherited context into a KNDLMeta."""
- meta = KNDLMeta(
- confidence=ctx.confidence,
- source=ctx.source,
- access=ctx.access,
- )
-
- # Apply inherited custom keys
- for k, v in ctx.extra.items():
- meta.custom[k] = v
-
- for ann in annotations:
- key = ann.key
- val_node = ann.value
-
- match key:
- case "confidence":
- raw = _eval_value(val_node)
- meta.confidence = float(raw) if raw is not None else meta.confidence
-
- case "source":
- raw = _eval_value(val_node)
- meta.source = str(raw) if raw is not None else meta.source
-
- case "access":
- raw = _eval_value(val_node)
- meta.access = str(raw) if raw is not None else meta.access
-
- case "valid":
- if isinstance(val_node, RangeExpr):
- start = _eval_value(val_node.start)
- end = _eval_value(val_node.end)
- meta.valid_start = str(start) if start not in (None, "null") else None
- meta.valid_end = str(end) if end not in ("*", None, "null") else None
- else:
- meta.valid_start = str(_eval_value(val_node))
-
- case "decay":
- if isinstance(val_node, DecayExpr):
- rate = _eval_value(val_node.rate)
- dur = _eval_value(val_node.duration)
- meta.decay_rate = float(rate) if rate is not None else None
- meta.decay_duration_seconds = (
- _duration_to_seconds(str(dur)) if dur is not None else None
- )
- elif isinstance(val_node, BinaryOp) and val_node.op == "/":
- # Parser may consume `0.95 / 1h` as BinaryOp division
- rate = _eval_value(val_node.left)
- dur = _eval_value(val_node.right)
- meta.decay_rate = float(rate) if rate is not None else None
- meta.decay_duration_seconds = (
- _duration_to_seconds(str(dur)) if dur is not None else None
- )
-
- case "supersedes":
- raw = _eval_value(val_node)
- meta.supersedes = str(raw) if raw else None
-
- case "derived":
- raw = _eval_value(val_node)
- if isinstance(raw, list):
- meta.derived_from = [str(r) for r in raw]
- elif raw:
- meta.derived_from = [str(raw)]
-
- case "priority":
- raw = _eval_value(val_node)
- meta.priority = float(raw) if raw is not None else meta.priority
-
- case "cooldown":
- raw = _eval_value(val_node)
- if raw is not None:
- meta.cooldown_seconds = _duration_to_seconds(str(raw))
-
- case "tags":
- raw = _eval_value(val_node)
- if isinstance(raw, list):
- meta.tags = [str(t) for t in raw]
-
- case "weight":
- raw = _eval_value(val_node)
- meta.custom["weight"] = float(raw) if raw is not None else None
-
- case "recorded":
- raw = _eval_value(val_node)
- meta.recorded = str(raw) if raw is not None else None
-
- case "observed":
- raw = _eval_value(val_node)
- meta.observed = str(raw) if raw is not None else None
-
- case "negated":
- raw = _eval_value(val_node)
- meta.negated = bool(raw) if raw is not None else False
-
- case "deadline":
- raw = _eval_value(val_node)
- meta.deadline = str(raw) if raw is not None else None
-
- case "classification":
- raw = _eval_value(val_node)
- meta.classification = str(raw) if raw is not None else None
-
- case "retention":
- raw = _eval_value(val_node)
- meta.retention = str(raw) if raw is not None else None
-
- case "uncertainty":
- raw = _eval_value(val_node)
- meta.uncertainty = dict(raw) if isinstance(raw, dict) else {"value": raw}
-
- case _:
- meta.custom[key] = _eval_value(val_node)
-
- return meta
-
-
-# ── Compiler ──────────────────────────────────────────────────────────────────
-
-class Compiler:
- """
- Compiles a KNDL Program AST into a KNDLGraph.
-
- Usage:
- compiler = Compiler()
- graph = compiler.compile(program)
- """
-
- def compile(self, program: Program) -> KNDLGraph:
- graph = KNDLGraph()
- ctx = _MetaContext()
- self._compile_program(program, graph, ctx)
- return graph
-
- # ── Program ──
-
- def _compile_program(self, program: Program, graph: KNDLGraph, ctx: _MetaContext) -> None:
- for type_decl in program.types:
- self._compile_type_decl(type_decl, graph)
- for node_decl in program.nodes:
- self._compile_node_decl(node_decl, graph, ctx)
- for edge_decl in program.edges:
- self._compile_edge_decl(edge_decl, graph, ctx)
- for context_decl in program.contexts:
- self._compile_context_decl(context_decl, graph, ctx)
- for intent_decl in program.intents:
- self._compile_intent_decl(intent_decl, graph, ctx)
- for process_decl in program.processes:
- self._compile_process_decl(process_decl, graph, ctx)
-
- # ── Types ──
-
- def _compile_type_decl(self, decl: TypeDecl, graph: KNDLGraph) -> None:
- graph.types[decl.name] = {
- "name": decl.name,
- "fields": {f.name: f.type_expr.name if f.type_expr else "Any" for f in decl.fields},
- "constraints": [str(c.expression) for c in decl.constraints] if decl.constraints else [],
- }
-
- # ── Nodes ──
-
- def _compile_node_decl(
- self, decl: NodeDecl, graph: KNDLGraph, ctx: _MetaContext
- ) -> GraphNode:
- node_id = decl.ref.name if decl.ref else ""
- fields: dict[str, Any] = {}
- inline_edges: list[tuple[str, str]] = []
-
- for member in decl.fields:
- fields[member.name] = _eval_value(member.value)
-
- for ie in decl.edges:
- # `ie.target` may be optional; skip if missing to satisfy type checker
- if ie.target is not None:
- inline_edges.append((ie.field_name, ie.target.name))
-
- meta = _build_meta(decl.meta, ctx)
- node = GraphNode(id=node_id, type_name=decl.type_name, fields=fields, meta=meta)
- graph.add_node(node)
-
- # Inline edges become GraphEdge objects
- for edge_field, target_id in inline_edges:
- edge = GraphEdge(
- source_id=node_id,
- target_id=target_id,
- edge_type=edge_field,
- meta=KNDLMeta(confidence=meta.confidence, source=meta.source),
- )
- graph.add_edge(edge)
-
- return node
-
- # ── Edges ──
-
- def _compile_edge_decl(
- self, decl: EdgeDecl, graph: KNDLGraph, ctx: _MetaContext
- ) -> list[GraphEdge]:
- source_id = decl.source.name if decl.source else ""
- meta = _build_meta(decl.meta, ctx)
- fields: dict[str, Any] = {f.name: _eval_value(f.value) for f in decl.fields}
- created: list[GraphEdge] = []
-
- for target_ref in decl.targets:
- edge = GraphEdge(
- source_id=source_id,
- target_id=target_ref.name,
- edge_type=decl.edge_type,
- direction=decl.direction,
- fields=dict(fields),
- meta=meta,
- )
- graph.add_edge(edge)
- created.append(edge)
-
- # Bidirectional → also add reverse edge
- if decl.direction == "bidirectional":
- rev = GraphEdge(
- source_id=target_ref.name,
- target_id=source_id,
- edge_type=decl.edge_type,
- direction="bidirectional",
- fields=dict(fields),
- meta=meta,
- )
- graph.add_edge(rev)
- created.append(rev)
-
- return created
-
- # ── Contexts ──
-
- def _compile_context_decl(
- self, decl: ContextDecl, graph: KNDLGraph, parent_ctx: _MetaContext
- ) -> None:
- # Context meta-annotations are inherited by all child nodes
- child_ctx = parent_ctx.override(decl.meta)
-
- for type_decl in getattr(decl, "types", []):
- self._compile_type_decl(type_decl, graph)
- for node_decl in decl.nodes:
- self._compile_node_decl(node_decl, graph, child_ctx)
- for edge_decl in decl.edges:
- self._compile_edge_decl(edge_decl, graph, child_ctx)
- for intent_decl in decl.intents:
- self._compile_intent_decl(intent_decl, graph, child_ctx)
- for nested_ctx in decl.contexts:
- self._compile_context_decl(nested_ctx, graph, child_ctx)
-
- # ── Intents ──
-
- def _compile_intent_decl(
- self, decl: IntentDecl, graph: KNDLGraph, ctx: _MetaContext
- ) -> GraphIntent:
- intent_id = decl.ref.name if decl.ref else ""
- meta = _build_meta(decl.meta, ctx)
-
- trigger_kind = "expression"
- trigger_data = ""
- if decl.trigger:
- trigger_kind = decl.trigger.kind
- if decl.trigger.kind == "cron":
- trigger_data = decl.trigger.cron_expr
- elif decl.trigger.kind == "query" and decl.trigger.query:
- trigger_data = decl.trigger.query.name or "inline_query"
- elif decl.trigger.expression:
- trigger_data = str(_eval_value(decl.trigger.expression))
-
- actions: list[dict[str, Any]] = []
- for action in decl.actions:
- if action.action_type == "create" and action.node_decl:
- nd = action.node_decl
- actions.append({
- "type": "create",
- "node_type": nd.type_name,
- "fields": {f.name: _eval_value(f.value) for f in nd.fields},
- })
- elif action.action_type == "delete" and action.target_ref:
- actions.append({
- "type": "delete",
- "target": action.target_ref.full_ref,
- })
- elif action.action_type == "update" and action.node_decl:
- nd = action.node_decl
- actions.append({
- "type": "update",
- "target": nd.ref.full_ref if nd.ref else "",
- "fields": {f.name: _eval_value(f.value) for f in nd.fields},
- })
-
- intent = GraphIntent(
- id=intent_id,
- type_name=decl.type_name,
- trigger_kind=trigger_kind,
- trigger_data=trigger_data,
- actions=actions,
- meta=meta,
- )
- graph.add_intent(intent)
- return intent
-
- # ── Processes ──
-
- def _compile_process_decl(
- self, decl: ProcessDecl, graph: KNDLGraph, ctx: _MetaContext
- ) -> None:
- process_id = decl.ref.name if decl.ref else ""
- meta = _build_meta(decl.meta, ctx)
-
- states = []
- for sd in decl.states:
- state_meta = _build_meta(sd.meta, ctx)
- states.append({
- "name": sd.name,
- "meta": state_meta.to_dict(),
- })
-
- transitions = []
- for td in decl.transitions:
- t_entry: dict[str, Any] = {
- "event": td.event,
- "from": td.from_state,
- "to": td.to_state,
- }
- if td.where_expr is not None:
- t_entry["where"] = str(_eval_value(td.where_expr))
- if td.actions:
- t_entry["actions"] = [
- {
- "type": a.action_type,
- "node_type": a.node_decl.type_name if a.node_decl else "",
- "fields": {f.name: _eval_value(f.value) for f in a.node_decl.fields} if a.node_decl else {},
- }
- for a in td.actions
- ]
- if td.compensate_actions:
- t_entry["compensate"] = [
- {
- "type": a.action_type,
- "node_type": a.node_decl.type_name if a.node_decl else "",
- "fields": {f.name: _eval_value(f.value) for f in a.node_decl.fields} if a.node_decl else {},
- }
- for a in td.compensate_actions
- ]
- transitions.append(t_entry)
-
- graph.processes[process_id] = {
- "id": process_id,
- "type": decl.type_name,
- "states": states,
- "transitions": transitions,
- "meta": meta.to_dict(),
- }
diff --git a/packages/python/src/kndl/graph.py b/packages/python/src/kndl/graph.py
deleted file mode 100644
index dd8552b..0000000
--- a/packages/python/src/kndl/graph.py
+++ /dev/null
@@ -1,474 +0,0 @@
-"""
-KNDL Graph — In-memory knowledge graph with confidence-aware operations.
-
-This is the runtime representation of a parsed KNDL program. It supports:
-- Node and edge storage with meta-annotations
-- Confidence decay computation
-- Simple graph queries
-- Serialization to/from dict (JSON-compatible)
-"""
-
-from __future__ import annotations
-
-import uuid
-from dataclasses import dataclass, field
-from datetime import datetime, timezone
-from typing import TYPE_CHECKING, Any, Optional
-
-if TYPE_CHECKING:
- from kndl.storage import KNDLStorage
-
-
-@dataclass
-class KNDLMeta:
- """Meta-annotations for a node or edge."""
- confidence: float = 1.0
- source: str = ""
- valid_start: Optional[str] = None
- valid_end: Optional[str] = None
- decay_rate: Optional[float] = None
- decay_duration_seconds: Optional[float] = None
- supersedes: Optional[str] = None
- derived_from: list[str] = field(default_factory=list)
- access: str = ""
- priority: float = 0.5
- cooldown_seconds: Optional[float] = None
- tags: list[str] = field(default_factory=list)
- custom: dict[str, Any] = field(default_factory=dict)
- recorded: Optional[str] = None
- observed: Optional[str] = None
- negated: bool = False
- deadline: Optional[str] = None
- classification: Optional[str] = None
- retention: Optional[str] = None
- uncertainty: Optional[dict[str, Any]] = None # §9: gaussian/interval/categorical/histogram
-
- def effective_confidence(self, at_time: Optional[datetime] = None) -> float:
- """
- Compute effective confidence with decay applied.
-
- Formula: confidence × (decay_rate ^ (elapsed / decay_duration))
- """
- if self.decay_rate is None or self.decay_duration_seconds is None:
- return self.confidence
-
- if self.valid_start is None:
- return self.confidence
-
- now = at_time or datetime.now(timezone.utc)
- try:
- start = datetime.fromisoformat(self.valid_start.replace("Z", "+00:00"))
- except (ValueError, AttributeError):
- return self.confidence
-
- elapsed = (now - start).total_seconds()
- if elapsed <= 0:
- return self.confidence
-
- # Narrow optional attributes to local floats for mypy
- decay_rate: float = float(self.decay_rate) # safe: guarded by earlier None check
- decay_dur: float = float(self.decay_duration_seconds) # safe: guarded above
- periods = elapsed / decay_dur
- return float(self.confidence * (decay_rate ** periods))
-
- def to_dict(self) -> dict[str, Any]:
- d: dict[str, Any] = {}
- if self.confidence != 1.0:
- d["confidence"] = self.confidence
- if self.source:
- d["source"] = self.source
- if self.valid_start:
- d["valid_start"] = self.valid_start
- if self.valid_end:
- d["valid_end"] = self.valid_end
- if self.decay_rate is not None:
- d["decay_rate"] = self.decay_rate
- d["decay_duration_seconds"] = self.decay_duration_seconds
- if self.supersedes:
- d["supersedes"] = self.supersedes
- if self.derived_from:
- d["derived_from"] = self.derived_from
- if self.access:
- d["access"] = self.access
- if self.priority != 0.5:
- d["priority"] = self.priority
- if self.cooldown_seconds:
- d["cooldown_seconds"] = self.cooldown_seconds
- if self.tags:
- d["tags"] = self.tags
- if self.custom:
- d["custom"] = self.custom
- if self.recorded:
- d["recorded"] = self.recorded
- if self.observed:
- d["observed"] = self.observed
- if self.negated:
- d["negated"] = self.negated
- if self.deadline:
- d["deadline"] = self.deadline
- if self.classification:
- d["classification"] = self.classification
- if self.retention:
- d["retention"] = self.retention
- if self.uncertainty is not None:
- d["uncertainty"] = self.uncertainty
- return d
-
- @classmethod
- def from_dict(cls, d: dict[str, Any]) -> KNDLMeta:
- return cls(
- confidence=d.get("confidence", 1.0),
- source=d.get("source", ""),
- valid_start=d.get("valid_start"),
- valid_end=d.get("valid_end"),
- decay_rate=d.get("decay_rate"),
- decay_duration_seconds=d.get("decay_duration_seconds"),
- supersedes=d.get("supersedes"),
- derived_from=d.get("derived_from", []),
- access=d.get("access", ""),
- priority=d.get("priority", 0.5),
- cooldown_seconds=d.get("cooldown_seconds"),
- tags=d.get("tags", []),
- custom=d.get("custom", {}),
- recorded=d.get("recorded"),
- observed=d.get("observed"),
- negated=d.get("negated", False),
- deadline=d.get("deadline"),
- classification=d.get("classification"),
- retention=d.get("retention"),
- uncertainty=d.get("uncertainty"),
- )
-
-
-@dataclass
-class GraphNode:
- """A node in the knowledge graph."""
- id: str = ""
- type_name: str = ""
- fields: dict[str, Any] = field(default_factory=dict)
- meta: KNDLMeta = field(default_factory=KNDLMeta)
-
- def to_dict(self) -> dict[str, Any]:
- d: dict[str, Any] = {
- "id": self.id,
- "type": self.type_name,
- "fields": self.fields,
- }
- meta_d = self.meta.to_dict()
- if meta_d:
- d["meta"] = meta_d
- return d
-
- @classmethod
- def from_dict(cls, d: dict[str, Any]) -> GraphNode:
- return cls(
- id=d["id"],
- type_name=d.get("type", ""),
- fields=d.get("fields", {}),
- meta=KNDLMeta.from_dict(d.get("meta", {})),
- )
-
-
-@dataclass
-class GraphEdge:
- """An edge in the knowledge graph."""
- id: str = ""
- source_id: str = ""
- target_id: str = ""
- edge_type: str = "relates_to"
- direction: str = "forward"
- fields: dict[str, Any] = field(default_factory=dict)
- meta: KNDLMeta = field(default_factory=KNDLMeta)
-
- def to_dict(self) -> dict[str, Any]:
- d: dict[str, Any] = {
- "id": self.id,
- "source": self.source_id,
- "target": self.target_id,
- "type": self.edge_type,
- "direction": self.direction,
- }
- if self.fields:
- d["fields"] = self.fields
- meta_d = self.meta.to_dict()
- if meta_d:
- d["meta"] = meta_d
- return d
-
-
-@dataclass
-class GraphIntent:
- """An intent (reactive rule) in the knowledge graph."""
- id: str = ""
- type_name: str = ""
- trigger_kind: str = "expression"
- trigger_data: str = ""
- actions: list[dict[str, Any]] = field(default_factory=list)
- meta: KNDLMeta = field(default_factory=KNDLMeta)
- last_fired: Optional[float] = None
-
- def to_dict(self) -> dict[str, Any]:
- d: dict[str, Any] = {
- "id": self.id,
- "type": self.type_name,
- "trigger": {"kind": self.trigger_kind, "data": self.trigger_data},
- "actions": self.actions,
- }
- meta_d = self.meta.to_dict()
- if meta_d:
- d["meta"] = meta_d
- return d
-
-
-class KNDLGraph:
- """
- In-memory knowledge graph.
-
- Supports CRUD operations, simple queries, confidence decay,
- and serialization.
- """
-
- def __init__(self, storage: "KNDLStorage | None" = None) -> None:
- self.nodes: dict[str, GraphNode] = {}
- self.edges: dict[str, GraphEdge] = {}
- self.intents: dict[str, GraphIntent] = {}
- self.types: dict[str, dict[str, Any]] = {}
- self.processes: dict[str, Any] = {}
- self._edge_index_out: dict[str, list[str]] = {} # node_id -> [edge_ids]
- self._edge_index_in: dict[str, list[str]] = {}
- self._storage: KNDLStorage | None = storage
-
- # ── Node operations ──
-
- def add_node(self, node: GraphNode) -> GraphNode:
- if not node.id:
- node.id = str(uuid.uuid4())
- self.nodes[node.id] = node
- if self._storage is not None:
- self._storage.upsert_node(node)
- return node
-
- def get_node(self, node_id: str) -> Optional[GraphNode]:
- return self.nodes.get(node_id)
-
- def remove_node(self, node_id: str) -> bool:
- if node_id not in self.nodes:
- return False
- del self.nodes[node_id]
- # Remove connected edges
- for eid in list(self._edge_index_out.get(node_id, [])):
- self.remove_edge(eid)
- for eid in list(self._edge_index_in.get(node_id, [])):
- self.remove_edge(eid)
- if self._storage is not None:
- self._storage.delete_node(node_id)
- return True
-
- def update_node(self, node_id: str, fields: Optional[dict[str, Any]] = None,
- meta_updates: Optional[dict[str, Any]] = None) -> Optional[GraphNode]:
- node = self.nodes.get(node_id)
- if not node:
- return None
- if fields:
- node.fields.update(fields)
- if meta_updates:
- for k, v in meta_updates.items():
- if hasattr(node.meta, k):
- setattr(node.meta, k, v)
- if self._storage is not None:
- self._storage.upsert_node(node)
- return node
-
- # ── Edge operations ──
-
- def add_edge(self, edge: GraphEdge) -> GraphEdge:
- if not edge.id:
- edge.id = str(uuid.uuid4())
- self.edges[edge.id] = edge
- self._edge_index_out.setdefault(edge.source_id, []).append(edge.id)
- self._edge_index_in.setdefault(edge.target_id, []).append(edge.id)
- if self._storage is not None:
- self._storage.upsert_edge(edge)
- return edge
-
- def get_edge(self, edge_id: str) -> Optional[GraphEdge]:
- return self.edges.get(edge_id)
-
- def remove_edge(self, edge_id: str) -> bool:
- edge = self.edges.pop(edge_id, None)
- if not edge:
- return False
- if edge.source_id in self._edge_index_out:
- self._edge_index_out[edge.source_id] = [
- e for e in self._edge_index_out[edge.source_id] if e != edge_id
- ]
- if edge.target_id in self._edge_index_in:
- self._edge_index_in[edge.target_id] = [
- e for e in self._edge_index_in[edge.target_id] if e != edge_id
- ]
- if self._storage is not None:
- self._storage.delete_edge(edge_id)
- return True
-
- def get_outgoing_edges(self, node_id: str, edge_type: Optional[str] = None) -> list[GraphEdge]:
- eids = self._edge_index_out.get(node_id, [])
- edges = [self.edges[eid] for eid in eids if eid in self.edges]
- if edge_type:
- edges = [e for e in edges if e.edge_type == edge_type]
- return edges
-
- def get_incoming_edges(self, node_id: str, edge_type: Optional[str] = None) -> list[GraphEdge]:
- eids = self._edge_index_in.get(node_id, [])
- edges = [self.edges[eid] for eid in eids if eid in self.edges]
- if edge_type:
- edges = [e for e in edges if e.edge_type == edge_type]
- return edges
-
- # ── Intent operations ──
-
- def add_intent(self, intent: GraphIntent) -> GraphIntent:
- if not intent.id:
- intent.id = str(uuid.uuid4())
- self.intents[intent.id] = intent
- if self._storage is not None:
- self._storage.upsert_intent(intent)
- return intent
-
- def remove_intent(self, intent_id: str) -> bool:
- if intent_id not in self.intents:
- return False
- del self.intents[intent_id]
- if self._storage is not None:
- self._storage.delete_intent(intent_id)
- return True
-
- # ── Query ──
-
- def query_nodes(
- self,
- type_name: Optional[str] = None,
- min_confidence: float = 0.0,
- field_filters: Optional[dict[str, Any]] = None,
- apply_decay: bool = True,
- ) -> list[GraphNode]:
- """
- Query nodes with optional type, confidence, and field filters.
- """
- results = []
- for node in self.nodes.values():
- if type_name and node.type_name != type_name:
- continue
-
- conf = node.meta.effective_confidence() if apply_decay else node.meta.confidence
- if conf < min_confidence:
- continue
-
- if field_filters:
- match = True
- for k, v in field_filters.items():
- if k not in node.fields or node.fields[k] != v:
- match = False
- break
- if not match:
- continue
-
- results.append(node)
- return results
-
- def query_neighborhood(self, node_id: str, hops: int = 1) -> dict[str, Any]:
- """Get the N-hop neighborhood around a node."""
- visited_nodes: set[str] = set()
- visited_edges: set[str] = set()
- frontier = {node_id}
-
- for _ in range(hops):
- next_frontier: set[str] = set()
- for nid in frontier:
- visited_nodes.add(nid)
- for edge in self.get_outgoing_edges(nid):
- visited_edges.add(edge.id)
- if edge.target_id not in visited_nodes:
- next_frontier.add(edge.target_id)
- for edge in self.get_incoming_edges(nid):
- visited_edges.add(edge.id)
- if edge.source_id not in visited_nodes:
- next_frontier.add(edge.source_id)
- frontier = next_frontier
-
- visited_nodes.update(frontier)
-
- return {
- "nodes": [self.nodes[nid].to_dict() for nid in visited_nodes if nid in self.nodes],
- "edges": [self.edges[eid].to_dict() for eid in visited_edges if eid in self.edges],
- }
-
- # ── Serialization ──
-
- def to_dict(self) -> dict[str, Any]:
- d: dict[str, Any] = {
- "nodes": [n.to_dict() for n in self.nodes.values()],
- "edges": [e.to_dict() for e in self.edges.values()],
- "intents": [i.to_dict() for i in self.intents.values()],
- "types": self.types,
- "summary": {
- "node_count": len(self.nodes),
- "edge_count": len(self.edges),
- "intent_count": len(self.intents),
- "type_count": len(self.types),
- },
- }
- if self.processes:
- d["processes"] = self.processes
- return d
-
- @classmethod
- def from_storage(cls, storage: "KNDLStorage") -> "KNDLGraph":
- """Create a graph pre-populated from an existing storage backend."""
- g = cls(storage=storage)
- nodes, edges, intents = storage.load()
- # Bypass storage writes during bulk load (data already persisted)
- g._storage = None
- for nd in nodes:
- g.add_node(GraphNode.from_dict(nd))
- for ed in edges:
- g.add_edge(GraphEdge(
- id=ed["id"],
- source_id=ed["source"],
- target_id=ed["target"],
- edge_type=ed.get("type", "relates_to"),
- direction=ed.get("direction", "forward"),
- fields=ed.get("fields", {}),
- meta=KNDLMeta.from_dict(ed.get("meta", {})),
- ))
- for it in intents:
- intent = GraphIntent(
- id=it["id"],
- type_name=it.get("type", ""),
- trigger_kind=it.get("trigger_kind", "expression"),
- trigger_data=it.get("trigger_data", ""),
- actions=it.get("actions", []),
- meta=KNDLMeta.from_dict(it.get("meta", {})),
- )
- g.intents[intent.id] = intent
- g._storage = storage # re-attach after load
- return g
-
- @classmethod
- def from_dict(cls, d: dict[str, Any]) -> "KNDLGraph":
- g = cls()
- for nd in d.get("nodes", []):
- g.add_node(GraphNode.from_dict(nd))
- for ed in d.get("edges", []):
- edge = GraphEdge(
- id=ed["id"],
- source_id=ed["source"],
- target_id=ed["target"],
- edge_type=ed.get("type", "relates_to"),
- direction=ed.get("direction", "forward"),
- fields=ed.get("fields", {}),
- meta=KNDLMeta.from_dict(ed.get("meta", {})),
- )
- g.add_edge(edge)
- g.types = d.get("types", {})
- g.processes = d.get("processes", {})
- return g
diff --git a/packages/python/src/kndl/lexer.py b/packages/python/src/kndl/lexer.py
deleted file mode 100644
index dee12c1..0000000
--- a/packages/python/src/kndl/lexer.py
+++ /dev/null
@@ -1,666 +0,0 @@
-"""
-KNDL Lexer — Tokenizer for the Knowledge Node Description Language.
-
-Converts raw KNDL source text into a stream of typed tokens.
-Implements KNDL Specification v1.0.0, Section 2 (Lexical Structure).
-"""
-
-from __future__ import annotations
-
-import re
-from dataclasses import dataclass
-from enum import Enum, auto
-from typing import Iterator
-
-
-class TokenType(Enum):
- """All token types in the KNDL language."""
-
- # Literals
- INT = auto()
- FLOAT = auto()
- DECIMAL = auto() # float with 'd' suffix (e.g. 19.99d)
- STRING = auto()
- BOOL = auto()
- NULL = auto()
- DURATION = auto()
- DATETIME = auto()
- BYTES = auto() # b"base64..."
- VECTOR = auto() # v[0.1, -0.2, 0.3]
- UUID = auto() # u"0189..."
-
- # Identifiers & references
- IDENTIFIER = auto()
- NODE_REF = auto() # @name or @name.sub
- VAR_BIND = auto() # ?name
- META_KEY = auto() # ~name
-
- # Keywords
- KW_NODE = auto()
- KW_EDGE = auto()
- KW_TYPE = auto()
- KW_INTENT = auto()
- KW_CONTEXT = auto()
- KW_QUERY = auto()
- KW_MATCH = auto()
- KW_WHERE = auto()
- KW_RETURN = auto()
- KW_WITH = auto()
- KW_EMIT = auto()
- KW_DO = auto()
- KW_TRIGGER = auto()
- KW_CRON = auto()
- KW_IF = auto()
- KW_ELSE = auto()
- KW_IN = auto()
- KW_AND = auto()
- KW_OR = auto()
- KW_NOT = auto()
- KW_TRUE = auto()
- KW_FALSE = auto()
- KW_NULL = auto()
- KW_NOW = auto()
- KW_LAST = auto()
- KW_WITHIN = auto()
- KW_OVERLAPS = auto()
- KW_AGGREGATE = auto()
- KW_SUM = auto()
- KW_AVG = auto()
- KW_MIN = auto()
- KW_MAX = auto()
- KW_COUNT = auto()
- KW_GROUP = auto()
- KW_AS = auto()
- KW_IMPORT = auto()
- KW_EXPORT = auto()
- KW_FROM = auto()
- KW_OPTIONAL = auto()
- KW_EDGES = auto()
- KW_UPDATE = auto()
- KW_DELETE = auto()
- KW_MATCHES = auto()
- KW_PROCESS = auto()
- KW_STATE = auto()
- KW_ON = auto()
- KW_GOTO = auto()
- KW_COMPENSATE = auto()
- KW_BY = auto()
- KW_OF = auto()
-
- # Operators
- OP_ASSIGN = auto() # =
- OP_DOUBLE_COLON = auto() # ::
- OP_COLON = auto() # :
- OP_ARROW = auto() # ->
- OP_BIARROW = auto() # <->
- OP_RANGE = auto() # ..
- OP_DOT = auto() # .
- OP_COMMA = auto() # ,
- OP_QUESTION = auto() # ?
- OP_AMP = auto() # &
- OP_PIPE = auto() # |
- OP_STAR = auto() # *
- OP_SLASH = auto() # /
- OP_PERCENT = auto() # %
- OP_PLUS = auto() # +
- OP_MINUS = auto() # -
- OP_GT = auto() # >
- OP_LT = auto() # <
- OP_GTE = auto() # >=
- OP_LTE = auto() # <=
- OP_EQ = auto() # ==
- OP_NEQ = auto() # !=
- OP_LOGICAL_AND = auto() # &&
- OP_LOGICAL_OR = auto() # ||
- OP_PLUS_ASSIGN = auto() # +=
-
- # Delimiters
- LBRACE = auto() # {
- RBRACE = auto() # }
- LBRACKET = auto() # [
- RBRACKET = auto() # ]
- LPAREN = auto() # (
- RPAREN = auto() # )
- MAP_OPEN = auto() # #{
-
- # Typed edge markers
- TYPED_ARROW_START = auto() # -[
- TYPED_ARROW_END = auto() # ]->
- TYPED_BIARROW_START = auto() # <-[
- TYPED_BIARROW_END = auto() # ]-> (same token, context-dependent)
-
- # Special
- NEWLINE = auto()
- EOF = auto()
- ERROR = auto()
-
-
-@dataclass(frozen=True, slots=True)
-class Token:
- """A single lexical token."""
- type: TokenType
- value: str
- line: int
- col: int
-
- def __repr__(self) -> str:
- val = self.value if len(self.value) <= 30 else self.value[:27] + "..."
- return f"Token({self.type.name}, {val!r}, {self.line}:{self.col})"
-
-
-# ── Keyword map ──
-KEYWORDS: dict[str, TokenType] = {
- "node": TokenType.KW_NODE,
- "edge": TokenType.KW_EDGE,
- "type": TokenType.KW_TYPE,
- "intent": TokenType.KW_INTENT,
- "context": TokenType.KW_CONTEXT,
- "query": TokenType.KW_QUERY,
- "match": TokenType.KW_MATCH,
- "where": TokenType.KW_WHERE,
- "return": TokenType.KW_RETURN,
- "with": TokenType.KW_WITH,
- "emit": TokenType.KW_EMIT,
- "do": TokenType.KW_DO,
- "trigger": TokenType.KW_TRIGGER,
- "cron": TokenType.KW_CRON,
- "if": TokenType.KW_IF,
- "else": TokenType.KW_ELSE,
- "in": TokenType.KW_IN,
- "and": TokenType.KW_AND,
- "or": TokenType.KW_OR,
- "not": TokenType.KW_NOT,
- "true": TokenType.KW_TRUE,
- "false": TokenType.KW_FALSE,
- "null": TokenType.KW_NULL,
- "now": TokenType.KW_NOW,
- "last": TokenType.KW_LAST,
- "within": TokenType.KW_WITHIN,
- "overlaps": TokenType.KW_OVERLAPS,
- "aggregate": TokenType.KW_AGGREGATE,
- "sum": TokenType.KW_SUM,
- "avg": TokenType.KW_AVG,
- "min": TokenType.KW_MIN,
- "max": TokenType.KW_MAX,
- "count": TokenType.KW_COUNT,
- "group": TokenType.KW_GROUP,
- "as": TokenType.KW_AS,
- "import": TokenType.KW_IMPORT,
- "export": TokenType.KW_EXPORT,
- "from": TokenType.KW_FROM,
- "optional": TokenType.KW_OPTIONAL,
- "edges": TokenType.KW_EDGES,
- "update": TokenType.KW_UPDATE,
- "delete": TokenType.KW_DELETE,
- "matches": TokenType.KW_MATCHES,
- "process": TokenType.KW_PROCESS,
- "state": TokenType.KW_STATE,
- "on": TokenType.KW_ON,
- "goto": TokenType.KW_GOTO,
- "compensate": TokenType.KW_COMPENSATE,
- "by": TokenType.KW_BY,
- "of": TokenType.KW_OF,
-}
-
-DURATION_UNITS = {"ms", "ns", "us", "mo", "s", "m", "h", "d", "w", "y"}
-
-# Datetime regex
-_DATETIME_RE = re.compile(
- r"\d{4}-(?:\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})?)?|Q[1-4]|W\d{2})"
-)
-
-
-class LexerError(Exception):
- """Raised when the lexer encounters an invalid token."""
-
- def __init__(self, message: str, line: int, col: int):
- super().__init__(f"Lexer error at {line}:{col}: {message}")
- self.line = line
- self.col = col
-
-
-class Lexer:
- """
- Tokenizer for KNDL source text.
-
- Usage:
- lexer = Lexer(source_code)
- tokens = list(lexer.tokenize())
- """
-
- def __init__(self, source: str):
- self.source = source
- self.pos = 0
- self.line = 1
- self.col = 1
-
- @property
- def _at_end(self) -> bool:
- return self.pos >= len(self.source)
-
- def _peek(self, offset: int = 0) -> str:
- idx = self.pos + offset
- if idx >= len(self.source):
- return "\0"
- return self.source[idx]
-
- def _advance(self) -> str:
- ch = self.source[self.pos]
- self.pos += 1
- if ch == "\n":
- self.line += 1
- self.col = 1
- else:
- self.col += 1
- return ch
-
- def _match(self, expected: str) -> bool:
- if self.pos < len(self.source) and self.source[self.pos] == expected:
- self._advance()
- return True
- return False
-
- def _lookahead(self, text: str) -> bool:
- return self.source[self.pos : self.pos + len(text)] == text
-
- def _skip_whitespace_and_comments(self) -> None:
- while not self._at_end:
- ch = self._peek()
-
- # Whitespace (not newlines)
- if ch in (" ", "\t", "\r"):
- self._advance()
- continue
-
- # Newlines
- if ch == "\n":
- return # Let caller handle newlines
-
- # Line comments
- if ch == "/" and self._peek(1) == "/":
- while not self._at_end and self._peek() != "\n":
- self._advance()
- continue
-
- # Block comments
- if ch == "/" and self._peek(1) == "*":
- self._advance() # /
- self._advance() # *
- depth = 1
- while not self._at_end and depth > 0:
- if self._peek() == "/" and self._peek(1) == "*":
- depth += 1
- self._advance()
- elif self._peek() == "*" and self._peek(1) == "/":
- depth -= 1
- self._advance()
- self._advance()
- continue
-
- break
-
- def _read_string(self) -> str:
- """Read a double-quoted string literal."""
- result: list[str] = []
- self._advance() # opening "
- while not self._at_end:
- ch = self._advance()
- if ch == '"':
- return "".join(result)
- if ch == "\\":
- esc = self._advance()
- if esc == '"':
- result.append('"')
- elif esc == "\\":
- result.append("\\")
- elif esc == "n":
- result.append("\n")
- elif esc == "r":
- result.append("\r")
- elif esc == "t":
- result.append("\t")
- elif esc == "/":
- result.append("/")
- elif esc == "u":
- hex_str = ""
- for _ in range(4):
- hex_str += self._advance()
- result.append(chr(int(hex_str, 16)))
- else:
- result.append(esc)
- else:
- result.append(ch)
- raise LexerError("Unterminated string literal", self.line, self.col)
-
- def _read_number_or_datetime(self, start_line: int, start_col: int) -> Token:
- """Read a number, duration, datetime, or decimal literal."""
- start = self.pos - 1 # We already consumed the first digit
-
- # Collect digits
- while not self._at_end and (self._peek().isdigit() or self._peek() == "_"):
- self._advance()
-
- # Check for datetime: NNNN- pattern
- text_so_far = self.source[start : self.pos]
- if len(text_so_far) == 4 and not self._at_end and self._peek() == "-":
- # Might be a datetime
- _remaining = self.source[self.pos :]
- m = _DATETIME_RE.match(self.source[start:])
- if m:
- full = m.group(0)
- # Advance past the rest
- while self.pos < start + len(full):
- self._advance()
- return Token(TokenType.DATETIME, full, start_line, start_col)
-
- # Check for hex/binary
- if text_so_far == "0" and not self._at_end:
- if self._peek() in ("x", "X"):
- self._advance()
- while not self._at_end and self._peek() in "0123456789abcdefABCDEF_":
- self._advance()
- return Token(TokenType.INT, self.source[start : self.pos], start_line, start_col)
- if self._peek() in ("b", "B"):
- self._advance()
- while not self._at_end and self._peek() in "01_":
- self._advance()
- return Token(TokenType.INT, self.source[start : self.pos], start_line, start_col)
-
- # Check for float
- is_float = False
- if not self._at_end and self._peek() == ".":
- # Disambiguate from range operator (..)
- if self._peek(1) != ".":
- is_float = True
- self._advance() # .
- while not self._at_end and (self._peek().isdigit() or self._peek() == "_"):
- self._advance()
-
- # Check for exponent
- if not self._at_end and self._peek() in ("e", "E"):
- is_float = True
- self._advance()
- if not self._at_end and self._peek() in ("+", "-"):
- self._advance()
- while not self._at_end and self._peek().isdigit():
- self._advance()
-
- # Check for decimal suffix ('d' after a float, e.g. 19.99d)
- # Must be a float (has decimal point) followed by 'd' not followed by more alpha chars
- if is_float and not self._at_end and self._peek() == "d":
- next_after = self._peek(1)
- if not next_after.isalpha() and next_after != "_":
- self._advance() # consume 'd'
- val = self.source[start : self.pos].replace("_", "")
- return Token(TokenType.DECIMAL, val, start_line, start_col)
-
- # Check for duration suffix
- if not self._at_end:
- suffix_start = self.pos
- suffix = ""
- # Read up to 2 alpha chars for suffix
- while not self._at_end and self._peek().isalpha() and len(suffix) < 2:
- suffix += self._peek()
- self.pos += 1
- if suffix in DURATION_UNITS:
- return Token(TokenType.DURATION, self.source[start : self.pos], start_line, start_col)
- self.pos = suffix_start # Reset if not a valid duration
-
- val = self.source[start : self.pos].replace("_", "")
- return Token(
- TokenType.FLOAT if is_float else TokenType.INT,
- val,
- start_line,
- start_col,
- )
-
- def _read_identifier_or_keyword(self, start_line: int, start_col: int) -> Token:
- """Read an identifier or keyword, or a prefixed literal (b\", v[, u\")."""
- start = self.pos - 1
- while not self._at_end and (self._peek().isalnum() or self._peek() == "_"):
- self._advance()
- text = self.source[start : self.pos]
-
- # Bytes literal: b"..."
- if text == "b" and not self._at_end and self._peek() == '"':
- content = self._read_string()
- return Token(TokenType.BYTES, content, start_line, start_col)
-
- # Vector literal: v[f, f, ...]
- if text == "v" and not self._at_end and self._peek() == "[":
- self._advance() # consume '['
- vec_start = self.pos
- depth = 1
- while not self._at_end and depth > 0:
- if self._peek() == "[":
- depth += 1
- elif self._peek() == "]":
- depth -= 1
- if depth > 0:
- self._advance()
- content = self.source[vec_start : self.pos].strip()
- if not self._at_end:
- self._advance() # consume ']'
- return Token(TokenType.VECTOR, content, start_line, start_col)
-
- # UUID literal: u"..."
- if text == "u" and not self._at_end and self._peek() == '"':
- content = self._read_string()
- return Token(TokenType.UUID, content, start_line, start_col)
-
- # Check keywords
- if text in KEYWORDS:
- tt = KEYWORDS[text]
- if tt == TokenType.KW_TRUE:
- return Token(TokenType.BOOL, "true", start_line, start_col)
- if tt == TokenType.KW_FALSE:
- return Token(TokenType.BOOL, "false", start_line, start_col)
- if tt == TokenType.KW_NULL:
- return Token(TokenType.NULL, "null", start_line, start_col)
- return Token(tt, text, start_line, start_col)
-
- return Token(TokenType.IDENTIFIER, text, start_line, start_col)
-
- def _read_node_ref(self, start_line: int, start_col: int) -> Token:
- """Read a node reference (@identifier.sub)."""
- start = self.pos - 1 # We consumed @
- while not self._at_end and (self._peek().isalnum() or self._peek() in ("_", ".")):
- # Don't consume trailing . before .
- if self._peek() == "." and (self._at_end or not self.source[self.pos + 1 :self.pos + 2].isalnum()):
- if self._peek(1) == ".":
- break # That's the range operator
- break
- self._advance()
- return Token(TokenType.NODE_REF, self.source[start : self.pos], start_line, start_col)
-
- def tokenize(self) -> Iterator[Token]:
- """Generate a stream of tokens from the source."""
- while not self._at_end:
- self._skip_whitespace_and_comments()
- if self._at_end:
- break
-
- start_line = self.line
- start_col = self.col
- ch = self._advance()
-
- # Newlines
- if ch == "\n":
- continue # Skip newlines as tokens (whitespace-insensitive)
-
- # Strings
- if ch == '"':
- self.pos -= 1
- self.col -= 1
- sl, sc = self.line, self.col
- val = self._read_string()
- yield Token(TokenType.STRING, val, sl, sc)
- continue
-
- # Node references
- if ch == "@":
- yield self._read_node_ref(start_line, start_col)
- continue
-
- # Meta keys
- if ch == "~":
- ms = self.pos
- while not self._at_end and (self._peek().isalnum() or self._peek() in ("_", ":")):
- self._advance()
- yield Token(TokenType.META_KEY, self.source[ms : self.pos], start_line, start_col)
- continue
-
- # Variable bindings
- if ch == "?":
- if not self._at_end and self._peek().isalpha():
- vs = self.pos
- while not self._at_end and (self._peek().isalnum() or self._peek() == "_"):
- self._advance()
- yield Token(TokenType.VAR_BIND, self.source[vs : self.pos], start_line, start_col)
- else:
- yield Token(TokenType.OP_QUESTION, "?", start_line, start_col)
- continue
-
- # Numbers and datetimes
- if ch.isdigit():
- yield self._read_number_or_datetime(start_line, start_col)
- continue
-
- # Negative numbers
- if ch == "-" and not self._at_end and self._peek().isdigit():
- yield self._read_number_or_datetime(start_line, start_col)
- continue
-
- # Identifiers and keywords
- if ch.isalpha() or ch == "_":
- yield self._read_identifier_or_keyword(start_line, start_col)
- continue
-
- # Degree symbol — start of temperature unit atom (°C, °F, °K)
- if ch == "°":
- unit_str = ch
- while not self._at_end and self._peek().isalpha():
- unit_str += self._advance()
- yield Token(TokenType.IDENTIFIER, unit_str, start_line, start_col)
- continue
-
- # Multi-character operators
- if ch == ":":
- if self._match(":"):
- yield Token(TokenType.OP_DOUBLE_COLON, "::", start_line, start_col)
- else:
- yield Token(TokenType.OP_COLON, ":", start_line, start_col)
- continue
-
- if ch == "-":
- if self._match(">"):
- yield Token(TokenType.OP_ARROW, "->", start_line, start_col)
- elif self._match("["):
- yield Token(TokenType.TYPED_ARROW_START, "-[", start_line, start_col)
- else:
- yield Token(TokenType.OP_MINUS, "-", start_line, start_col)
- continue
-
- if ch == "<":
- if self._match("-"):
- if self._match(">"):
- yield Token(TokenType.OP_BIARROW, "<->", start_line, start_col)
- elif self._match("["):
- yield Token(TokenType.TYPED_BIARROW_START, "<-[", start_line, start_col)
- else:
- yield Token(TokenType.OP_LT, "<", start_line, start_col)
- yield Token(TokenType.OP_MINUS, "-", start_line, start_col)
- elif self._match("="):
- yield Token(TokenType.OP_LTE, "<=", start_line, start_col)
- else:
- yield Token(TokenType.OP_LT, "<", start_line, start_col)
- continue
-
- if ch == ">":
- if self._match("="):
- yield Token(TokenType.OP_GTE, ">=", start_line, start_col)
- else:
- yield Token(TokenType.OP_GT, ">", start_line, start_col)
- continue
-
- if ch == "=":
- if self._match("="):
- yield Token(TokenType.OP_EQ, "==", start_line, start_col)
- else:
- yield Token(TokenType.OP_ASSIGN, "=", start_line, start_col)
- continue
-
- if ch == "!":
- if self._match("="):
- yield Token(TokenType.OP_NEQ, "!=", start_line, start_col)
- else:
- yield Token(TokenType.ERROR, "!", start_line, start_col)
- continue
-
- if ch == "&":
- if self._match("&"):
- yield Token(TokenType.OP_LOGICAL_AND, "&&", start_line, start_col)
- else:
- yield Token(TokenType.OP_AMP, "&", start_line, start_col)
- continue
-
- if ch == "|":
- if self._match("|"):
- yield Token(TokenType.OP_LOGICAL_OR, "||", start_line, start_col)
- else:
- yield Token(TokenType.OP_PIPE, "|", start_line, start_col)
- continue
-
- if ch == ".":
- if self._match("."):
- yield Token(TokenType.OP_RANGE, "..", start_line, start_col)
- else:
- yield Token(TokenType.OP_DOT, ".", start_line, start_col)
- continue
-
- if ch == "+":
- if self._match("="):
- yield Token(TokenType.OP_PLUS_ASSIGN, "+=", start_line, start_col)
- else:
- yield Token(TokenType.OP_PLUS, "+", start_line, start_col)
- continue
-
- if ch == "]":
- # Use lookahead to check for ]-> before consuming any characters.
- # Without lookahead, _match("-") would consume "-" even when ">" doesn't follow.
- if self._lookahead("->"):
- self._advance() # -
- self._advance() # >
- yield Token(TokenType.TYPED_ARROW_END, "]->", start_line, start_col)
- else:
- yield Token(TokenType.RBRACKET, "]", start_line, start_col)
- continue
-
- # Hash — check for #{ (MAP_OPEN)
- if ch == "#":
- if self._match("{"):
- yield Token(TokenType.MAP_OPEN, "#{", start_line, start_col)
- else:
- yield Token(TokenType.ERROR, "#", start_line, start_col)
- continue
-
- # Single-character tokens
- simple: dict[str, TokenType] = {
- "{": TokenType.LBRACE,
- "}": TokenType.RBRACE,
- "[": TokenType.LBRACKET,
- "(": TokenType.LPAREN,
- ")": TokenType.RPAREN,
- ",": TokenType.OP_COMMA,
- "*": TokenType.OP_STAR,
- "/": TokenType.OP_SLASH,
- "%": TokenType.OP_PERCENT,
- }
- if ch in simple:
- yield Token(simple[ch], ch, start_line, start_col)
- continue
-
- # Unknown character
- raise LexerError(f"Unexpected character: {ch!r}", start_line, start_col)
-
- yield Token(TokenType.EOF, "", self.line, self.col)
diff --git a/packages/python/src/kndl/parser.py b/packages/python/src/kndl/parser.py
deleted file mode 100644
index cf49b4b..0000000
--- a/packages/python/src/kndl/parser.py
+++ /dev/null
@@ -1,1075 +0,0 @@
-"""
-KNDL Parser — Recursive descent parser for KNDL.
-
-Consumes a token stream from the Lexer and produces an AST (Program).
-Implements KNDL Specification v1.0.0, Sections 3–6.
-"""
-
-from __future__ import annotations
-
-from typing import Optional
-
-from .lexer import Lexer, Token, TokenType
-
-from .ast_nodes import (
- ASTNode, Program, NodeDecl, EdgeDecl, TypeDecl, ContextDecl,
- IntentDecl, QueryDecl, ImportDecl, ExportDecl,
- NodeRef, VarBind, Literal, FieldAssignment, InlineEdge,
- MetaAnnotation, FieldDecl, TypeExpr, ConstraintExpr,
- MatchClause, EdgePattern, ReturnClause, AggField,
- TriggerClause, EmitAction,
- BinaryOp, UnaryOp, FuncCall, FieldAccess, IndexAccess,
- ArrayLiteral, MapLiteral, RangeExpr, DecayExpr,
- StateDecl, TransitionDecl, ProcessDecl,
-)
-
-# Unit atoms from the spec §2.8.9. Used to recognise quantity literals.
-_UNIT_ATOMS: frozenset[str] = frozenset({
- "°C", "°F", "K",
- "m", "cm", "mm", "km", "ft", "in",
- "kg", "g", "mg", "lb",
- "s", "ms", "min", "hr",
- "A", "V", "W", "Wh", "kWh", "J",
- "Pa", "kPa", "bar",
- "mol", "cd", "lm", "lx",
- "Hz", "kHz", "MHz", "GHz",
- "B", "KB", "MB", "GB", "TB",
- "bps", "kbps", "Mbps", "Gbps",
-})
-
-
-class ParseError(Exception):
- """Raised when the parser encounters a syntax error."""
-
- def __init__(self, message: str, token: Token):
- loc = f"{token.line}:{token.col}"
- super().__init__(f"Parse error at {loc}: {message} (got {token.type.name} '{token.value}')")
- self.token = token
-
-
-class Parser:
- """
- Recursive descent parser for KNDL.
-
- Usage:
- parser = Parser(source_code)
- program = parser.parse()
- """
-
- def __init__(self, source: str):
- self.lexer = Lexer(source)
- self.tokens: list[Token] = list(self.lexer.tokenize())
- self.pos = 0
-
- # ── Token navigation ──
-
- def _current(self) -> Token:
- if self.pos < len(self.tokens):
- return self.tokens[self.pos]
- return Token(TokenType.EOF, "", 0, 0)
-
- def _peek(self, offset: int = 0) -> Token:
- idx = self.pos + offset
- if idx < len(self.tokens):
- return self.tokens[idx]
- return Token(TokenType.EOF, "", 0, 0)
-
- def _advance(self) -> Token:
- tok = self._current()
- self.pos += 1
- return tok
-
- def _expect(self, tt: TokenType, msg: str = "") -> Token:
- tok = self._current()
- if tok.type != tt:
- err_msg = msg or f"Expected {tt.name}"
- raise ParseError(err_msg, tok)
- return self._advance()
-
- def _match(self, *types: TokenType) -> Optional[Token]:
- if self._current().type in types:
- return self._advance()
- return None
-
- def _at(self, *types: TokenType) -> bool:
- return self._current().type in types
-
- # ── Program (top-level) ──
-
- def parse(self) -> Program:
- """Parse a complete KNDL program."""
- prog = Program()
-
- while not self._at(TokenType.EOF):
- tok = self._current()
-
- if tok.type == TokenType.KW_NODE:
- prog.nodes.append(self._parse_node_decl())
- elif tok.type == TokenType.KW_EDGE:
- prog.edges.append(self._parse_edge_decl())
- elif tok.type == TokenType.KW_TYPE:
- prog.types.append(self._parse_type_decl())
- elif tok.type == TokenType.KW_CONTEXT:
- prog.contexts.append(self._parse_context_decl())
- elif tok.type == TokenType.KW_INTENT:
- prog.intents.append(self._parse_intent_decl())
- elif tok.type == TokenType.KW_QUERY:
- prog.queries.append(self._parse_query_decl())
- elif tok.type == TokenType.KW_IMPORT:
- prog.imports.append(self._parse_import_decl())
- elif tok.type == TokenType.KW_EXPORT:
- prog.exports.append(self._parse_export_decl())
- elif tok.type == TokenType.KW_PROCESS:
- prog.processes.append(self._parse_process_decl())
- else:
- raise ParseError(f"Unexpected top-level token, {tok}", tok)
-
- return prog
-
- # ── Node declaration ──
-
- def _parse_node_decl(self) -> NodeDecl:
- tok = self._expect(TokenType.KW_NODE)
- node = NodeDecl(line=tok.line, col=tok.col)
-
- ref_tok = self._expect(TokenType.NODE_REF, "Expected node reference (@name)")
- node.ref = self._make_node_ref(ref_tok)
-
- self._expect(TokenType.OP_DOUBLE_COLON, "Expected '::'")
-
- type_tok = self._expect(TokenType.IDENTIFIER, "Expected type name")
- node.type_name = type_tok.value
-
- self._expect(TokenType.LBRACE, "Expected '{'")
- self._parse_node_body(node)
- self._expect(TokenType.RBRACE, "Expected '}'")
-
- return node
-
- def _parse_node_body(self, node: NodeDecl) -> None:
- """Parse the body of a node: fields, inline edges, meta annotations."""
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- tok = self._current()
-
- if tok.type == TokenType.META_KEY:
- node.meta.append(self._parse_meta_annotation())
- elif tok.type == TokenType.IDENTIFIER:
- # Look ahead: is this field = value or field -> @ref?
- if self._peek(1).type == TokenType.OP_ARROW:
- node.edges.append(self._parse_inline_edge())
- elif self._peek(1).type in (TokenType.OP_ASSIGN, TokenType.OP_PLUS_ASSIGN):
- node.fields.append(self._parse_field_assignment())
- else:
- raise ParseError("Expected '=' or '->' after field name", self._peek(1))
- else:
- raise ParseError("Unexpected token in node body", tok)
-
- def _parse_field_assignment(self) -> FieldAssignment:
- name_tok = self._expect(TokenType.IDENTIFIER)
- op = self._match(TokenType.OP_ASSIGN, TokenType.OP_PLUS_ASSIGN)
- if not op:
- raise ParseError("Expected '=' or '+='", self._current())
- value = self._parse_expression()
- fa = FieldAssignment(name=name_tok.value, value=value, line=name_tok.line, col=name_tok.col)
- return fa
-
- def _parse_inline_edge(self) -> InlineEdge:
- name_tok = self._expect(TokenType.IDENTIFIER)
- self._expect(TokenType.OP_ARROW)
- ref_tok = self._expect(TokenType.NODE_REF, "Expected node reference after '->'")
- return InlineEdge(
- field_name=name_tok.value,
- target=self._make_node_ref(ref_tok),
- line=name_tok.line,
- col=name_tok.col,
- )
-
- def _parse_meta_annotation(self) -> MetaAnnotation:
- key_tok = self._expect(TokenType.META_KEY)
- meta = MetaAnnotation(key=key_tok.value, line=key_tok.line, col=key_tok.col)
-
- # Parse value — could be simple, range (x .. y), or decay (x / dur)
- val = self._parse_expression()
-
- # Check for range (..)
- if self._match(TokenType.OP_RANGE):
- end = self._parse_expression()
- meta.value = RangeExpr(start=val, end=end, line=val.line, col=val.col)
- # Check for decay (/ duration)
- elif self._match(TokenType.OP_SLASH):
- dur = self._parse_expression()
- meta.value = DecayExpr(rate=val, duration=dur, line=val.line, col=val.col)
- else:
- meta.value = val
-
- return meta
-
- # ── Edge declaration ──
-
- def _parse_edge_decl(self) -> EdgeDecl:
- tok = self._expect(TokenType.KW_EDGE)
- edge = EdgeDecl(line=tok.line, col=tok.col)
-
- src_tok = self._expect(TokenType.NODE_REF, "Expected source node reference")
- edge.source = self._make_node_ref(src_tok)
-
- # Parse edge operator
- if self._match(TokenType.TYPED_ARROW_START):
- # -[T]-> (forward typed) or -[T]- (undirected typed)
- type_tok = self._expect(TokenType.IDENTIFIER, "Expected edge type name")
- edge.edge_type = type_tok.value
- if self._at(TokenType.TYPED_ARROW_END):
- self._advance() # consume ]->
- edge.direction = "forward"
- elif self._at(TokenType.RBRACKET):
- # Check for ]- (undirected)
- self._advance() # consume ]
- self._expect(TokenType.OP_MINUS, "Expected '-' for undirected edge -[T]-")
- edge.direction = "undirected"
- else:
- raise ParseError("Expected ']->'" , self._current())
- elif self._at(TokenType.TYPED_BIARROW_START):
- # <-[T]-> (bidirectional) OR <-[T]- (reverse)
- self._advance() # consume <-[
- type_tok = self._expect(TokenType.IDENTIFIER, "Expected edge type name")
- edge.edge_type = type_tok.value
- # Check what follows: ]-> is bidirectional, ]- is reverse
- if self._at(TokenType.TYPED_ARROW_END):
- # ]->) bidirectional
- self._advance()
- edge.direction = "bidirectional"
- elif self._at(TokenType.RBRACKET):
- # ] followed by - => reverse
- self._advance() # consume ]
- self._expect(TokenType.OP_MINUS, "Expected '-' for reverse edge <-[T]-")
- edge.direction = "reverse"
- else:
- raise ParseError("Expected ']->'" , self._current())
- elif self._match(TokenType.OP_ARROW):
- edge.direction = "forward"
- elif self._match(TokenType.OP_BIARROW):
- edge.direction = "bidirectional"
- else:
- raise ParseError("Expected edge operator (-> or -[type]->)", self._current())
-
- # Parse targets: single ref or array
- if self._at(TokenType.LBRACKET):
- self._advance()
- while not self._at(TokenType.RBRACKET, TokenType.EOF):
- ref_tok = self._expect(TokenType.NODE_REF)
- edge.targets.append(self._make_node_ref(ref_tok))
- if not self._match(TokenType.OP_COMMA):
- break
- self._expect(TokenType.RBRACKET, "Expected ']'")
- else:
- ref_tok = self._expect(TokenType.NODE_REF, "Expected target node reference")
- edge.targets.append(self._make_node_ref(ref_tok))
-
- # Optional body
- if self._match(TokenType.LBRACE):
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- if self._at(TokenType.META_KEY):
- edge.meta.append(self._parse_meta_annotation())
- elif self._at(TokenType.IDENTIFIER):
- edge.fields.append(self._parse_field_assignment())
- else:
- raise ParseError("Unexpected token in edge body", self._current())
- self._expect(TokenType.RBRACE)
-
- return edge
-
- # ── Type declaration ──
-
- def _parse_type_decl(self) -> TypeDecl:
- tok = self._expect(TokenType.KW_TYPE)
- td = TypeDecl(line=tok.line, col=tok.col)
-
- name_tok = self._expect(TokenType.IDENTIFIER, "Expected type name")
- td.name = name_tok.value
-
- # Optional = type_expr
- if self._match(TokenType.OP_ASSIGN):
- td.type_expr = self._parse_type_expr()
-
- # Optional struct body { fields }
- if self._match(TokenType.LBRACE):
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- fd = self._parse_field_decl()
- td.fields.append(fd)
- self._expect(TokenType.RBRACE)
-
- # Optional where { constraints }
- if self._match(TokenType.KW_WHERE):
- self._expect(TokenType.LBRACE)
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- expr = self._parse_expression()
- td.constraints.append(ConstraintExpr(expression=expr))
- self._expect(TokenType.RBRACE)
-
- return td
-
- def _parse_type_expr(self) -> TypeExpr:
- """Parse a type expression with union and intersection."""
- left = self._parse_type_primary()
-
- while self._at(TokenType.OP_AMP, TokenType.OP_PIPE):
- op_tok = self._advance()
- right = self._parse_type_primary()
- kind = "intersection" if op_tok.type == TokenType.OP_AMP else "union"
- combined = TypeExpr(kind=kind, children=[left, right], line=op_tok.line, col=op_tok.col)
- left = combined
-
- return left
-
- def _parse_type_primary(self) -> TypeExpr:
- tok = self._current()
-
- if tok.type == TokenType.STRING:
- self._advance()
- te = TypeExpr(name=tok.value, kind="literal", line=tok.line, col=tok.col)
- elif tok.type == TokenType.IDENTIFIER:
- self._advance()
- te = TypeExpr(name=tok.value, kind="named", line=tok.line, col=tok.col)
- # Parameterised type: Name
- if self._at(TokenType.OP_LT):
- self._advance() # consume <
- te.kind = "parameterised"
- while not self._at(TokenType.OP_GT, TokenType.EOF):
- te.params.append(self._parse_type_primary())
- if not self._match(TokenType.OP_COMMA):
- break
- self._expect(TokenType.OP_GT, "Expected '>' to close parameterised type")
- elif tok.type == TokenType.LBRACE:
- self._advance()
- te = TypeExpr(kind="struct", line=tok.line, col=tok.col)
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- te.fields.append(self._parse_field_decl())
- self._expect(TokenType.RBRACE)
- else:
- raise ParseError("Expected type expression", tok)
-
- # Optional ?
- if self._match(TokenType.OP_QUESTION):
- te = TypeExpr(kind="optional", children=[te], line=te.line, col=te.col)
-
- return te
-
- def _parse_field_decl(self) -> FieldDecl:
- name_tok = self._expect(TokenType.IDENTIFIER, "Expected field name")
- self._expect(TokenType.OP_COLON, "Expected ':'")
- type_expr = self._parse_type_expr()
- return FieldDecl(name=name_tok.value, type_expr=type_expr, line=name_tok.line, col=name_tok.col)
-
- # ── Context declaration ──
-
- def _parse_context_decl(self) -> ContextDecl:
- tok = self._expect(TokenType.KW_CONTEXT)
- ref_tok = self._expect(TokenType.NODE_REF)
- ctx = ContextDecl(ref=self._make_node_ref(ref_tok), line=tok.line, col=tok.col)
-
- self._expect(TokenType.LBRACE)
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- cur = self._current()
- if cur.type == TokenType.META_KEY:
- ctx.meta.append(self._parse_meta_annotation())
- elif cur.type == TokenType.KW_NODE:
- ctx.nodes.append(self._parse_node_decl())
- elif cur.type == TokenType.KW_EDGE:
- ctx.edges.append(self._parse_edge_decl())
- elif cur.type == TokenType.KW_INTENT:
- ctx.intents.append(self._parse_intent_decl())
- elif cur.type == TokenType.KW_CONTEXT:
- ctx.contexts.append(self._parse_context_decl())
- else:
- raise ParseError("Unexpected token in context body", cur)
- self._expect(TokenType.RBRACE)
-
- return ctx
-
- # ── Intent declaration ──
-
- def _parse_intent_decl(self) -> IntentDecl:
- tok = self._expect(TokenType.KW_INTENT)
- ref_tok = self._expect(TokenType.NODE_REF)
- self._expect(TokenType.OP_DOUBLE_COLON)
- type_tok = self._expect(TokenType.IDENTIFIER)
-
- intent = IntentDecl(
- ref=self._make_node_ref(ref_tok),
- type_name=type_tok.value,
- line=tok.line,
- col=tok.col,
- )
-
- self._expect(TokenType.LBRACE)
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- cur = self._current()
- if cur.type == TokenType.KW_TRIGGER:
- intent.trigger = self._parse_trigger_clause()
- elif cur.type == TokenType.KW_DO:
- intent.actions = self._parse_do_clause()
- elif cur.type == TokenType.META_KEY:
- intent.meta.append(self._parse_meta_annotation())
- else:
- raise ParseError("Unexpected token in intent body", cur)
- self._expect(TokenType.RBRACE)
-
- return intent
-
- def _parse_trigger_clause(self) -> TriggerClause:
- self._expect(TokenType.KW_TRIGGER)
- self._expect(TokenType.OP_ASSIGN)
- tok = self._current()
-
- if tok.type == TokenType.KW_QUERY:
- query = self._parse_query_decl()
- return TriggerClause(kind="query", query=query, line=tok.line, col=tok.col)
- elif tok.type == TokenType.KW_CRON:
- self._advance()
- cron_tok = self._expect(TokenType.STRING)
- return TriggerClause(kind="cron", cron_expr=cron_tok.value, line=tok.line, col=tok.col)
- else:
- expr = self._parse_expression()
- return TriggerClause(kind="expression", expression=expr, line=tok.line, col=tok.col)
-
- def _parse_do_clause(self) -> list[EmitAction]:
- self._expect(TokenType.KW_DO)
- self._expect(TokenType.LBRACE)
- actions = []
-
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- if self._at(TokenType.KW_EMIT):
- self._advance()
- if self._at(TokenType.KW_NODE):
- nd = self._parse_node_decl()
- actions.append(EmitAction(node_decl=nd, action_type="create"))
- elif self._at(TokenType.KW_UPDATE):
- self._advance()
- ref_tok = self._expect(TokenType.NODE_REF)
- self._expect(TokenType.LBRACE)
- nd = NodeDecl(ref=self._make_node_ref(ref_tok))
- self._parse_node_body(nd)
- self._expect(TokenType.RBRACE)
- actions.append(EmitAction(node_decl=nd, action_type="update", target_ref=nd.ref))
- elif self._at(TokenType.KW_DELETE):
- self._advance()
- ref_tok = self._expect(TokenType.NODE_REF)
- actions.append(EmitAction(action_type="delete", target_ref=self._make_node_ref(ref_tok)))
- elif self._at(TokenType.OP_DOUBLE_COLON):
- # emit :: Type { ... } (anonymous node)
- self._advance()
- type_tok = self._expect(TokenType.IDENTIFIER)
- nd = NodeDecl(type_name=type_tok.value)
- if self._match(TokenType.LBRACE):
- self._parse_node_body(nd)
- self._expect(TokenType.RBRACE)
- actions.append(EmitAction(node_decl=nd, action_type="create"))
- else:
- raise ParseError("Expected node declaration after 'emit'", self._current())
- elif self._at(TokenType.KW_GOTO):
- # goto STATE_NAME (process transition action)
- self._advance()
- state_tok = self._expect(TokenType.IDENTIFIER, "Expected state name after 'goto'")
- actions.append(EmitAction(action_type="goto", goto_state=state_tok.value))
- else:
- raise ParseError("Expected 'emit' or 'goto' in do block", self._current())
-
- self._expect(TokenType.RBRACE)
- return actions
-
- # ── Query declaration ──
-
- def _parse_query_decl(self) -> QueryDecl:
- tok = self._expect(TokenType.KW_QUERY)
- query = QueryDecl(line=tok.line, col=tok.col)
-
- # Optional name
- if self._at(TokenType.IDENTIFIER):
- query.name = self._advance().value
-
- self._expect(TokenType.LBRACE)
-
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- cur = self._current()
- if cur.type in (TokenType.KW_MATCH, TokenType.KW_OPTIONAL):
- query.matches.append(self._parse_match_clause())
- elif cur.type == TokenType.KW_WHERE:
- self._advance()
- query.where_expr = self._parse_expression()
- elif cur.type == TokenType.KW_RETURN:
- query.return_clause = self._parse_return_clause()
- elif cur.type == TokenType.KW_GROUP:
- self._advance() # consume 'group'
- self._expect(TokenType.KW_BY, "Expected 'by' after 'group'")
- # parse comma-separated expressions
- query.group_by.append(self._parse_expression())
- while self._match(TokenType.OP_COMMA):
- query.group_by.append(self._parse_expression())
- else:
- raise ParseError("Unexpected token in query body", cur)
-
- self._expect(TokenType.RBRACE)
- return query
-
- def _parse_match_clause(self) -> MatchClause:
- optional = bool(self._match(TokenType.KW_OPTIONAL))
- self._expect(TokenType.KW_MATCH)
-
- var_tok = self._expect(TokenType.VAR_BIND, "Expected variable binding (?name)")
- mc = MatchClause(
- variable=VarBind(name=var_tok.value, line=var_tok.line, col=var_tok.col),
- optional=optional,
- line=var_tok.line,
- col=var_tok.col,
- )
-
- self._expect(TokenType.OP_DOUBLE_COLON)
- type_tok = self._expect(TokenType.IDENTIFIER)
- mc.type_name = type_tok.value
-
- # Optional edge pattern
- if self._at(TokenType.TYPED_ARROW_START, TokenType.OP_ARROW):
- mc.edge_pattern = self._parse_edge_pattern()
-
- return mc
-
- def _parse_edge_pattern(self) -> EdgePattern:
- ep = EdgePattern(line=self._current().line, col=self._current().col)
-
- if self._match(TokenType.TYPED_ARROW_START):
- # -[T]-> or -[T*]-> or -[T*1..5]->
- type_tok = self._expect(TokenType.IDENTIFIER)
- ep.edge_type = type_tok.value
- # Optional hop quantifier: * or *N or *N..M
- if self._match(TokenType.OP_STAR):
- ep.hop_min = 1
- ep.hop_max = -1 # unbounded by default
- if self._at(TokenType.INT):
- ep.hop_min = int(self._advance().value)
- if self._match(TokenType.OP_RANGE):
- if self._at(TokenType.INT):
- ep.hop_max = int(self._advance().value)
- # else *N.. → N to unbounded (-1)
- else:
- ep.hop_max = ep.hop_min # exact N hops
- self._expect(TokenType.TYPED_ARROW_END)
- elif self._match(TokenType.OP_ARROW):
- ep.edge_type = "relates_to"
- else:
- raise ParseError("Expected edge operator", self._current())
-
- # Target: variable or node ref
- if self._at(TokenType.VAR_BIND):
- var_tok = self._advance()
- ep.target = VarBind(name=var_tok.value, line=var_tok.line, col=var_tok.col)
- if self._match(TokenType.OP_DOUBLE_COLON):
- type_tok = self._expect(TokenType.IDENTIFIER)
- ep.target_type = type_tok.value
- elif self._at(TokenType.NODE_REF):
- ref_tok = self._advance()
- ep.target = self._make_node_ref(ref_tok)
- else:
- raise ParseError("Expected variable or node reference", self._current())
-
- return ep
-
- def _parse_return_clause(self) -> ReturnClause:
- self._expect(TokenType.KW_RETURN)
- rc = ReturnClause(line=self._current().line, col=self._current().col)
- rc.expression = self._parse_expression()
-
- # Optional 'with edges N'
- if self._match(TokenType.KW_WITH):
- self._expect(TokenType.KW_EDGES)
- n_tok = self._expect(TokenType.INT)
- rc.with_edges = int(n_tok.value)
-
- # Optional 'aggregate { ... }'
- if self._match(TokenType.KW_AGGREGATE):
- self._expect(TokenType.LBRACE)
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- name_tok = self._expect(TokenType.IDENTIFIER)
- self._expect(TokenType.OP_ASSIGN)
- func_tok = self._current()
- if func_tok.type not in (
- TokenType.KW_SUM, TokenType.KW_AVG, TokenType.KW_MIN,
- TokenType.KW_MAX, TokenType.KW_COUNT, TokenType.KW_GROUP,
- TokenType.IDENTIFIER,
- ):
- raise ParseError("Expected aggregation function", func_tok)
- func_name = self._advance().value
- self._expect(TokenType.LPAREN)
- expr = self._parse_expression()
- self._expect(TokenType.RPAREN)
- rc.aggregations.append(AggField(name=name_tok.value, func=func_name, expr=expr))
- self._expect(TokenType.RBRACE)
-
- return rc
-
- # ── Process declaration ──
-
- def _parse_process_decl(self) -> ProcessDecl:
- """Parse: process @ref :: TypeName { states... transitions... meta... }"""
- tok = self._expect(TokenType.KW_PROCESS)
- pd = ProcessDecl(line=tok.line, col=tok.col)
-
- ref_tok = self._expect(TokenType.NODE_REF, "Expected node reference (@name)")
- pd.ref = self._make_node_ref(ref_tok)
-
- self._expect(TokenType.OP_DOUBLE_COLON, "Expected '::'")
-
- type_tok = self._expect(TokenType.IDENTIFIER, "Expected type name")
- pd.type_name = type_tok.value
-
- self._expect(TokenType.LBRACE, "Expected '{'")
-
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- cur = self._current()
- if cur.type == TokenType.KW_STATE:
- pd.states.append(self._parse_state_decl())
- elif cur.type == TokenType.KW_ON:
- pd.transitions.append(self._parse_transition_decl())
- elif cur.type == TokenType.META_KEY:
- pd.meta.append(self._parse_meta_annotation())
- else:
- raise ParseError("Unexpected token in process body", cur)
-
- self._expect(TokenType.RBRACE, "Expected '}'")
- return pd
-
- def _parse_state_decl(self) -> StateDecl:
- """Parse: state NAME { meta... }"""
- self._expect(TokenType.KW_STATE)
- name_tok = self._expect(TokenType.IDENTIFIER, "Expected state name")
- sd = StateDecl(name=name_tok.value, line=name_tok.line, col=name_tok.col)
-
- if self._match(TokenType.LBRACE):
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- if self._at(TokenType.META_KEY):
- sd.meta.append(self._parse_meta_annotation())
- else:
- raise ParseError("Unexpected token in state body", self._current())
- self._expect(TokenType.RBRACE)
-
- return sd
-
- def _parse_transition_decl(self) -> TransitionDecl:
- """Parse: on EVENT in FROM_STATE -> TO_STATE [where EXPR] [do { actions }] [compensate { actions }]"""
- self._expect(TokenType.KW_ON)
- event_tok = self._expect(TokenType.IDENTIFIER, "Expected event name")
- self._expect(TokenType.KW_IN, "Expected 'in'")
- from_tok = self._expect(TokenType.IDENTIFIER, "Expected from-state name")
- self._expect(TokenType.OP_ARROW, "Expected '->'")
- to_tok = self._expect(TokenType.IDENTIFIER, "Expected to-state name")
-
- td = TransitionDecl(
- event=event_tok.value,
- from_state=from_tok.value,
- to_state=to_tok.value,
- line=event_tok.line,
- col=event_tok.col,
- )
-
- # Optional where
- if self._match(TokenType.KW_WHERE):
- td.where_expr = self._parse_expression()
-
- # Optional do { actions }
- if self._at(TokenType.KW_DO):
- td.actions = self._parse_do_clause()
-
- # Optional compensate { actions }
- if self._match(TokenType.KW_COMPENSATE):
- self._expect(TokenType.LBRACE)
- compensate_actions: list[EmitAction] = []
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- if self._at(TokenType.KW_EMIT):
- self._advance()
- if self._at(TokenType.KW_NODE):
- nd = self._parse_node_decl()
- compensate_actions.append(EmitAction(node_decl=nd, action_type="create"))
- elif self._at(TokenType.OP_DOUBLE_COLON):
- self._advance()
- type_tok = self._expect(TokenType.IDENTIFIER)
- nd = NodeDecl(type_name=type_tok.value)
- if self._match(TokenType.LBRACE):
- self._parse_node_body(nd)
- self._expect(TokenType.RBRACE)
- compensate_actions.append(EmitAction(node_decl=nd, action_type="create"))
- else:
- raise ParseError("Expected node declaration after 'emit'", self._current())
- else:
- raise ParseError("Expected 'emit' in compensate block", self._current())
- self._expect(TokenType.RBRACE)
- td.compensate_actions = compensate_actions
-
- return td
-
- # ── Import / Export ──
-
- def _parse_import_decl(self) -> ImportDecl:
- tok = self._expect(TokenType.KW_IMPORT)
- imp = ImportDecl(line=tok.line, col=tok.col)
-
- self._expect(TokenType.LBRACE)
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- name_tok = self._expect(TokenType.IDENTIFIER)
- imp.names.append(name_tok.value)
- self._match(TokenType.OP_COMMA)
- self._expect(TokenType.RBRACE)
-
- self._expect(TokenType.KW_FROM)
- src_tok = self._expect(TokenType.STRING)
- imp.source = src_tok.value
-
- return imp
-
- def _parse_export_decl(self) -> ExportDecl:
- tok = self._expect(TokenType.KW_EXPORT)
- cur = self._current()
- if cur.type == TokenType.KW_NODE:
- decl: ASTNode = self._parse_node_decl()
- elif cur.type == TokenType.KW_TYPE:
- decl = self._parse_type_decl()
- elif cur.type == TokenType.KW_CONTEXT:
- decl = self._parse_context_decl()
- elif cur.type == TokenType.KW_INTENT:
- decl = self._parse_intent_decl()
- else:
- raise ParseError("Expected declaration after 'export'", cur)
-
- return ExportDecl(declaration=decl, line=tok.line, col=tok.col)
-
- # ── Expression parser (Pratt / precedence climbing) ──
-
- def _parse_expression(self) -> ASTNode:
- return self._parse_or()
-
- def _parse_or(self) -> ASTNode:
- left = self._parse_and()
- while self._at(TokenType.OP_LOGICAL_OR, TokenType.KW_OR):
- op = self._advance()
- right = self._parse_and()
- left = BinaryOp(left=left, op="||", right=right, line=op.line, col=op.col)
- return left
-
- def _parse_and(self) -> ASTNode:
- left = self._parse_equality()
- while self._at(TokenType.OP_LOGICAL_AND, TokenType.KW_AND):
- op = self._advance()
- right = self._parse_equality()
- left = BinaryOp(left=left, op="&&", right=right, line=op.line, col=op.col)
- return left
-
- def _parse_equality(self) -> ASTNode:
- left = self._parse_comparison()
- while self._at(TokenType.OP_EQ, TokenType.OP_NEQ):
- op = self._advance()
- right = self._parse_comparison()
- left = BinaryOp(left=left, op=op.value, right=right, line=op.line, col=op.col)
- return left
-
- def _parse_comparison(self) -> ASTNode:
- left = self._parse_set_ops()
- while self._at(TokenType.OP_GT, TokenType.OP_LT, TokenType.OP_GTE, TokenType.OP_LTE):
- op = self._advance()
- right = self._parse_set_ops()
- left = BinaryOp(left=left, op=op.value, right=right, line=op.line, col=op.col)
- return left
-
- def _parse_set_ops(self) -> ASTNode:
- left = self._parse_addition()
- while self._at(TokenType.KW_IN, TokenType.KW_OVERLAPS, TokenType.KW_WITHIN, TokenType.KW_MATCHES):
- op = self._advance()
- right = self._parse_addition()
- left = BinaryOp(left=left, op=op.value, right=right, line=op.line, col=op.col)
- return left
-
- def _parse_addition(self) -> ASTNode:
- left = self._parse_multiplication()
- while self._at(TokenType.OP_PLUS, TokenType.OP_MINUS):
- op = self._advance()
- right = self._parse_multiplication()
- left = BinaryOp(left=left, op=op.value, right=right, line=op.line, col=op.col)
- return left
-
- def _parse_multiplication(self) -> ASTNode:
- left = self._parse_unary()
- while self._at(TokenType.OP_STAR, TokenType.OP_SLASH, TokenType.OP_PERCENT):
- op = self._advance()
- right = self._parse_unary()
- left = BinaryOp(left=left, op=op.value, right=right, line=op.line, col=op.col)
- return left
-
- def _parse_unary(self) -> ASTNode:
- if self._at(TokenType.KW_NOT):
- op = self._advance()
- operand = self._parse_unary()
- return UnaryOp(op="not", operand=operand, line=op.line, col=op.col)
- if self._at(TokenType.OP_MINUS):
- op = self._advance()
- operand = self._parse_unary()
- return UnaryOp(op="-", operand=operand, line=op.line, col=op.col)
- return self._parse_postfix()
-
- def _parse_postfix(self) -> ASTNode:
- expr = self._parse_primary()
-
- while True:
- if self._at(TokenType.OP_DOT):
- self._advance()
- if self._at(TokenType.META_KEY):
- # .~confidence etc.
- tok = self._advance()
- expr = FieldAccess(target=expr, field_name=f"~{tok.value}", line=tok.line, col=tok.col)
- else:
- field_tok = self._expect(TokenType.IDENTIFIER, "Expected field name after '.'")
- expr = FieldAccess(target=expr, field_name=field_tok.value, line=field_tok.line, col=field_tok.col)
- elif self._at(TokenType.LBRACKET):
- self._advance()
- index = self._parse_expression()
- self._expect(TokenType.RBRACKET)
- expr = IndexAccess(target=expr, index=index)
- else:
- break
-
- return expr
-
- def _try_quantity_unit(self) -> str | None:
- """If the next token is a recognised unit atom (and not a field name),
- consume it and return the full unit expression string; else return None."""
- if not self._at(TokenType.IDENTIFIER):
- return None
- unit_val = self._current().value
- if unit_val not in _UNIT_ATOMS:
- return None
- # Don't consume if followed by '=' / '->' / '::' / ':' — that means
- # this identifier is a field name, not a unit.
- nxt = self._peek(1).type
- if nxt in (TokenType.OP_ASSIGN, TokenType.OP_ARROW,
- TokenType.OP_DOUBLE_COLON, TokenType.OP_COLON,
- TokenType.OP_PLUS_ASSIGN):
- return None
- self._advance() # consume unit atom
- unit_expr = unit_val
- # Handle composite units: m/s, kg*m, m/s^2 (^ not yet a token, skip for now)
- while self._at(TokenType.OP_SLASH, TokenType.OP_STAR):
- op_tok = self._advance()
- if self._at(TokenType.IDENTIFIER):
- unit_expr += op_tok.value + self._advance().value
- else:
- break
- return unit_expr
-
- def _parse_primary(self) -> ASTNode:
- tok = self._current()
-
- # Bytes literal: b"..."
- if tok.type == TokenType.BYTES:
- self._advance()
- return Literal(value=tok.value, kind="bytes", line=tok.line, col=tok.col)
-
- # Vector literal: v[f, f, ...]
- if tok.type == TokenType.VECTOR:
- self._advance()
- floats = [float(s.strip()) for s in tok.value.split(",") if s.strip()]
- return Literal(value=floats, kind="vector", line=tok.line, col=tok.col)
-
- # UUID literal: u"..."
- if tok.type == TokenType.UUID:
- self._advance()
- return Literal(value=tok.value, kind="uuid", line=tok.line, col=tok.col)
-
- # Literals
- if tok.type == TokenType.INT:
- self._advance()
- mag = int(tok.value)
- unit = self._try_quantity_unit()
- if unit:
- return Literal(value={"magnitude": mag, "unit": unit},
- kind="quantity", line=tok.line, col=tok.col)
- return Literal(value=mag, kind="int", line=tok.line, col=tok.col)
-
- if tok.type == TokenType.FLOAT:
- self._advance()
- fmag = float(tok.value)
- unit = self._try_quantity_unit()
- if unit:
- return Literal(value={"magnitude": fmag, "unit": unit},
- kind="quantity", line=tok.line, col=tok.col)
- return Literal(value=fmag, kind="float", line=tok.line, col=tok.col)
-
- if tok.type == TokenType.DECIMAL:
- self._advance()
- raw = tok.value.rstrip("d")
- decimal_val = float(raw)
- # Money: DECIMAL followed by an ISO 4217-style code (2–4 uppercase letters)
- if self._at(TokenType.IDENTIFIER):
- code = self._current().value
- if code.isupper() and code.isalpha() and 2 <= len(code) <= 4:
- self._advance()
- return Literal(value={"amount": decimal_val, "currency": code},
- kind="money", line=tok.line, col=tok.col)
- return Literal(value=decimal_val, kind="decimal", line=tok.line, col=tok.col)
-
- if tok.type == TokenType.STRING:
- self._advance()
- return Literal(value=tok.value, kind="string", line=tok.line, col=tok.col)
-
- if tok.type == TokenType.BOOL:
- self._advance()
- return Literal(value=(tok.value == "true"), kind="bool", line=tok.line, col=tok.col)
-
- if tok.type == TokenType.NULL:
- self._advance()
- return Literal(value=None, kind="null", line=tok.line, col=tok.col)
-
- if tok.type == TokenType.DURATION:
- self._advance()
- return Literal(value=tok.value, kind="duration", line=tok.line, col=tok.col)
-
- if tok.type == TokenType.DATETIME:
- self._advance()
- return Literal(value=tok.value, kind="datetime", line=tok.line, col=tok.col)
-
- # Star (wildcard, used in ranges like .. *)
- if tok.type == TokenType.OP_STAR:
- self._advance()
- return Literal(value="*", kind="string", line=tok.line, col=tok.col)
-
- # now keyword
- if tok.type == TokenType.KW_NOW:
- self._advance()
- return Literal(value="now", kind="datetime", line=tok.line, col=tok.col)
-
- # last keyword (e.g. 'last 30d')
- if tok.type == TokenType.KW_LAST:
- self._advance()
- dur = self._parse_expression()
- return FuncCall(name="last", args=[dur], line=tok.line, col=tok.col)
-
- # Node references
- if tok.type == TokenType.NODE_REF:
- self._advance()
- return self._make_node_ref(tok)
-
- # Variable bindings
- if tok.type == TokenType.VAR_BIND:
- self._advance()
- return VarBind(name=tok.value, line=tok.line, col=tok.col)
-
- # Parenthesized expression
- if tok.type == TokenType.LPAREN:
- self._advance()
- expr = self._parse_expression()
- self._expect(TokenType.RPAREN)
- return expr
-
- # Array literal
- if tok.type == TokenType.LBRACKET:
- return self._parse_array_literal()
-
- if tok.type == TokenType.MAP_OPEN:
- return self._parse_hash_map_literal()
-
- # Map literal (or anonymous struct — context determines) with { ... }
- if tok.type == TokenType.LBRACE:
- return self._parse_map_or_struct()
-
- # Function call, named struct, or bare identifier
- if tok.type == TokenType.IDENTIFIER:
- self._advance()
- if self._at(TokenType.LPAREN):
- self._advance()
- args = []
- while not self._at(TokenType.RPAREN, TokenType.EOF):
- args.append(self._parse_expression())
- if not self._match(TokenType.OP_COMMA):
- break
- self._expect(TokenType.RPAREN)
- return FuncCall(name=tok.value, args=args, line=tok.line, col=tok.col)
- # Named struct literal: TypeName { key = val, ... }
- # Used for ~uncertainty gaussian { ... } and similar compound values
- if self._at(TokenType.LBRACE):
- self._advance() # consume {
- pairs: list[tuple[ASTNode, ASTNode]] = [
- (Literal(value="_type", kind="string"), Literal(value=tok.value, kind="string"))
- ]
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- k_tok = self._expect(TokenType.IDENTIFIER)
- self._expect(TokenType.OP_ASSIGN)
- v = self._parse_expression()
- pairs.append((Literal(value=k_tok.value, kind="string"), v))
- self._match(TokenType.OP_COMMA)
- self._expect(TokenType.RBRACE)
- return MapLiteral(pairs=pairs, line=tok.line, col=tok.col)
- return Literal(value=tok.value, kind="string", line=tok.line, col=tok.col)
-
- # Aggregation keywords used as identifiers in some contexts
- if tok.type in (TokenType.KW_SUM, TokenType.KW_AVG, TokenType.KW_MIN,
- TokenType.KW_MAX, TokenType.KW_COUNT, TokenType.KW_GROUP):
- self._advance()
- if self._at(TokenType.LPAREN):
- self._advance()
- args = []
- while not self._at(TokenType.RPAREN, TokenType.EOF):
- args.append(self._parse_expression())
- if not self._match(TokenType.OP_COMMA):
- break
- self._expect(TokenType.RPAREN)
- return FuncCall(name=tok.value, args=args, line=tok.line, col=tok.col)
- return Literal(value=tok.value, kind="string", line=tok.line, col=tok.col)
-
- raise ParseError("Expected expression", tok)
-
- def _parse_array_literal(self) -> ArrayLiteral:
- tok = self._expect(TokenType.LBRACKET)
- elements = []
- while not self._at(TokenType.RBRACKET, TokenType.EOF):
- elements.append(self._parse_expression())
- if not self._match(TokenType.OP_COMMA):
- break
- self._expect(TokenType.RBRACKET)
- return ArrayLiteral(elements=elements, line=tok.line, col=tok.col)
-
- def _parse_hash_map_literal(self) -> MapLiteral:
- """Parse a map literal: #{ key: value, ... }"""
- tok = self._expect(TokenType.MAP_OPEN)
- pairs = []
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- key = self._parse_expression()
- if self._match(TokenType.OP_COLON):
- val = self._parse_expression()
- pairs.append((key, val))
- elif self._match(TokenType.OP_ASSIGN):
- val = self._parse_expression()
- pairs.append((key, val))
- else:
- pairs.append((key, Literal(value=True, kind="bool")))
- self._match(TokenType.OP_COMMA)
- self._expect(TokenType.RBRACE)
- return MapLiteral(pairs=pairs, line=tok.line, col=tok.col)
-
- def _parse_map_or_struct(self) -> MapLiteral:
- tok = self._expect(TokenType.LBRACE)
- pairs = []
- while not self._at(TokenType.RBRACE, TokenType.EOF):
- key = self._parse_expression()
- if self._match(TokenType.OP_COLON):
- val = self._parse_expression()
- pairs.append((key, val))
- elif self._match(TokenType.OP_ASSIGN):
- val = self._parse_expression()
- pairs.append((key, val))
- else:
- pairs.append((key, Literal(value=True, kind="bool")))
- self._match(TokenType.OP_COMMA)
- self._expect(TokenType.RBRACE)
- return MapLiteral(pairs=pairs, line=tok.line, col=tok.col)
-
- # ── Helpers ──
-
- def _make_node_ref(self, tok: Token) -> NodeRef:
- raw = tok.value.lstrip("@")
- parts = raw.split(".")
- return NodeRef(path=parts, line=tok.line, col=tok.col)
diff --git a/packages/python/src/kndl/py.typed b/packages/python/src/kndl/py.typed
deleted file mode 100644
index e69de29..0000000
diff --git a/packages/python/src/kndl/serializer.py b/packages/python/src/kndl/serializer.py
deleted file mode 100644
index 7660c74..0000000
--- a/packages/python/src/kndl/serializer.py
+++ /dev/null
@@ -1,195 +0,0 @@
-"""
-KNDL Serializer — Converts a KNDLGraph back to KNDL text format.
-
-This enables round-tripping: parse KNDL → graph → KNDL text.
-"""
-
-from __future__ import annotations
-
-from typing import Any
-
-from .graph import KNDLGraph, GraphNode, GraphEdge, GraphIntent, KNDLMeta
-
-
-def _format_value(val: Any) -> str:
- """Format a Python value as a KNDL literal."""
- if val is None:
- return "null"
- if isinstance(val, bool):
- return "true" if val else "false"
- if isinstance(val, int):
- return str(val)
- if isinstance(val, float):
- return str(val)
- if isinstance(val, str):
- if val.startswith("@"):
- return val
- return f'"{val}"'
- if isinstance(val, list):
- # Vector: homogeneous list of numbers
- if val and all(isinstance(v, (int, float)) for v in val):
- items = ", ".join(str(v) for v in val)
- return f"v[ {items} ]"
- items = ", ".join(_format_value(v) for v in val)
- return f"[ {items} ]"
- if isinstance(val, dict):
- if "currency" in val and "amount" in val: # Money
- return f'{val["amount"]}d {val["currency"]}'
- if "unit" in val and "magnitude" in val: # Quantity
- return f'{val["magnitude"]} {val["unit"]}'
- pairs = ", ".join(f'"{k}": {_format_value(v)}' for k, v in val.items())
- return f"#{{ {pairs} }}"
- return f'"{val}"'
-
-
-def _seconds_to_duration(s: float) -> str:
- """Convert seconds to a KNDL duration string."""
- if s < 1:
- return f"{int(s * 1000)}ms"
- if s < 60:
- return f"{int(s)}s"
- if s < 3600:
- return f"{int(s / 60)}m"
- if s < 86400:
- return f"{int(s / 3600)}h"
- if s < 604800:
- return f"{int(s / 86400)}d"
- return f"{int(s / 604800)}w"
-
-
-def _serialize_meta(meta: KNDLMeta, indent: str = " ") -> list[str]:
- """Serialize meta-annotations to KNDL lines."""
- lines = []
-
- if meta.confidence != 1.0:
- lines.append(f"{indent}~confidence {meta.confidence}")
- if meta.source:
- lines.append(f'{indent}~source "{meta.source}"')
- if meta.valid_start:
- end = meta.valid_end if meta.valid_end else "*"
- lines.append(f"{indent}~valid {meta.valid_start} .. {end}")
- if meta.decay_rate is not None and meta.decay_duration_seconds is not None:
- dur = _seconds_to_duration(meta.decay_duration_seconds)
- lines.append(f"{indent}~decay {meta.decay_rate} / {dur}")
- if meta.supersedes:
- lines.append(f"{indent}~supersedes {meta.supersedes}")
- if meta.derived_from:
- refs = ", ".join(meta.derived_from)
- lines.append(f"{indent}~derived [ {refs} ]")
- if meta.access:
- lines.append(f'{indent}~access "{meta.access}"')
- if meta.priority != 0.5:
- lines.append(f"{indent}~priority {meta.priority}")
- if meta.cooldown_seconds:
- dur = _seconds_to_duration(meta.cooldown_seconds)
- lines.append(f"{indent}~cooldown {dur}")
- if meta.tags:
- tags = ", ".join(f'"{t}"' for t in meta.tags)
- lines.append(f"{indent}~tags [ {tags} ]")
- if meta.recorded:
- lines.append(f'{indent}~recorded "{meta.recorded}"')
- if meta.observed:
- lines.append(f'{indent}~observed "{meta.observed}"')
- if meta.negated:
- lines.append(f"{indent}~negated true")
- if meta.deadline:
- lines.append(f'{indent}~deadline "{meta.deadline}"')
- if meta.classification:
- lines.append(f'{indent}~classification "{meta.classification}"')
- if meta.uncertainty is not None:
- dist_type = meta.uncertainty.get("_type", "")
- if dist_type:
- params = {k: v for k, v in meta.uncertainty.items() if k != "_type"}
- pairs = ", ".join(f"{k} = {_format_value(v)}" for k, v in params.items())
- lines.append(f"{indent}~uncertainty {dist_type} {{ {pairs} }}")
- else:
- lines.append(f"{indent}~uncertainty {_format_value(meta.uncertainty)}")
- for k, v in meta.custom.items():
- lines.append(f"{indent}~{k} {_format_value(v)}")
-
- return lines
-
-
-class Serializer:
- """
- Serializes a KNDLGraph back to KNDL text format.
-
- Usage:
- serializer = Serializer()
- text = serializer.serialize(graph)
- """
-
- def serialize(self, graph: KNDLGraph) -> str:
- """Serialize the entire graph to KNDL text."""
- parts: list[str] = []
-
- # Nodes
- for node in graph.nodes.values():
- parts.append(self._serialize_node(node, graph))
- parts.append("")
-
- # Standalone edges (not inline)
- for edge in graph.edges.values():
- parts.append(self._serialize_edge(edge))
- parts.append("")
-
- # Intents
- for intent in graph.intents.values():
- parts.append(self._serialize_intent(intent))
- parts.append("")
-
- return "\n".join(parts).strip() + "\n"
-
- def _serialize_node(self, node: GraphNode, _graph: KNDLGraph) -> str:
- lines = [f"node @{node.id} :: {node.type_name} {{"]
-
- for k, v in node.fields.items():
- lines.append(f" {k:<8} = {_format_value(v)}")
-
- # Edges are emitted as standalone declarations in serialize(), not inline,
- # to avoid duplication on roundtrip.
-
- lines.extend(_serialize_meta(node.meta))
- lines.append("}")
- return "\n".join(lines)
-
- def _serialize_edge(self, edge: GraphEdge) -> str:
- header = f"edge @{edge.source_id} -[{edge.edge_type}]-> @{edge.target_id}"
-
- if not edge.fields and not edge.meta.to_dict():
- return header
-
- lines = [f"{header} {{"]
- for k, v in edge.fields.items():
- lines.append(f" {k:<8} = {_format_value(v)}")
- lines.extend(_serialize_meta(edge.meta))
- lines.append("}")
- return "\n".join(lines)
-
- def _serialize_intent(self, intent: GraphIntent) -> str:
- lines = [f"intent @{intent.id} :: {intent.type_name} {{"]
-
- if intent.trigger_kind == "cron":
- lines.append(f' trigger = cron "{intent.trigger_data}"')
- elif intent.trigger_data:
- lines.append(f" trigger = {intent.trigger_data}")
-
- if intent.actions:
- lines.append(" do {")
- for action in intent.actions:
- atype = action.get("type", "create")
- ntype = action.get("node_type", "Node")
- fields = action.get("fields", {})
- if atype == "create":
- lines.append(f" emit :: {ntype} {{")
- for k, v in fields.items():
- lines.append(f" {k:<8} = {_format_value(v)}")
- lines.append(" }")
- elif atype == "delete":
- target = action.get("target", "")
- lines.append(f" emit delete {target}")
- lines.append(" }")
-
- lines.extend(_serialize_meta(intent.meta))
- lines.append("}")
- return "\n".join(lines)
diff --git a/packages/python/src/kndl/storage.py b/packages/python/src/kndl/storage.py
deleted file mode 100644
index c9b4994..0000000
--- a/packages/python/src/kndl/storage.py
+++ /dev/null
@@ -1,85 +0,0 @@
-"""
-KNDL Storage — Protocol + factory for pluggable persistence backends.
-
-Configure via DATABASE_URL environment variable (or a .env file):
- (unset / "memory") → in-memory only, no persistence
- sqlite:///./kndl.db → SQLite (default, zero deps)
- sqlite:///:memory: → SQLite in-memory (useful for tests)
- postgresql://user:pw@host/db → PostgreSQL with JSONB (requires psycopg2)
-
-Example .env:
- DATABASE_URL=sqlite:///./kndl.db
- # DATABASE_URL=postgresql://kndl:secret@localhost:5432/kndl
-"""
-
-from __future__ import annotations
-
-import os
-from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
-
-if TYPE_CHECKING:
- from kndl.graph import GraphEdge, GraphIntent, GraphNode
-
-
-def _load_dotenv() -> None:
- """Load .env from the current working directory (if python-dotenv is installed)."""
- try:
- from dotenv import load_dotenv
- load_dotenv(override=False) # existing env vars take precedence
- except ImportError:
- pass # python-dotenv is optional
-
-
-@runtime_checkable
-class KNDLStorage(Protocol):
- """Pluggable persistence backend for KNDLGraph.
-
- Implementations must be safe to call from a single thread.
- All write methods should persist immediately (commit on each call).
- """
-
- def load(
- self,
- ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
- """Return (node_dicts, edge_dicts, intent_dicts) to warm an empty graph."""
- ...
-
- def upsert_node(self, node: "GraphNode") -> None: ...
- def delete_node(self, node_id: str) -> None: ...
- def upsert_edge(self, edge: "GraphEdge") -> None: ...
- def delete_edge(self, edge_id: str) -> None: ...
- def upsert_intent(self, intent: "GraphIntent") -> None: ...
- def delete_intent(self, intent_id: str) -> None: ...
- def clear(self) -> None: ...
- def close(self) -> None: ...
-
-
-def create_storage(database_url: str | None = None) -> "KNDLStorage | None":
- """
- Instantiate a storage backend from a DATABASE_URL string.
-
- Returns None (no persistence) when DATABASE_URL is absent or "memory".
- Falls back to DATABASE_URL env var when *database_url* is not provided.
- Automatically reads a .env file in the current directory if python-dotenv
- is installed and DATABASE_URL is not already set in the environment.
- """
- if database_url is None:
- _load_dotenv()
- database_url = os.environ.get("DATABASE_URL", "")
-
- url = database_url
- if not url or url.lower() in ("memory", "none", ""):
- return None
-
- if url.startswith("sqlite"):
- from kndl.backends.sqlite_backend import SQLiteStorage
- return SQLiteStorage(url)
-
- if url.startswith("postgresql") or url.startswith("postgres"):
- from kndl.backends.postgres_backend import PostgresStorage
- return PostgresStorage(url)
-
- raise ValueError(
- f"Unsupported DATABASE_URL: {url!r}\n"
- "Supported schemes: sqlite:///, postgresql://"
- )
diff --git a/packages/python/tests/test_advanced_types.py b/packages/python/tests/test_advanced_types.py
deleted file mode 100644
index 531f7a3..0000000
--- a/packages/python/tests/test_advanced_types.py
+++ /dev/null
@@ -1,376 +0,0 @@
-"""Tests for newly implemented KNDL v0.2 features (second wave)."""
-import sys
-import os
-import pytest
-
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
-
-from kndl import parse, compile, serialize
-
-
-# ─────────────────────────────────────────────────────────────────────────────
-# §3: Parameterised types Type
-# ─────────────────────────────────────────────────────────────────────────────
-
-class TestParameterisedTypes:
- def test_simple_param(self):
- """type Temperature = Quantity<°C> parses correctly."""
- src = 'type Temperature = Quantity<°C>'
- prog = parse(src)
- te = prog.types[0].type_expr
- assert te.kind == "parameterised"
- assert te.name == "Quantity"
- assert len(te.params) == 1
- assert te.params[0].name == "°C"
-
- def test_string_param(self):
- """type ICD10Code = Code<"ICD-10"> parses string param."""
- src = 'type ICD10Code = Code<"ICD-10">'
- prog = parse(src)
- te = prog.types[0].type_expr
- assert te.kind == "parameterised"
- assert te.name == "Code"
- assert te.params[0].kind == "literal"
- assert te.params[0].name == "ICD-10"
-
- def test_multi_params(self):
- """Localized parses two params."""
- src = 'type LocalStr = Localized'
- prog = parse(src)
- te = prog.types[0].type_expr
- assert te.kind == "parameterised"
- assert len(te.params) == 2
- assert te.params[0].name == "string"
- assert te.params[1].name == "en"
-
- def test_nested_param(self):
- """Distribution — nested parameterised type."""
- src = 'type DistNode = Distribution'
- prog = parse(src)
- te = prog.types[0].type_expr
- assert te.kind == "parameterised"
- assert te.params[0].name == "Gaussian"
-
- def test_optional_param(self):
- """Vector? — optional parameterised type."""
- src = 'type MaybeVec = Vector?'
- prog = parse(src)
- te = prog.types[0].type_expr
- assert te.kind == "optional"
- inner = te.children[0]
- assert inner.kind == "parameterised"
- assert inner.name == "Vector"
-
- def test_compile_type_with_param(self):
- """type decl with param compiles without error."""
- src = 'type Reading = Quantity<°C>'
- g = compile(src)
- assert "Reading" in g.types
-
- def test_param_in_field_decl(self):
- """Field declarations with parameterised types parse."""
- src = """type Sensor {
- reading: Quantity<°C>
- label: Localized
- }"""
- prog = parse(src)
- fields = prog.types[0].fields
- assert fields[0].type_expr.kind == "parameterised"
- assert fields[0].type_expr.name == "Quantity"
- assert fields[1].type_expr.kind == "parameterised"
- assert fields[1].type_expr.name == "Localized"
-
-
-# ─────────────────────────────────────────────────────────────────────────────
-# §5.2: Multi-hop path patterns -[T*1..5]->
-# ─────────────────────────────────────────────────────────────────────────────
-
-class TestMultiHopPatterns:
- def test_unbounded_star(self):
- """-[knows*]-> means 1..∞ hops."""
- src = """query { match ?a :: Person -[knows*]-> ?b :: Person return ?b }"""
- prog = parse(src)
- ep = prog.queries[0].matches[0].edge_pattern
- assert ep.edge_type == "knows"
- assert ep.hop_min == 1
- assert ep.hop_max == -1
-
- def test_exact_hops(self):
- """-[knows*2]-> means exactly 2 hops."""
- src = """query { match ?a :: Person -[knows*2]-> ?b :: Person return ?b }"""
- prog = parse(src)
- ep = prog.queries[0].matches[0].edge_pattern
- assert ep.hop_min == 2
- assert ep.hop_max == 2
-
- def test_range_hops(self):
- """-[knows*1..5]-> means 1 to 5 hops."""
- src = """query { match ?a :: Person -[knows*1..5]-> ?b :: Person return ?b }"""
- prog = parse(src)
- ep = prog.queries[0].matches[0].edge_pattern
- assert ep.hop_min == 1
- assert ep.hop_max == 5
-
- def test_lower_bound_only(self):
- """-[knows*2..]-> means 2 to unbounded."""
- src = """query { match ?a :: Person -[knows*2..]-> ?b :: Person return ?b }"""
- prog = parse(src)
- ep = prog.queries[0].matches[0].edge_pattern
- assert ep.hop_min == 2
- assert ep.hop_max == -1
-
- def test_single_hop_default(self):
- """-[knows]-> without * means 1 hop."""
- src = """query { match ?a :: Person -[knows]-> ?b :: Person return ?b }"""
- prog = parse(src)
- ep = prog.queries[0].matches[0].edge_pattern
- assert ep.hop_min == 1
- assert ep.hop_max == 1
-
- def test_multi_hop_with_type(self):
- """-[located_in*1..3]-> with meaningful edge type."""
- src = """query { match ?place :: Location -[located_in*1..3]-> ?region :: Region return ?region }"""
- prog = parse(src)
- ep = prog.queries[0].matches[0].edge_pattern
- assert ep.edge_type == "located_in"
- assert ep.hop_min == 1
- assert ep.hop_max == 3
-
-
-# ─────────────────────────────────────────────────────────────────────────────
-# Undirected typed edge -[T]-
-# ─────────────────────────────────────────────────────────────────────────────
-
-class TestUndirectedEdge:
- def test_undirected_edge_parses(self):
- """edge @a -[related_to]- @b is undirected."""
- src = "edge @a -[related_to]- @b"
- prog = parse(src)
- e = prog.edges[0]
- assert e.edge_type == "related_to"
- assert e.direction == "undirected"
-
- def test_undirected_edge_compiles(self):
- """Undirected edge compiles to a GraphEdge with direction='undirected'."""
- src = """
- node @a :: T {}
- node @b :: T {}
- edge @a -[peer_of]- @b
- """
- g = compile(src)
- edges = list(g.edges.values())
- assert any(e.direction == "undirected" and e.edge_type == "peer_of" for e in edges)
-
- def test_undirected_vs_forward(self):
- """Undirected -[T]- is distinct from forward -[T]->."""
- src = """
- node @a :: T {}
- node @b :: T {}
- edge @a -[connected]- @b
- edge @a -[links_to]-> @b
- """
- g = compile(src)
- edges = list(g.edges.values())
- directions = {e.edge_type: e.direction for e in edges}
- assert directions["connected"] == "undirected"
- assert directions["links_to"] == "forward"
-
- def test_undirected_with_body(self):
- """Undirected edge with meta block."""
- src = """
- node @a :: T {}
- node @b :: T {}
- edge @a -[peer_of]- @b {
- ~confidence 0.9
- }
- """
- g = compile(src)
- edges = list(g.edges.values())
- e = next(e for e in edges if e.edge_type == "peer_of")
- assert e.direction == "undirected"
- assert e.meta.confidence == pytest.approx(0.9)
-
-
-# ─────────────────────────────────────────────────────────────────────────────
-# Named struct literal TypeName { key = value }
-# (used for ~uncertainty and other compound meta values)
-# ─────────────────────────────────────────────────────────────────────────────
-
-class TestNamedStructLiteral:
- def test_uncertainty_gaussian_parses(self):
- """~uncertainty gaussian { mean = 0.5, std = 0.1 } parses as MapLiteral."""
- from kndl.ast_nodes import MapLiteral
- src = """node @n :: T {
- ~uncertainty gaussian { mean = 0.5, std = 0.1 }
- }"""
- prog = parse(src)
- meta = prog.nodes[0].meta[0]
- assert meta.key == "uncertainty"
- assert isinstance(meta.value, MapLiteral)
-
- def test_uncertainty_gaussian_compile(self):
- """~uncertainty gaussian { ... } compiles to KNDLMeta.uncertainty dict."""
- src = """node @n :: T {
- ~uncertainty gaussian { mean = 0.5, std = 0.1 }
- }"""
- g = compile(src)
- u = g.nodes["n"].meta.uncertainty
- assert u is not None
- assert u.get("_type") == "gaussian"
- assert u.get("mean") == pytest.approx(0.5)
- assert u.get("std") == pytest.approx(0.1)
-
- def test_uncertainty_interval(self):
- """~uncertainty interval { low = 0.0, high = 1.0 } compiles."""
- src = """node @n :: T {
- ~uncertainty interval { low = 0.0, high = 1.0 }
- }"""
- g = compile(src)
- u = g.nodes["n"].meta.uncertainty
- assert u["_type"] == "interval"
- assert u["low"] == pytest.approx(0.0)
- assert u["high"] == pytest.approx(1.0)
-
- def test_uncertainty_categorical(self):
- """~uncertainty categorical { A = 0.3, B = 0.7 } compiles."""
- src = """node @n :: T {
- ~uncertainty categorical { A = 0.3, B = 0.7 }
- }"""
- g = compile(src)
- u = g.nodes["n"].meta.uncertainty
- assert u["_type"] == "categorical"
- assert u["A"] == pytest.approx(0.3)
- assert u["B"] == pytest.approx(0.7)
-
- def test_uncertainty_serializes(self):
- """~uncertainty gaussian block round-trips through the serializer."""
- src = """node @n :: T {
- ~uncertainty gaussian { mean = 0.5, std = 0.1 }
- }"""
- g = compile(src)
- text = serialize(g)
- assert "~uncertainty" in text
- assert "gaussian" in text
-
- def test_named_struct_in_field(self):
- """TypeName { ... } in a field value compiles to a dict."""
- src = """node @n :: T {
- dist = Gaussian { mean = 0.0, std = 1.0 }
- }"""
- g = compile(src)
- d = g.nodes["n"].fields["dist"]
- assert isinstance(d, dict)
- assert d.get("_type") == "Gaussian"
- assert d.get("mean") == pytest.approx(0.0)
-
-
-# ─────────────────────────────────────────────────────────────────────────────
-# §6: goto action in process transitions
-# ─────────────────────────────────────────────────────────────────────────────
-
-class TestGotoAction:
- def test_goto_parses(self):
- """goto STATE_NAME parses as EmitAction with action_type='goto'."""
- src = """process @order :: OrderProcess {
- state PENDING {}
- state APPROVED {}
- on approve in PENDING -> APPROVED do {
- goto APPROVED
- }
- }"""
- prog = parse(src)
- td = prog.processes[0].transitions[0]
- goto_actions = [a for a in td.actions if a.action_type == "goto"]
- assert len(goto_actions) == 1
- assert goto_actions[0].goto_state == "APPROVED"
-
- def test_goto_with_emit(self):
- """goto can coexist with emit actions."""
- src = """process @order :: OrderProcess {
- state PENDING {}
- state REVIEWING {}
- on submit in PENDING -> REVIEWING do {
- emit :: ReviewTask { priority = 1 }
- goto REVIEWING
- }
- }"""
- prog = parse(src)
- td = prog.processes[0].transitions[0]
- assert any(a.action_type == "create" for a in td.actions)
- assert any(a.action_type == "goto" for a in td.actions)
-
- def test_goto_compiles(self):
- """Process with goto compiles without error."""
- src = """process @order :: OrderProcess {
- state PENDING {}
- state DONE {}
- on complete in PENDING -> DONE do {
- goto DONE
- }
- }"""
- g = compile(src)
- assert "order" in g.processes
-
-
-# ─────────────────────────────────────────────────────────────────────────────
-# §9: Uncertainty model — meta field round-trip
-# ─────────────────────────────────────────────────────────────────────────────
-
-class TestUncertaintyModel:
- def test_uncertainty_in_meta_dict(self):
- """KNDLMeta.uncertainty survives to_dict/from_dict round-trip."""
- from kndl.graph import KNDLMeta
- m = KNDLMeta(uncertainty={"_type": "gaussian", "mean": 0.5, "std": 0.1})
- d = m.to_dict()
- assert "uncertainty" in d
- m2 = KNDLMeta.from_dict(d)
- assert m2.uncertainty["_type"] == "gaussian"
- assert m2.uncertainty["mean"] == pytest.approx(0.5)
-
- def test_uncertainty_none_not_in_dict(self):
- """uncertainty=None should not appear in to_dict output."""
- from kndl.graph import KNDLMeta
- m = KNDLMeta()
- d = m.to_dict()
- assert "uncertainty" not in d
-
- def test_uncertainty_histogram(self):
- """~uncertainty histogram with named buckets compiles."""
- src = """node @n :: T {
- ~uncertainty histogram { p0_25 = 0.1, p0_75 = 0.8, p1_0 = 1.0 }
- }"""
- g = compile(src)
- u = g.nodes["n"].meta.uncertainty
- assert u["_type"] == "histogram"
- assert "p0_25" in u
-
-
-# ─────────────────────────────────────────────────────────────────────────────
-# Additional parameterised type edge cases
-# ─────────────────────────────────────────────────────────────────────────────
-
-class TestParameterisedTypeEdgeCases:
- def test_union_of_parameterised(self):
- """Quantity | Quantity parses as union of two parameterised types."""
- src = 'type Distance = Quantity | Quantity'
- prog = parse(src)
- te = prog.types[0].type_expr
- assert te.kind == "union"
- assert te.children[0].kind == "parameterised"
- assert te.children[1].kind == "parameterised"
-
- def test_intersection_parameterised(self):
- """Code & Localized parses as intersection."""
- src = 'type MedCode = Code & Localized'
- prog = parse(src)
- te = prog.types[0].type_expr
- assert te.kind == "intersection"
-
- def test_deeply_nested_type(self):
- """Distribution> — deeply nested."""
- src = 'type D = Distribution>'
- prog = parse(src)
- te = prog.types[0].type_expr
- assert te.kind == "parameterised"
- assert te.params[0].kind == "parameterised"
- assert te.params[0].params[0].name == "float"
diff --git a/packages/python/tests/test_kndl.py b/packages/python/tests/test_kndl.py
deleted file mode 100644
index bf7dbae..0000000
--- a/packages/python/tests/test_kndl.py
+++ /dev/null
@@ -1,461 +0,0 @@
-"""
-KNDL test suite — covers lexer, parser, compiler, and serializer.
-Run with: pytest
-"""
-
-import sys
-import os
-from datetime import datetime, timezone
-
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
-
-from kndl import (
- parse, compile, serialize, tokenize,
- ParseError,
- KNDLGraph
-)
-from kndl.lexer import TokenType
-
-# ── Fixtures ──────────────────────────────────────────────────────────────────
-
-SIMPLE_NODE = """
-node @sensor_01 :: Temperature {
- value = 22.5
- unit = "°C"
- location -> @building_7
- ~confidence 0.94
- ~source "sensor://bldg-7/t-001"
- ~valid 2026-04-10T14:00Z .. *
- ~decay 0.95 / 1h
-}
-"""
-
-EDGE_DECL = """
-edge @room_204 -[located_in]-> @floor_2 {
- ~weight 0.95
-}
-"""
-
-TYPE_DECL = """
-type SmartRoom {
- temp : Float
- unit : String
-}
-
-type Protocol = "knx" | "bacnet"
-"""
-
-CONTEXT_DECL = """
-context @campus {
- ~source "system://dt"
- ~access "role:ops"
-
- node @building_7 :: Building {
- name = "HQ"
- floors = 4
- ~confidence 0.99
- }
-}
-"""
-
-INTENT_DECL = """
-intent @overheat :: Action {
- trigger = @sensor_01.value > 30.0
- do {
- emit :: Alert { level = "critical" }
- }
- ~priority 0.9
- ~cooldown 15m
-}
-"""
-
-QUERY_DECL = """
-query hot_rooms {
- match ?sensor :: Temperature
- -[located_in]-> ?room :: Room
- where ?sensor.value > 26.0
- return ?room
-}
-"""
-
-
-# ── Lexer tests ───────────────────────────────────────────────────────────────
-
-class TestLexer:
- def test_tokenize_keywords(self):
- tokens = tokenize("node edge type intent context query")
- types = [t.type for t in tokens if t.type.name != "EOF"]
- assert TokenType.KW_NODE in types
- assert TokenType.KW_EDGE in types
- assert TokenType.KW_TYPE in types
- assert TokenType.KW_INTENT in types
- assert TokenType.KW_CONTEXT in types
- assert TokenType.KW_QUERY in types
-
- def test_tokenize_node_ref(self):
- tokens = tokenize("@sensor_01")
- assert tokens[0].type == TokenType.NODE_REF
- assert tokens[0].value == "@sensor_01"
-
- def test_tokenize_meta_key(self):
- tokens = tokenize("~confidence")
- assert tokens[0].type == TokenType.META_KEY
- assert tokens[0].value == "confidence"
-
- def test_tokenize_float(self):
- tokens = tokenize("0.94")
- assert tokens[0].type == TokenType.FLOAT
- assert float(tokens[0].value) == 0.94
-
- def test_tokenize_duration(self):
- tokens = tokenize("1h 30m 5s 100ms")
- duration_tokens = [t for t in tokens if t.type == TokenType.DURATION]
- assert len(duration_tokens) == 4
- assert duration_tokens[0].value == "1h"
- assert duration_tokens[3].value == "100ms"
-
- def test_tokenize_datetime(self):
- tokens = tokenize("2026-04-10T14:00Z")
- assert tokens[0].type == TokenType.DATETIME
-
- def test_tokenize_string(self):
- tokens = tokenize('"hello world"')
- assert tokens[0].type == TokenType.STRING
- assert tokens[0].value == "hello world"
-
- def test_tokenize_string_escape(self):
- tokens = tokenize(r'"hello\nworld"')
- assert tokens[0].type == TokenType.STRING
- assert "\n" in tokens[0].value
-
- def test_typed_arrow(self):
- tokens = tokenize("-[located_in]->")
- types = [t.type for t in tokens]
- assert TokenType.TYPED_ARROW_START in types
- assert TokenType.TYPED_ARROW_END in types
-
- def test_range_operator(self):
- tokens = tokenize("2026-04-10T14:00Z .. *")
- types = [t.type for t in tokens]
- assert TokenType.OP_RANGE in types
-
- def test_line_comment_stripped(self):
- tokens = tokenize("node // this is a comment\nedge")
- types = [t.type for t in tokens if t.type != TokenType.EOF]
- assert TokenType.KW_NODE in types
- assert TokenType.KW_EDGE in types
-
- def test_block_comment_stripped(self):
- tokens = tokenize("node /* block comment */ edge")
- types = [t.type for t in tokens if t.type != TokenType.EOF]
- assert TokenType.KW_NODE in types
- assert TokenType.KW_EDGE in types
-
-
-# ── Parser tests ──────────────────────────────────────────────────────────────
-
-class TestParser:
- def test_parse_simple_node(self):
- program = parse(SIMPLE_NODE)
- assert len(program.nodes) == 1
- node = program.nodes[0]
- assert node.ref.name == "sensor_01"
- assert node.type_name == "Temperature"
-
- def test_parse_node_fields(self):
- program = parse(SIMPLE_NODE)
- node = program.nodes[0]
- fields = {f.name: f for f in node.fields}
- assert "value" in fields
- assert "unit" in fields
-
- def test_parse_inline_edge(self):
- program = parse(SIMPLE_NODE)
- node = program.nodes[0]
- assert len(node.edges) == 1
- assert node.edges[0].field_name == "location"
- assert node.edges[0].target.name == "building_7"
-
- def test_parse_meta_annotations(self):
- program = parse(SIMPLE_NODE)
- node = program.nodes[0]
- meta_keys = {m.key for m in node.meta}
- assert "confidence" in meta_keys
- assert "source" in meta_keys
- assert "valid" in meta_keys
- assert "decay" in meta_keys
-
- def test_parse_edge_decl(self):
- program = parse(EDGE_DECL)
- assert len(program.edges) == 1
- edge = program.edges[0]
- assert edge.source.name == "room_204"
- assert edge.edge_type == "located_in"
- assert len(edge.targets) == 1
- assert edge.targets[0].name == "floor_2"
-
- def test_parse_type_decl(self):
- program = parse(TYPE_DECL)
- assert len(program.types) == 2
- assert program.types[0].name == "SmartRoom"
-
- def test_parse_context_decl(self):
- program = parse(CONTEXT_DECL)
- assert len(program.contexts) == 1
- ctx = program.contexts[0]
- assert ctx.ref.name == "campus"
- assert len(ctx.nodes) == 1
-
- def test_parse_intent_decl(self):
- program = parse(INTENT_DECL)
- assert len(program.intents) == 1
- intent = program.intents[0]
- assert intent.ref.name == "overheat"
- assert intent.type_name == "Action"
- assert intent.trigger is not None
- assert intent.trigger.kind == "expression"
-
- def test_parse_query_decl(self):
- program = parse(QUERY_DECL)
- assert len(program.queries) == 1
- q = program.queries[0]
- assert q.name == "hot_rooms"
- assert len(q.matches) == 1
-
- def test_parse_cron_trigger(self):
- src = """intent @monthly :: ScheduledAction {
- trigger = cron "0 0 1 * *"
- do { emit :: Report }
- ~priority 0.5
- }"""
- program = parse(src)
- assert program.intents[0].trigger.kind == "cron"
- assert program.intents[0].trigger.cron_expr == "0 0 1 * *"
-
- def test_parse_multi_target_edge(self):
- src = "edge @building_7 -[contains]-> [ @floor_1, @floor_2, @floor_3 ]"
- program = parse(src)
- edge = program.edges[0]
- assert len(edge.targets) == 3
-
- def test_parse_type_union(self):
- src = 'type Protocol = "knx" | "bacnet" | "modbus"'
- program = parse(src)
- assert program.types[0].type_expr.kind == "union"
-
- def test_parse_type_intersection(self):
- src = "type SmartSensor = Device & Measurement"
- program = parse(src)
- assert program.types[0].type_expr.kind == "intersection"
-
- def test_parse_optional_type(self):
- src = "type Sensor { location : Place? }"
- program = parse(src)
- fields = program.types[0].fields
- assert fields[0].name == "location"
- assert fields[0].type_expr.kind == "optional"
-
- def test_parse_error_unexpected_token(self):
- import pytest
- with pytest.raises(ParseError):
- parse("!!! invalid !!!")
-
- def test_parse_import(self):
- src = 'import { Temperature, Measurement } from "kndl://std/units"'
- program = parse(src)
- assert len(program.imports) == 1
- assert "Temperature" in program.imports[0].names
- assert program.imports[0].source == "kndl://std/units"
-
-
-# ── Compiler tests ────────────────────────────────────────────────────────────
-
-class TestCompiler:
- def test_compile_node(self):
- graph = compile(SIMPLE_NODE)
- assert "sensor_01" in graph.nodes
- node = graph.nodes["sensor_01"]
- assert node.type_name == "Temperature"
- assert node.fields["value"] == 22.5
- assert node.fields["unit"] == "°C"
-
- def test_compile_meta_confidence(self):
- graph = compile(SIMPLE_NODE)
- node = graph.nodes["sensor_01"]
- assert node.meta.confidence == 0.94
-
- def test_compile_meta_source(self):
- graph = compile(SIMPLE_NODE)
- node = graph.nodes["sensor_01"]
- assert node.meta.source == "sensor://bldg-7/t-001"
-
- def test_compile_meta_valid(self):
- graph = compile(SIMPLE_NODE)
- node = graph.nodes["sensor_01"]
- assert node.meta.valid_start == "2026-04-10T14:00Z"
- assert node.meta.valid_end is None # * → open-ended
-
- def test_compile_meta_decay(self):
- graph = compile(SIMPLE_NODE)
- node = graph.nodes["sensor_01"]
- assert node.meta.decay_rate == 0.95
- assert node.meta.decay_duration_seconds == 3600.0 # 1h
-
- def test_compile_inline_edge(self):
- graph = compile(SIMPLE_NODE)
- edges = graph.get_outgoing_edges("sensor_01")
- assert len(edges) == 1
- assert edges[0].target_id == "building_7"
- assert edges[0].edge_type == "location"
-
- def test_compile_standalone_edge(self):
- graph = compile(EDGE_DECL)
- edges = graph.edges
- edge = next(iter(edges.values()))
- assert edge.source_id == "room_204"
- assert edge.target_id == "floor_2"
- assert edge.edge_type == "located_in"
-
- def test_compile_context_inherits_meta(self):
- graph = compile(CONTEXT_DECL)
- assert "building_7" in graph.nodes
- node = graph.nodes["building_7"]
- assert node.meta.source == "system://dt"
- assert node.meta.access == "role:ops"
-
- def test_compile_context_node_overrides(self):
- graph = compile(CONTEXT_DECL)
- node = graph.nodes["building_7"]
- # Node's own ~confidence 0.99 should win over context default
- assert node.meta.confidence == 0.99
-
- def test_compile_intent(self):
- graph = compile(INTENT_DECL)
- assert "overheat" in graph.intents
- intent = graph.intents["overheat"]
- assert intent.type_name == "Action"
- assert intent.trigger_kind == "expression"
- assert intent.meta.priority == 0.9
-
- def test_compile_intent_cooldown(self):
- graph = compile(INTENT_DECL)
- intent = graph.intents["overheat"]
- assert intent.meta.cooldown_seconds == 900.0 # 15m
-
- def test_compile_type(self):
- graph = compile(TYPE_DECL)
- assert "SmartRoom" in graph.types
-
- def test_confidence_decay(self):
- src = """
- node @s :: Sensor {
- value = 1.0
- ~confidence 1.0
- ~valid 2026-01-01T00:00Z .. *
- ~decay 0.5 / 1h
- }
- """
- graph = compile(src)
- node = graph.nodes["s"]
- # Compute effective confidence 2 hours after valid_start
- t = datetime(2026, 1, 1, 2, 0, 0, tzinfo=timezone.utc)
- eff = node.meta.effective_confidence(at_time=t)
- assert abs(eff - 0.25) < 0.001 # 1.0 * 0.5^2 = 0.25
-
-
-# ── Query tests ───────────────────────────────────────────────────────────────
-
-class TestGraph:
- def _make_graph(self) -> KNDLGraph:
- src = """
- node @t1 :: Temperature {
- value = 22.5
- unit = "°C"
- ~confidence 0.9
- ~source "sensor://a"
- }
- node @t2 :: Temperature {
- value = 30.0
- unit = "°C"
- ~confidence 0.4
- ~source "sensor://b"
- }
- node @r1 :: Room {
- name = "Meeting Room 204"
- }
- edge @t1 -[located_in]-> @r1
- """
- return compile(src)
-
- def test_query_by_type(self):
- graph = self._make_graph()
- nodes = graph.query_nodes(type_name="Temperature")
- assert len(nodes) == 2
-
- def test_query_by_confidence(self):
- graph = self._make_graph()
- nodes = graph.query_nodes(type_name="Temperature", min_confidence=0.8)
- assert len(nodes) == 1
- assert nodes[0].id == "t1"
-
- def test_query_by_field(self):
- graph = self._make_graph()
- nodes = graph.query_nodes(field_filters={"unit": "°C"})
- assert len(nodes) == 2
-
- def test_neighborhood(self):
- graph = self._make_graph()
- result = graph.query_neighborhood("t1", hops=1)
- node_ids = {n["id"] for n in result["nodes"]}
- assert "t1" in node_ids
- assert "r1" in node_ids
-
- def test_remove_node_cleans_edges(self):
- graph = self._make_graph()
- initial_edges = len(graph.edges)
- graph.remove_node("t1")
- assert "t1" not in graph.nodes
- # The edge t1 -[located_in]-> r1 should be gone
- assert len(graph.edges) < initial_edges
-
- def test_update_node(self):
- graph = self._make_graph()
- graph.update_node("t1", fields={"value": 25.0})
- assert graph.nodes["t1"].fields["value"] == 25.0
-
- def test_to_dict_roundtrip(self):
- graph = self._make_graph()
- d = graph.to_dict()
- assert d["summary"]["node_count"] == 3
- assert d["summary"]["edge_count"] >= 1
-
-
-# ── Serializer tests ──────────────────────────────────────────────────────────
-
-class TestSerializer:
- def test_serialize_node(self):
- graph = compile(SIMPLE_NODE)
- text = serialize(graph)
- assert "node @sensor_01" in text
- assert ":: Temperature" in text
- assert "~confidence" in text
- assert "~source" in text
-
- def test_serialize_edge(self):
- src = "edge @a -[linked_to]-> @b"
- graph = compile(src)
- text = serialize(graph)
- assert "edge" in text
- assert "linked_to" in text
-
- def test_serialize_preserves_confidence(self):
- graph = compile(SIMPLE_NODE)
- text = serialize(graph)
- assert "0.94" in text
-
- def test_roundtrip(self):
- """Parse → compile → serialize → parse again → same graph size."""
- graph1 = compile(SIMPLE_NODE)
- text = serialize(graph1)
- graph2 = compile(text)
- assert len(graph1.nodes) == len(graph2.nodes)
diff --git a/packages/python/tests/test_kndl_extended.py b/packages/python/tests/test_kndl_extended.py
deleted file mode 100644
index fda3372..0000000
--- a/packages/python/tests/test_kndl_extended.py
+++ /dev/null
@@ -1,642 +0,0 @@
-"""
-Extended KNDL test suite — additional coverage for edge cases, integration
-scenarios, and deeper coverage of the type system, contexts, and serializer.
-
-Run with: pytest
-"""
-
-import sys
-import os
-import pytest
-from datetime import datetime, timezone
-
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
-
-from kndl import parse, compile, serialize, tokenize, LexerError, ParseError
-from kndl.lexer import TokenType
-from kndl.graph import KNDLGraph, GraphNode, GraphEdge, KNDLMeta
-
-
-# ── Lexer edge cases ──────────────────────────────────────────────────────────
-
-class TestLexerEdgeCases:
- def test_hex_literal(self):
- tokens = tokenize("0xFF 0x1A 0x00")
- ints = [t for t in tokens if t.type == TokenType.INT]
- assert len(ints) == 3
- assert ints[0].value == "0xFF"
-
- def test_binary_literal(self):
- tokens = tokenize("0b1010 0b0001")
- ints = [t for t in tokens if t.type == TokenType.INT]
- assert len(ints) == 2
- assert ints[0].value == "0b1010"
-
- def test_negative_int(self):
- tokens = tokenize("-42")
- ints = [t for t in tokens if t.type == TokenType.INT]
- assert len(ints) == 1
-
- def test_negative_float(self):
- tokens = tokenize("-3.14")
- floats = [t for t in tokens if t.type == TokenType.FLOAT]
- assert len(floats) == 1
-
- def test_all_duration_units(self):
- tokens = tokenize("1ms 2s 3m 4h 5d 1w")
- durations = [t for t in tokens if t.type == TokenType.DURATION]
- assert len(durations) == 6
- values = [t.value for t in durations]
- assert "1ms" in values
- assert "2s" in values
- assert "3m" in values
- assert "4h" in values
- assert "5d" in values
- assert "1w" in values
-
- def test_duration_float_amount(self):
- tokens = tokenize("0.5h")
- durations = [t for t in tokens if t.type == TokenType.DURATION]
- assert len(durations) == 1
- assert durations[0].value == "0.5h"
-
- def test_unterminated_string_raises(self):
- with pytest.raises(LexerError):
- tokenize('"unterminated')
-
- def test_nested_block_comment(self):
- tokens = tokenize("/* outer /* inner */ outer end */ node")
- types = [t.type for t in tokens if t.type != TokenType.EOF]
- assert TokenType.KW_NODE in types
-
- def test_string_with_escape_sequences(self):
- tokens = tokenize(r'"tab\there"')
- assert tokens[0].type == TokenType.STRING
- assert "\t" in tokens[0].value
-
- def test_boolean_literals(self):
- tokens = tokenize("true false")
- bools = [t for t in tokens if t.type == TokenType.BOOL]
- assert len(bools) == 2
-
- def test_node_ref_with_dot(self):
- tokens = tokenize("@building.floor_3")
- assert tokens[0].type == TokenType.NODE_REF
- assert tokens[0].value == "@building.floor_3"
-
- def test_var_bind(self):
- tokens = tokenize("?sensor")
- assert tokens[0].type == TokenType.VAR_BIND
-
- def test_arrow_variants(self):
- tokens = tokenize("-> <->")
- types = [t.type for t in tokens if t.type != TokenType.EOF]
- assert TokenType.OP_ARROW in types
- assert TokenType.OP_BIARROW in types
-
- def test_operator_precedence_tokens(self):
- tokens = tokenize("== != >= <=")
- types = [t.type for t in tokens if t.type != TokenType.EOF]
- assert TokenType.OP_EQ in types
- assert TokenType.OP_NEQ in types
- assert TokenType.OP_GTE in types
- assert TokenType.OP_LTE in types
-
- def test_large_float_exponent(self):
- tokens = tokenize("1.5e10 2.0E-3")
- floats = [t for t in tokens if t.type == TokenType.FLOAT]
- assert len(floats) == 2
-
- def test_underscore_separator_in_int(self):
- tokens = tokenize("1_000_000")
- ints = [t for t in tokens if t.type == TokenType.INT]
- assert len(ints) == 1
-
-
-# ── Parser extended tests ─────────────────────────────────────────────────────
-
-class TestParserExtended:
- def test_parse_constrained_type(self):
- # Constraints live in TypeDecl.constraints, not TypeExpr.kind
- src = "type ValidTemp = Float where { value >= -50 and value <= 150 }"
- program = parse(src)
- assert len(program.types) == 1
- assert len(program.types[0].constraints) > 0
-
- def test_parse_export_declaration(self):
- # Parser supports `export ` — not `export { names }`
- src = "export type SmartRoom { name : String }"
- program = parse(src)
- assert len(program.exports) == 1
-
- def test_parse_multi_field_node(self):
- src = """node @sensor :: Sensor {
- id = "S-001"
- active = true
- floor = 3
- rating = 4.5
- }"""
- program = parse(src)
- node = program.nodes[0]
- fields = {f.name: f for f in node.fields}
- assert "id" in fields
- assert "active" in fields
- assert "floor" in fields
- assert "rating" in fields
-
- def test_parse_context_with_edges(self):
- src = """context @building {
- ~source "system://dt"
- node @floor_1 :: Floor { level = 1 }
- node @floor_2 :: Floor { level = 2 }
- edge @floor_1 -[above]-> @floor_2
- }"""
- program = parse(src)
- ctx = program.contexts[0]
- assert len(ctx.nodes) == 2
- assert len(ctx.edges) == 1
-
- def test_parse_query_with_aggregation(self):
- # aggregate clause lives after return expression, in { } block
- src = """query avg_temp {
- match ?s :: Temperature
- where ?s.value > 0
- return ?s aggregate { mean_temp = avg(?s.value) }
- }"""
- program = parse(src)
- assert len(program.queries) == 1
-
- def test_parse_optional_match_in_query(self):
- # optional keyword precedes match keyword; avoid `in` (reserved keyword)
- src = """query rooms {
- match ?room :: Room
- optional match ?sensor :: Sensor -[located_in]-> ?room
- return ?room
- }"""
- program = parse(src)
- assert program.queries[0].name == "rooms"
-
- def test_parse_query_trigger_intent(self):
- # trigger = query
- src = """intent @check :: Monitor {
- trigger = query hot_rooms {
- match ?s :: Temperature
- where ?s.value > 30
- return ?s
- }
- do { emit :: Alert { level = "warn" } }
- }"""
- program = parse(src)
- assert program.intents[0].trigger.kind == "query"
-
- def test_parse_type_struct_with_optional_fields(self):
- src = """type Building {
- name : String
- floors : Int
- manager : Person?
- }"""
- program = parse(src)
- fields = program.types[0].fields
- optional_field = next(f for f in fields if f.name == "manager")
- assert optional_field.type_expr.kind == "optional"
-
- def test_parse_nested_context(self):
- src = """context @campus {
- ~source "system://campus"
- context @building_7 {
- ~source "system://bldg7"
- node @room_204 :: Room { name = "204" }
- }
- }"""
- program = parse(src)
- assert len(program.contexts) == 1
- outer = program.contexts[0]
- assert len(outer.contexts) == 1
-
- def test_parse_error_missing_closing_brace(self):
- with pytest.raises(ParseError):
- parse("node @x :: Foo { val = 1")
-
- def test_parse_multi_target_edge_with_meta(self):
- src = """edge @hub -[connects]-> [ @a, @b, @c ] {
- ~confidence 0.9
- }"""
- program = parse(src)
- edge = program.edges[0]
- assert len(edge.targets) == 3
- assert len(edge.meta) == 1
-
-
-# ── Compiler extended tests ───────────────────────────────────────────────────
-
-class TestCompilerExtended:
- def test_compile_multiple_nodes(self):
- src = """
- node @a :: Person { name = "Alice" }
- node @b :: Person { name = "Bob" }
- node @c :: Location { name = "Office" }
- """
- graph = compile(src)
- assert len(graph.nodes) == 3
- assert "a" in graph.nodes
- assert "b" in graph.nodes
- assert "c" in graph.nodes
-
- def test_compile_edge_creates_both_node_refs(self):
- src = """
- node @alice :: Person { name = "Alice" }
- node @lab :: Organization { name = "Lab" }
- edge @alice -[works_at]-> @lab { ~confidence 0.98 }
- """
- graph = compile(src)
- edges = graph.get_outgoing_edges("alice")
- assert len(edges) == 1
- assert edges[0].target_id == "lab"
- assert edges[0].meta.confidence == 0.98
-
- def test_compile_standalone_edge_with_fields(self):
- src = """
- edge @a -[links]-> @b {
- ~confidence 0.7
- ~source "agent://linker"
- }
- """
- graph = compile(src)
- edge = next(iter(graph.edges.values()))
- assert edge.meta.confidence == 0.7
- assert edge.meta.source == "agent://linker"
-
- def test_compile_context_with_multiple_nodes(self):
- src = """
- context @site {
- ~source "system://site"
- ~confidence 0.85
- node @a :: Floor { level = 1 }
- node @b :: Floor { level = 2 }
- node @c :: Floor { level = 3 }
- }
- """
- graph = compile(src)
- for node_id in ("a", "b", "c"):
- assert graph.nodes[node_id].meta.source == "system://site"
- assert graph.nodes[node_id].meta.confidence == 0.85
-
- def test_compile_context_node_confidence_override(self):
- src = """
- context @site {
- ~confidence 0.5
- node @special :: Room {
- name = "VIP"
- ~confidence 0.99
- }
- }
- """
- graph = compile(src)
- assert graph.nodes["special"].meta.confidence == 0.99
-
- def test_compile_tags_on_node(self):
- src = """
- node @sensor :: Sensor {
- ~tags ["iot", "outdoor", "v2"]
- }
- """
- graph = compile(src)
- assert "iot" in graph.nodes["sensor"].meta.tags
- assert "outdoor" in graph.nodes["sensor"].meta.tags
-
- def test_compile_decay_all_units(self):
- for dur, expected in [("1s", 1.0), ("5m", 300.0), ("2h", 7200.0), ("1d", 86400.0)]:
- src = f"""
- node @x :: Sensor {{
- ~confidence 1.0
- ~decay 0.9 / {dur}
- }}
- """
- graph = compile(src)
- assert graph.nodes["x"].meta.decay_duration_seconds == expected
-
- def test_compile_type_struct(self):
- src = """
- type SmartSensor {
- temp : Float
- unit : String
- active : Bool
- }
- """
- graph = compile(src)
- assert "SmartSensor" in graph.types
-
- def test_compile_intent_with_emit(self):
- src = """
- intent @alert :: Action {
- trigger = @sensor.value > 40
- do { emit :: HeatAlert { level = "high" } }
- ~priority 0.95
- ~cooldown 5m
- }
- """
- graph = compile(src)
- intent = graph.intents["alert"]
- assert intent.type_name == "Action"
- assert intent.meta.cooldown_seconds == 300.0 # 5m
- assert intent.meta.priority == 0.95
-
- def test_compile_valid_range_with_end(self):
- src = """
- node @event :: Event {
- ~valid 2026-01-01T00:00Z .. 2026-12-31T23:59Z
- }
- """
- graph = compile(src)
- node = graph.nodes["event"]
- assert node.meta.valid_start == "2026-01-01T00:00Z"
- assert node.meta.valid_end == "2026-12-31T23:59Z"
-
- def test_compile_supersedes(self):
- src = """
- node @reading_v2 :: Temperature {
- value = 22.0
- ~supersedes "reading_v1"
- }
- """
- graph = compile(src)
- assert graph.nodes["reading_v2"].meta.supersedes == "reading_v1"
-
-
-# ── Graph extended tests ──────────────────────────────────────────────────────
-
-class TestGraphExtended:
- def _make_graph(self) -> KNDLGraph:
- src = """
- node @t1 :: Temperature { value = 22.5 ~confidence 0.9 }
- node @t2 :: Temperature { value = 30.0 ~confidence 0.4 }
- node @r1 :: Room { name = "Lab" }
- node @b1 :: Building { name = "HQ" }
- edge @t1 -[in_room]-> @r1
- edge @r1 -[in_building]-> @b1
- """
- return compile(src)
-
- def test_two_hop_neighborhood(self):
- graph = self._make_graph()
- result = graph.query_neighborhood("t1", hops=2)
- ids = {n["id"] for n in result["nodes"]}
- assert "t1" in ids
- assert "r1" in ids
- assert "b1" in ids # reached via 2 hops
-
- def test_one_hop_neighborhood_excludes_distant(self):
- graph = self._make_graph()
- result = graph.query_neighborhood("t1", hops=1)
- ids = {n["id"] for n in result["nodes"]}
- assert "b1" not in ids
-
- def test_get_incoming_edges(self):
- graph = self._make_graph()
- incoming = graph.get_incoming_edges("r1")
- assert any(e.source_id == "t1" for e in incoming)
-
- def test_get_outgoing_edges(self):
- graph = self._make_graph()
- outgoing = graph.get_outgoing_edges("r1")
- assert any(e.target_id == "b1" for e in outgoing)
-
- def test_update_node_meta(self):
- graph = self._make_graph()
- graph.update_node("t1", meta_updates={"confidence": 0.75})
- assert graph.nodes["t1"].meta.confidence == 0.75
-
- def test_add_and_query_node(self):
- graph = KNDLGraph()
- meta = KNDLMeta(confidence=0.9, source="test://unit")
- node = GraphNode(id="n1", type_name="Widget", fields={"x": 1}, meta=meta)
- graph.add_node(node)
- results = graph.query_nodes(type_name="Widget")
- assert len(results) == 1
- assert results[0].id == "n1"
-
- def test_add_edge_and_query(self):
- graph = KNDLGraph()
- meta = KNDLMeta(confidence=1.0)
- graph.add_node(GraphNode(id="a", type_name="T", fields={}, meta=KNDLMeta()))
- graph.add_node(GraphNode(id="b", type_name="T", fields={}, meta=KNDLMeta()))
- edge = GraphEdge(source_id="a", target_id="b", edge_type="links", fields={}, meta=meta)
- graph.add_edge(edge)
- outgoing = graph.get_outgoing_edges("a")
- assert len(outgoing) == 1
- assert outgoing[0].target_id == "b"
-
- def test_remove_node_removes_all_edges(self):
- graph = self._make_graph()
- # r1 has both incoming (t1) and outgoing (b1) edges
- graph.remove_node("r1")
- assert "r1" not in graph.nodes
- remaining_edges = list(graph.edges.values())
- for e in remaining_edges:
- assert e.source_id != "r1" and e.target_id != "r1"
-
- def test_from_dict_roundtrip(self):
- graph = self._make_graph()
- d = graph.to_dict()
- graph2 = KNDLGraph.from_dict(d)
- assert len(graph2.nodes) == len(graph.nodes)
- assert len(graph2.edges) == len(graph.edges)
-
- def test_query_by_field_filter(self):
- graph = self._make_graph()
- results = graph.query_nodes(field_filters={"name": "Lab"})
- assert len(results) == 1
- assert results[0].id == "r1"
-
- def test_query_all_nodes_no_filter(self):
- graph = self._make_graph()
- all_nodes = graph.query_nodes()
- assert len(all_nodes) == 4
-
- def test_effective_confidence_no_decay(self):
- node_meta = KNDLMeta(confidence=0.8)
- assert node_meta.effective_confidence() == 0.8
-
- def test_effective_confidence_with_decay(self):
- node_meta = KNDLMeta(
- confidence=1.0,
- valid_start="2026-01-01T00:00Z",
- decay_rate=0.5,
- decay_duration_seconds=3600.0, # 1h
- )
- at = datetime(2026, 1, 1, 3, 0, 0, tzinfo=timezone.utc) # 3 hours later
- eff = node_meta.effective_confidence(at_time=at)
- assert abs(eff - 0.125) < 0.001 # 1.0 * 0.5^3
-
-
-# ── Serializer extended tests ─────────────────────────────────────────────────
-
-class TestSerializerExtended:
- def test_serialize_multi_node_graph(self):
- src = """
- node @alice :: Person { name = "Alice" ~confidence 0.9 }
- node @bob :: Person { name = "Bob" ~confidence 0.8 }
- """
- graph = compile(src)
- text = serialize(graph)
- assert "node @alice" in text
- assert "node @bob" in text
-
- def test_serialize_edge_with_confidence(self):
- src = """
- edge @a -[links]-> @b { ~confidence 0.75 ~source "agent://linker" }
- """
- graph = compile(src)
- text = serialize(graph)
- assert "edge" in text
- assert "links" in text
- assert "0.75" in text
-
- def test_roundtrip_preserves_edge_count(self):
- src = """
- node @a :: T { ~confidence 0.9 }
- node @b :: T { ~confidence 0.8 }
- node @c :: T { ~confidence 0.7 }
- edge @a -[x]-> @b
- edge @b -[x]-> @c
- edge @a -[x]-> @c
- """
- g1 = compile(src)
- text = serialize(g1)
- g2 = compile(text)
- assert len(g1.edges) == len(g2.edges)
-
- def test_roundtrip_preserves_meta_decay(self):
- src = """
- node @sensor :: Sensor {
- ~confidence 0.95
- ~decay 0.8 / 30m
- }
- """
- g1 = compile(src)
- text = serialize(g1)
- g2 = compile(text)
- n = g2.nodes["sensor"]
- assert n.meta.decay_rate == 0.8
- assert n.meta.decay_duration_seconds == 1800.0 # 30m
-
- def test_serialize_tags(self):
- src = """
- node @sensor :: Sensor { ~tags ["iot", "v2"] }
- """
- graph = compile(src)
- text = serialize(graph)
- assert "iot" in text
-
- def test_serialize_valid_range(self):
- src = """
- node @event :: Event {
- ~valid 2026-01-01T00:00Z .. 2026-12-31T23:59Z
- }
- """
- graph = compile(src)
- text = serialize(graph)
- assert "2026-01-01T00:00Z" in text
-
-
-# ── Integration tests ─────────────────────────────────────────────────────────
-
-class TestIntegration:
- FULL_DOC = """
- // Smart building IoT scenario
- type SmartRoom {
- name : String
- floor : Int
- }
-
- context @campus {
- ~source "system://dt"
- ~confidence 0.95
-
- node @building_7 :: Building {
- name = "HQ"
- floors = 5
- ~confidence 0.99
- }
-
- node @floor_3 :: Floor {
- level = 3
- above -> @floor_2
- ~confidence 0.97
- }
-
- node @floor_2 :: Floor {
- level = 2
- }
- }
-
- node @temp_001 :: Temperature {
- value = 21.5
- unit = "°C"
- location -> @floor_3
- ~confidence 0.93
- ~source "sensor://bldg7/f3/t001"
- ~valid 2026-04-10T14:00Z .. *
- ~decay 0.95 / 1h
- }
-
- edge @temp_001 -[monitors]-> @building_7 {
- ~confidence 0.9
- }
-
- intent @overheat :: Alert {
- trigger = @temp_001.value > 30.0
- do { emit :: HeatAlert { level = "critical" } }
- ~priority 0.95
- ~cooldown 15m
- }
-
- import { StandardUnits } from "kndl://std/units"
- export type SmartRoom { name : String }
- """
-
- def test_compile_full_document(self):
- graph = compile(self.FULL_DOC)
- assert "building_7" in graph.nodes
- assert "floor_3" in graph.nodes
- assert "temp_001" in graph.nodes
-
- def test_context_meta_inherited(self):
- graph = compile(self.FULL_DOC)
- assert graph.nodes["building_7"].meta.confidence == 0.99
- assert graph.nodes["floor_3"].meta.source == "system://dt"
-
- def test_standalone_edge_in_full_doc(self):
- graph = compile(self.FULL_DOC)
- edges = graph.get_outgoing_edges("temp_001")
- types = {e.edge_type for e in edges}
- assert "monitors" in types or "location" in types
-
- def test_intent_compiled(self):
- graph = compile(self.FULL_DOC)
- assert "overheat" in graph.intents
- intent = graph.intents["overheat"]
- assert intent.meta.cooldown_seconds == 900.0
-
- def test_type_compiled(self):
- graph = compile(self.FULL_DOC)
- assert "SmartRoom" in graph.types
-
- def test_full_roundtrip(self):
- graph1 = compile(self.FULL_DOC)
- text = serialize(graph1)
- graph2 = compile(text)
- assert len(graph1.nodes) == len(graph2.nodes)
- assert "temp_001" in graph2.nodes
-
- def test_graph_stats_via_dict(self):
- graph = compile(self.FULL_DOC)
- d = graph.to_dict()
- assert d["summary"]["node_count"] >= 3
-
- def test_neighborhood_across_context_boundary(self):
- graph = compile(self.FULL_DOC)
- result = graph.query_neighborhood("temp_001", hops=2)
- ids = {n["id"] for n in result["nodes"]}
- assert "temp_001" in ids
- assert "floor_3" in ids
diff --git a/packages/python/tests/test_processes.py b/packages/python/tests/test_processes.py
deleted file mode 100644
index 1cf8107..0000000
--- a/packages/python/tests/test_processes.py
+++ /dev/null
@@ -1,665 +0,0 @@
-"""Tests for KNDL v0.2 features."""
-import sys
-import os
-import pytest
-
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
-
-from kndl import parse, compile, serialize, tokenize
-from kndl.lexer import TokenType
-from kndl.graph import KNDLMeta
-
-
-# ─────────────────────────────────────────────
-# Lexer tests
-# ─────────────────────────────────────────────
-
-class TestLexerV02:
- def test_decimal_literal(self):
- """19.99d tokenizes as DECIMAL."""
- tokens = tokenize("19.99d")
- assert tokens[0].type == TokenType.DECIMAL
- assert tokens[0].value == "19.99d"
-
- def test_decimal_literal_small(self):
- """0.0001d tokenizes as DECIMAL."""
- tokens = tokenize("0.0001d")
- assert tokens[0].type == TokenType.DECIMAL
- assert tokens[0].value == "0.0001d"
-
- def test_duration_ns(self):
- """5ns tokenizes as DURATION."""
- tokens = tokenize("5ns")
- assert tokens[0].type == TokenType.DURATION
- assert tokens[0].value == "5ns"
-
- def test_duration_us(self):
- """10us tokenizes as DURATION."""
- tokens = tokenize("10us")
- assert tokens[0].type == TokenType.DURATION
- assert tokens[0].value == "10us"
-
- def test_duration_mo(self):
- """3mo tokenizes as DURATION."""
- tokens = tokenize("3mo")
- assert tokens[0].type == TokenType.DURATION
- assert tokens[0].value == "3mo"
-
- def test_duration_y(self):
- """2y tokenizes as DURATION."""
- tokens = tokenize("2y")
- assert tokens[0].type == TokenType.DURATION
- assert tokens[0].value == "2y"
-
- def test_map_open_token(self):
- """#{ tokenizes as MAP_OPEN."""
- tokens = tokenize("#{")
- assert tokens[0].type == TokenType.MAP_OPEN
- assert tokens[0].value == "#{"
-
- def test_process_keyword(self):
- """'process' tokenizes as KW_PROCESS."""
- tokens = tokenize("process")
- assert tokens[0].type == TokenType.KW_PROCESS
-
- def test_state_keyword(self):
- """'state' tokenizes as KW_STATE."""
- tokens = tokenize("state")
- assert tokens[0].type == TokenType.KW_STATE
-
- def test_on_keyword(self):
- """'on' tokenizes as KW_ON."""
- tokens = tokenize("on")
- assert tokens[0].type == TokenType.KW_ON
-
- def test_by_keyword(self):
- """'by' tokenizes as KW_BY."""
- tokens = tokenize("by")
- assert tokens[0].type == TokenType.KW_BY
-
- def test_decimal_not_duration_d(self):
- """Float with 'd' suffix is DECIMAL, not DURATION."""
- tokens = tokenize("3.14d")
- assert tokens[0].type == TokenType.DECIMAL
-
- def test_integer_d_still_duration(self):
- """Integer followed by 'd' remains a DURATION (days)."""
- tokens = tokenize("1d")
- assert tokens[0].type == TokenType.DURATION
- assert tokens[0].value == "1d"
-
- def test_goto_keyword(self):
- """'goto' tokenizes as KW_GOTO."""
- tokens = tokenize("goto")
- assert tokens[0].type == TokenType.KW_GOTO
-
- def test_compensate_keyword(self):
- """'compensate' tokenizes as KW_COMPENSATE."""
- tokens = tokenize("compensate")
- assert tokens[0].type == TokenType.KW_COMPENSATE
-
- def test_of_keyword(self):
- """'of' tokenizes as KW_OF."""
- tokens = tokenize("of")
- assert tokens[0].type == TokenType.KW_OF
-
-
-# ─────────────────────────────────────────────
-# Parser tests
-# ─────────────────────────────────────────────
-
-class TestParserV02:
- def test_parse_decimal_field(self):
- """Node with a decimal field value parses correctly."""
- src = """
-node @item :: Product {
- price = 19.99d
-}
-"""
- prog = parse(src)
- assert len(prog.nodes) == 1
- field = prog.nodes[0].fields[0]
- assert field.name == "price"
- # The literal node should have kind "decimal"
- assert field.value.kind == "decimal"
- assert float(field.value.value) == pytest.approx(19.99)
-
- def test_parse_map_literal(self):
- """Map literal with #{ ... } syntax parses to MapLiteral."""
- src = """
-node @cfg :: Config {
- data = #{ "key": "value" }
-}
-"""
- prog = parse(src)
- field = prog.nodes[0].fields[0]
- from kndl.ast_nodes import MapLiteral
- assert isinstance(field.value, MapLiteral)
- assert len(field.value.pairs) == 1
-
- def test_parse_process_decl(self):
- """Basic process declaration parses without error."""
- src = """
-process @order_flow :: OrderProcess {
-}
-"""
- prog = parse(src)
- assert len(prog.processes) == 1
- p = prog.processes[0]
- assert p.ref.name == "order_flow"
- assert p.type_name == "OrderProcess"
-
- def test_parse_process_with_states_and_transitions(self):
- """Process with states and transitions parses correctly."""
- src = """
-process @checkout :: CheckoutFlow {
- state pending {}
- state confirmed {}
- on payment_received in pending -> confirmed
-}
-"""
- prog = parse(src)
- assert len(prog.processes) == 1
- p = prog.processes[0]
- assert len(p.states) == 2
- assert p.states[0].name == "pending"
- assert p.states[1].name == "confirmed"
- assert len(p.transitions) == 1
- t = p.transitions[0]
- assert t.event == "payment_received"
- assert t.from_state == "pending"
- assert t.to_state == "confirmed"
-
- def test_parse_group_by_query(self):
- """Query with top-level group by clause parses correctly."""
- src = """
-query findByType {
- match ?n :: Node
- return ?n
- group by ?n
-}
-"""
- prog = parse(src)
- assert len(prog.queries) == 1
- q = prog.queries[0]
- assert len(q.group_by) == 1
-
- def test_parse_reverse_edge(self):
- """Reverse-directed edge <-[T]- parses with direction='reverse'."""
- src = """
-edge @b <-[depends_on]- @a
-"""
- prog = parse(src)
- assert len(prog.edges) == 1
- e = prog.edges[0]
- assert e.edge_type == "depends_on"
- assert e.direction == "reverse"
- assert e.source.name == "b"
- assert e.targets[0].name == "a"
-
- def test_parse_process_meta(self):
- """Process declaration with meta-annotations parses correctly."""
- src = """
-process @flow :: MyFlow {
- ~confidence 0.9
- state idle {}
-}
-"""
- prog = parse(src)
- p = prog.processes[0]
- assert len(p.meta) == 1
- assert p.meta[0].key == "confidence"
-
- def test_parse_multiple_group_by_exprs(self):
- """group by with multiple comma-separated expressions."""
- src = """
-query multiGroup {
- match ?n :: Node
- return ?n
- group by ?n, ?n
-}
-"""
- prog = parse(src)
- q = prog.queries[0]
- assert len(q.group_by) == 2
-
- def test_parse_map_literal_multiple_pairs(self):
- """Map literal with multiple key-value pairs."""
- src = """
-node @cfg :: Config {
- data = #{ "k1": "v1", "k2": "v2" }
-}
-"""
- prog = parse(src)
- from kndl.ast_nodes import MapLiteral
- field = prog.nodes[0].fields[0]
- assert isinstance(field.value, MapLiteral)
- assert len(field.value.pairs) == 2
-
-
-# ─────────────────────────────────────────────
-# Compiler tests
-# ─────────────────────────────────────────────
-
-class TestCompilerV02:
- def test_compile_negated_meta(self):
- """~negated true compiles to meta.negated = True."""
- src = """
-node @fact :: Statement {
- ~negated true
-}
-"""
- graph = compile(src)
- node = graph.get_node("fact")
- assert node is not None
- assert node.meta.negated is True
-
- def test_compile_recorded_meta(self):
- """~recorded compiles to meta.recorded."""
- src = """
-node @obs :: Observation {
- ~recorded "2026-04-22T10:00Z"
-}
-"""
- graph = compile(src)
- node = graph.get_node("obs")
- assert node.meta.recorded == "2026-04-22T10:00Z"
-
- def test_compile_observed_meta(self):
- """~observed compiles to meta.observed."""
- src = """
-node @sensor_r :: Reading {
- ~observed "2026-04-22T09:00Z"
-}
-"""
- graph = compile(src)
- node = graph.get_node("sensor_r")
- assert node.meta.observed == "2026-04-22T09:00Z"
-
- def test_compile_deadline_meta(self):
- """~deadline compiles to meta.deadline."""
- src = """
-node @task1 :: Task {
- ~deadline "2026-05-01"
-}
-"""
- graph = compile(src)
- node = graph.get_node("task1")
- assert node.meta.deadline == "2026-05-01"
-
- def test_compile_classification_meta(self):
- """~classification compiles to meta.classification."""
- src = """
-node @doc1 :: Document {
- ~classification "confidential"
-}
-"""
- graph = compile(src)
- node = graph.get_node("doc1")
- assert node.meta.classification == "confidential"
-
- def test_compile_process(self):
- """Process declaration compiles into graph.processes."""
- src = """
-process @order_flow :: OrderProcess {
-}
-"""
- graph = compile(src)
- assert "order_flow" in graph.processes
- p = graph.processes["order_flow"]
- assert p["type"] == "OrderProcess"
-
- def test_compile_process_states(self):
- """Process states compile into the processes dict."""
- src = """
-process @checkout :: CheckoutFlow {
- state pending {}
- state confirmed {}
-}
-"""
- graph = compile(src)
- p = graph.processes["checkout"]
- assert len(p["states"]) == 2
- state_names = [s["name"] for s in p["states"]]
- assert "pending" in state_names
- assert "confirmed" in state_names
-
- def test_compile_process_transitions(self):
- """Process transitions compile correctly."""
- src = """
-process @checkout :: CheckoutFlow {
- state pending {}
- state confirmed {}
- on payment_received in pending -> confirmed
-}
-"""
- graph = compile(src)
- p = graph.processes["checkout"]
- assert len(p["transitions"]) == 1
- t = p["transitions"][0]
- assert t["event"] == "payment_received"
- assert t["from"] == "pending"
- assert t["to"] == "confirmed"
-
- def test_compile_process_in_graph(self):
- """Compiled process appears in graph.to_dict()."""
- src = """
-process @flow1 :: MyFlow {
- state initial {}
-}
-"""
- graph = compile(src)
- d = graph.to_dict()
- assert "processes" in d
- assert "flow1" in d["processes"]
-
- def test_compile_negated_false_by_default(self):
- """meta.negated defaults to False when not set."""
- src = """
-node @x :: Thing {}
-"""
- graph = compile(src)
- node = graph.get_node("x")
- assert node.meta.negated is False
-
- def test_compile_retention_meta(self):
- """~retention compiles to meta.retention."""
- src = """
-node @log1 :: LogEntry {
- ~retention "90d"
-}
-"""
- graph = compile(src)
- node = graph.get_node("log1")
- assert node.meta.retention == "90d"
-
-
-# ─────────────────────────────────────────────
-# Serializer tests
-# ─────────────────────────────────────────────
-
-class TestSerializerV02:
- def test_serialize_negated_meta(self):
- """Negated meta serializes to ~negated true."""
- meta = KNDLMeta(negated=True)
- from kndl.serializer import _serialize_meta
- lines = _serialize_meta(meta)
- assert any("~negated" in ln and "true" in ln for ln in lines)
-
- def test_serialize_recorded_meta(self):
- """Recorded meta serializes to ~recorded."""
- meta = KNDLMeta(recorded="2026-04-22T10:00Z")
- from kndl.serializer import _serialize_meta
- lines = _serialize_meta(meta)
- assert any("~recorded" in ln for ln in lines)
-
- def test_serialize_observed_meta(self):
- """Observed meta serializes to ~observed."""
- meta = KNDLMeta(observed="2026-04-22T09:00Z")
- from kndl.serializer import _serialize_meta
- lines = _serialize_meta(meta)
- assert any("~observed" in ln for ln in lines)
-
- def test_serialize_deadline_meta(self):
- """Deadline meta serializes to ~deadline."""
- meta = KNDLMeta(deadline="2026-05-01")
- from kndl.serializer import _serialize_meta
- lines = _serialize_meta(meta)
- assert any("~deadline" in ln for ln in lines)
-
- def test_serialize_classification_meta(self):
- """Classification meta serializes to ~classification."""
- meta = KNDLMeta(classification="confidential")
- from kndl.serializer import _serialize_meta
- lines = _serialize_meta(meta)
- assert any("~classification" in ln for ln in lines)
-
- def test_roundtrip_with_v02_meta(self):
- """Node with v0.2 meta survives a parse → compile → serialize roundtrip."""
- src = """
-node @sensor_a :: Reading {
- value = 42
- ~confidence 0.85
- ~recorded "2026-04-22T10:00Z"
- ~negated false
-}
-"""
- graph = compile(src)
- text = serialize(graph)
- # Reparse and recompile the serialized form
- graph2 = compile(text)
- node = graph2.get_node("sensor_a")
- assert node is not None
- assert node.meta.confidence == pytest.approx(0.85)
- assert node.meta.recorded == "2026-04-22T10:00Z"
-
- def test_negated_false_not_serialized(self):
- """meta.negated == False does not emit ~negated line."""
- meta = KNDLMeta(negated=False)
- from kndl.serializer import _serialize_meta
- lines = _serialize_meta(meta)
- assert not any("~negated" in ln for ln in lines)
-
- def test_serialize_v02_meta_to_dict_roundtrip(self):
- """v0.2 meta fields survive to_dict / from_dict roundtrip."""
- meta = KNDLMeta(
- recorded="2026-04-22",
- observed="2026-04-21",
- negated=True,
- deadline="2026-05-01",
- classification="secret",
- retention="30d",
- )
- d = meta.to_dict()
- meta2 = KNDLMeta.from_dict(d)
- assert meta2.recorded == "2026-04-22"
- assert meta2.observed == "2026-04-21"
- assert meta2.negated is True
- assert meta2.deadline == "2026-05-01"
- assert meta2.classification == "secret"
- assert meta2.retention == "30d"
-
-
-# ─────────────────────────────────────────────
-# Version check
-# ─────────────────────────────────────────────
-
-class TestVersion:
- def test_version_is_v02(self):
- import kndl
- assert kndl.__version__ == "1.0.0"
-
-
-# ─────────────────────────────────────────────
-# Literal type tests (§2.8, §3.1)
-# ─────────────────────────────────────────────
-
-class TestLiteralTypes:
- # ── Bytes (§2.8.11) ──────────────────────
-
- def test_bytes_token(self):
- tokens = tokenize('b"SGVsbG8="')
- assert tokens[0].type == TokenType.BYTES
- assert tokens[0].value == "SGVsbG8="
-
- def test_bytes_in_node_field(self):
- src = 'node @n :: T { payload = b"SGVsbG8=" }'
- program = parse(src)
- field = program.nodes[0].fields[0]
- assert field.value.kind == "bytes"
- assert field.value.value == "SGVsbG8="
-
- def test_bytes_compile(self):
- src = 'node @n :: T { payload = b"SGVsbG8=" }'
- graph = compile(src)
- assert graph.nodes["n"].fields["payload"] == "SGVsbG8="
-
- def test_bytes_serialize(self):
- from kndl.serializer import _format_value
- # Bytes are stored as plain strings; serializer wraps in quotes
- assert _format_value("SGVsbG8=") == '"SGVsbG8="'
-
- # ── Vector (§2.8.12) ─────────────────────
-
- def test_vector_token(self):
- tokens = tokenize("v[0.12, -0.03, 0.91]")
- assert tokens[0].type == TokenType.VECTOR
- assert tokens[0].value == "0.12, -0.03, 0.91"
-
- def test_vector_in_node_field(self):
- src = "node @n :: T { embedding = v[0.1, 0.2, 0.3] }"
- program = parse(src)
- field = program.nodes[0].fields[0]
- assert field.value.kind == "vector"
- assert field.value.value == pytest.approx([0.1, 0.2, 0.3])
-
- def test_vector_compile(self):
- src = "node @n :: T { embedding = v[0.1, 0.2, 0.3] }"
- graph = compile(src)
- assert graph.nodes["n"].fields["embedding"] == pytest.approx([0.1, 0.2, 0.3])
-
- def test_vector_serialize(self):
- from kndl.serializer import _format_value
- assert _format_value([0.1, 0.2, 0.3]) == "v[ 0.1, 0.2, 0.3 ]"
-
- def test_vector_roundtrip(self):
- src = "node @n :: T { embedding = v[0.5, -0.5, 1.0] }"
- g1 = compile(src)
- text = serialize(g1)
- g2 = compile(text)
- assert g2.nodes["n"].fields["embedding"] == pytest.approx([0.5, -0.5, 1.0])
-
- # ── Money (§2.8.10) ──────────────────────
-
- def test_money_token_decimal_plus_code(self):
- tokens = tokenize("19.99d USD")
- assert tokens[0].type == TokenType.DECIMAL
- assert tokens[1].type == TokenType.IDENTIFIER
- assert tokens[1].value == "USD"
-
- def test_money_literal_parse(self):
- src = "node @n :: T { price = 19.99d USD }"
- program = parse(src)
- field = program.nodes[0].fields[0]
- assert field.value.kind == "money"
- assert field.value.value["currency"] == "USD"
- assert field.value.value["amount"] == pytest.approx(19.99)
-
- def test_money_compile(self):
- src = "node @n :: T { price = 19.99d USD }"
- graph = compile(src)
- price = graph.nodes["n"].fields["price"]
- assert isinstance(price, dict)
- assert price["currency"] == "USD"
- assert price["amount"] == pytest.approx(19.99)
-
- def test_money_serialize(self):
- from kndl.serializer import _format_value
- result = _format_value({"amount": 19.99, "currency": "EUR"})
- assert "EUR" in result
- assert "19.99" in result
-
- def test_money_roundtrip(self):
- src = "node @n :: T { price = 100.00d EUR }"
- g1 = compile(src)
- text = serialize(g1)
- g2 = compile(text)
- price = g2.nodes["n"].fields["price"]
- assert price["currency"] == "EUR"
- assert price["amount"] == pytest.approx(100.0)
-
- def test_decimal_without_currency_stays_decimal(self):
- src = "node @n :: T { rate = 0.05d }"
- program = parse(src)
- field = program.nodes[0].fields[0]
- assert field.value.kind == "decimal"
-
- # ── Quantity (§2.8.9) ────────────────────
-
- def test_quantity_temperature(self):
- src = "node @n :: T { temp = 22.5 °C }"
- program = parse(src)
- field = program.nodes[0].fields[0]
- assert field.value.kind == "quantity"
- assert field.value.value["magnitude"] == pytest.approx(22.5)
- assert field.value.value["unit"] == "°C"
-
- def test_quantity_compile(self):
- src = "node @sensor :: Reading { value = 22.5 °C }"
- graph = compile(src)
- v = graph.nodes["sensor"].fields["value"]
- assert isinstance(v, dict)
- assert v["unit"] == "°C"
- assert v["magnitude"] == pytest.approx(22.5)
-
- def test_quantity_integer_magnitude(self):
- src = "node @n :: T { dist = 5 km }"
- program = parse(src)
- field = program.nodes[0].fields[0]
- assert field.value.kind == "quantity"
- assert field.value.value["unit"] == "km"
-
- def test_quantity_composite_unit(self):
- src = "node @n :: T { speed = 5.0 m/s }"
- program = parse(src)
- field = program.nodes[0].fields[0]
- assert field.value.kind == "quantity"
- assert "m" in field.value.value["unit"]
- assert "s" in field.value.value["unit"]
-
- def test_quantity_serialize(self):
- from kndl.serializer import _format_value
- result = _format_value({"magnitude": 22.5, "unit": "°C"})
- assert "22.5" in result
- assert "°C" in result
-
- def test_quantity_roundtrip(self):
- src = "node @n :: T { temp = 22.5 °C }"
- g1 = compile(src)
- text = serialize(g1)
- g2 = compile(text)
- v = g2.nodes["n"].fields["temp"]
- assert v["magnitude"] == pytest.approx(22.5)
- assert v["unit"] == "°C"
-
- def test_identifier_not_confused_with_quantity_unit(self):
- # 'label' is not a unit atom — field named 'label' after int must not
- # be treated as a quantity unit.
- src = """node @n :: T {
- total = 5
- label = "foo"
- }"""
- graph = compile(src)
- assert graph.nodes["n"].fields["total"] == 5
- assert graph.nodes["n"].fields["label"] == "foo"
-
- # ── UUID (§3.1 type table) ────────────────
-
- def test_uuid_token(self):
- tokens = tokenize('u"01890000-0000-0000-0000-000000000001"')
- assert tokens[0].type == TokenType.UUID
- assert "0189" in tokens[0].value
-
- def test_uuid_in_node_field(self):
- src = 'node @n :: T { id = u"01890000-0000-0000-0000-000000000001" }'
- program = parse(src)
- field = program.nodes[0].fields[0]
- assert field.value.kind == "uuid"
- assert "0189" in field.value.value
-
- def test_uuid_compile(self):
- src = 'node @n :: T { id = u"01890000-0000-0000-0000-000000000001" }'
- graph = compile(src)
- assert "0189" in graph.nodes["n"].fields["id"]
-
- # ── Degree-sign lexer edge case ───────────
-
- def test_degree_symbol_lexes_as_identifier(self):
- tokens = tokenize("°C")
- assert tokens[0].type == TokenType.IDENTIFIER
- assert tokens[0].value == "°C"
-
- def test_degree_F_lexes_as_identifier(self):
- tokens = tokenize("°F")
- assert tokens[0].type == TokenType.IDENTIFIER
- assert tokens[0].value == "°F"
diff --git a/packages/python/tests/test_storage.py b/packages/python/tests/test_storage.py
deleted file mode 100644
index a9684bc..0000000
--- a/packages/python/tests/test_storage.py
+++ /dev/null
@@ -1,313 +0,0 @@
-"""
-Storage backend tests — SQLite in-memory CRUD, persistence roundtrip,
-create_storage() factory, and KNDLGraph.from_storage() / remove_intent().
-"""
-
-from __future__ import annotations
-
-import os
-import sys
-
-sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
-
-import pytest
-
-from kndl.graph import GraphEdge, GraphIntent, GraphNode, KNDLGraph, KNDLMeta
-from kndl.storage import KNDLStorage, create_storage
-from kndl.backends.sqlite_backend import SQLiteStorage
-
-
-# ── Helpers ───────────────────────────────────────────────────────────────────
-
-def _mem_storage() -> SQLiteStorage:
- return SQLiteStorage("sqlite:///:memory:")
-
-
-def _sample_node(node_id: str = "n1", type_name: str = "Temperature") -> GraphNode:
- return GraphNode(
- id=node_id,
- type_name=type_name,
- fields={"value": 22.5, "unit": "°C"},
- meta=KNDLMeta(confidence=0.9, source="sensor://test"),
- )
-
-
-def _sample_edge(edge_id: str = "e1") -> GraphEdge:
- return GraphEdge(
- id=edge_id,
- source_id="n1",
- target_id="n2",
- edge_type="located_in",
- fields={"weight": 0.8},
- )
-
-
-def _sample_intent(intent_id: str = "i1") -> GraphIntent:
- return GraphIntent(
- id=intent_id,
- type_name="Action",
- trigger_kind="expression",
- trigger_data="@n1.value > 30",
- actions=[{"type": "emit", "node_type": "Alert"}],
- meta=KNDLMeta(priority=0.9),
- )
-
-
-# ── Protocol conformance ──────────────────────────────────────────────────────
-
-class TestStorageProtocol:
- def test_sqlite_implements_protocol(self) -> None:
- s = _mem_storage()
- assert isinstance(s, KNDLStorage)
- s.close()
-
-
-# ── create_storage factory ────────────────────────────────────────────────────
-
-class TestCreateStorage:
- def test_none_returns_none(self) -> None:
- assert create_storage("") is None
-
- def test_memory_string_returns_none(self) -> None:
- assert create_storage("memory") is None
- assert create_storage("MEMORY") is None
- assert create_storage("none") is None
-
- def test_sqlite_in_memory(self) -> None:
- s = create_storage("sqlite:///:memory:")
- assert s is not None
- assert isinstance(s, SQLiteStorage)
- s.close()
-
- def test_sqlite_file(self, tmp_path) -> None: # type: ignore[no-untyped-def]
- db = tmp_path / "test.db"
- s = create_storage(f"sqlite:///{db}")
- assert s is not None
- s.close()
-
- def test_unsupported_scheme_raises(self) -> None:
- with pytest.raises(ValueError, match="Unsupported DATABASE_URL"):
- create_storage("mysql://localhost/db")
-
- def test_reads_env_var(self, monkeypatch) -> None: # type: ignore[no-untyped-def]
- monkeypatch.setenv("DATABASE_URL", "sqlite:///:memory:")
- s = create_storage()
- assert s is not None
- s.close()
-
- def test_explicit_url_overrides_env(self, monkeypatch) -> None: # type: ignore[no-untyped-def]
- monkeypatch.setenv("DATABASE_URL", "postgresql://never/used")
- s = create_storage("sqlite:///:memory:")
- assert isinstance(s, SQLiteStorage)
- s.close()
-
-
-# ── SQLite CRUD ───────────────────────────────────────────────────────────────
-
-class TestSQLiteCRUD:
- def test_node_upsert_and_load(self) -> None:
- s = _mem_storage()
- node = _sample_node()
- s.upsert_node(node)
- nodes, edges, intents = s.load()
- assert len(nodes) == 1
- assert nodes[0]["id"] == "n1"
- assert nodes[0]["type"] == "Temperature"
- assert nodes[0]["fields"]["value"] == 22.5
- assert nodes[0]["meta"]["confidence"] == 0.9
- s.close()
-
- def test_node_upsert_replaces(self) -> None:
- s = _mem_storage()
- node = _sample_node()
- s.upsert_node(node)
- node.fields["value"] = 99.0
- s.upsert_node(node)
- nodes, _, _ = s.load()
- assert len(nodes) == 1
- assert nodes[0]["fields"]["value"] == 99.0
- s.close()
-
- def test_node_delete(self) -> None:
- s = _mem_storage()
- s.upsert_node(_sample_node())
- s.delete_node("n1")
- nodes, _, _ = s.load()
- assert nodes == []
- s.close()
-
- def test_edge_upsert_and_load(self) -> None:
- s = _mem_storage()
- s.upsert_edge(_sample_edge())
- _, edges, _ = s.load()
- assert len(edges) == 1
- e = edges[0]
- assert e["id"] == "e1"
- assert e["source"] == "n1"
- assert e["target"] == "n2"
- assert e["type"] == "located_in"
- s.close()
-
- def test_edge_delete(self) -> None:
- s = _mem_storage()
- s.upsert_edge(_sample_edge())
- s.delete_edge("e1")
- _, edges, _ = s.load()
- assert edges == []
- s.close()
-
- def test_intent_upsert_and_load(self) -> None:
- s = _mem_storage()
- s.upsert_intent(_sample_intent())
- _, _, intents = s.load()
- assert len(intents) == 1
- i = intents[0]
- assert i["id"] == "i1"
- assert i["type"] == "Action"
- assert i["trigger_kind"] == "expression"
- assert i["trigger_data"] == "@n1.value > 30"
- assert len(i["actions"]) == 1
- s.close()
-
- def test_intent_delete(self) -> None:
- s = _mem_storage()
- s.upsert_intent(_sample_intent())
- s.delete_intent("i1")
- _, _, intents = s.load()
- assert intents == []
- s.close()
-
- def test_clear(self) -> None:
- s = _mem_storage()
- s.upsert_node(_sample_node())
- s.upsert_edge(_sample_edge())
- s.upsert_intent(_sample_intent())
- s.clear()
- nodes, edges, intents = s.load()
- assert nodes == edges == intents == []
- s.close()
-
- def test_multiple_items(self) -> None:
- s = _mem_storage()
- for i in range(5):
- s.upsert_node(_sample_node(f"node_{i}", "Sensor"))
- nodes, _, _ = s.load()
- assert len(nodes) == 5
- s.close()
-
-
-# ── Persistence roundtrip ─────────────────────────────────────────────────────
-
-class TestPersistenceRoundtrip:
- def test_file_survives_reopen(self, tmp_path) -> None: # type: ignore[no-untyped-def]
- db_url = f"sqlite:///{tmp_path / 'kndl_test.db'}"
-
- s1 = SQLiteStorage(db_url)
- s1.upsert_node(_sample_node("persist_node"))
- s1.upsert_edge(_sample_edge("persist_edge"))
- s1.upsert_intent(_sample_intent("persist_intent"))
- s1.close()
-
- s2 = SQLiteStorage(db_url)
- nodes, edges, intents = s2.load()
- assert any(n["id"] == "persist_node" for n in nodes)
- assert any(e["id"] == "persist_edge" for e in edges)
- assert any(i["id"] == "persist_intent" for i in intents)
- s2.close()
-
- def test_graph_from_storage_loads_data(self) -> None:
- s = _mem_storage()
- s.upsert_node(_sample_node("loaded_node"))
- s.upsert_edge(_sample_edge("loaded_edge"))
- s.upsert_intent(_sample_intent("loaded_intent"))
-
- g = KNDLGraph.from_storage(s)
- assert "loaded_node" in g.nodes
- assert "loaded_edge" in g.edges
- assert "loaded_intent" in g.intents
- s.close()
-
- def test_graph_changes_persist(self) -> None:
- s = _mem_storage()
- g = KNDLGraph(storage=s)
-
- g.add_node(_sample_node("p1"))
- g.add_edge(_sample_edge("pe1"))
- g.add_intent(_sample_intent("pi1"))
-
- nodes, edges, intents = s.load()
- assert any(n["id"] == "p1" for n in nodes)
- assert any(e["id"] == "pe1" for e in edges)
- assert any(i["id"] == "pi1" for i in intents)
-
- g.remove_node("p1")
- nodes, _, _ = s.load()
- assert not any(n["id"] == "p1" for n in nodes)
-
- g.remove_edge("pe1")
- _, edges, _ = s.load()
- assert not any(e["id"] == "pe1" for e in edges)
-
- g.remove_intent("pi1")
- _, _, intents = s.load()
- assert not any(i["id"] == "pi1" for i in intents)
-
- s.close()
-
-
-# ── KNDLGraph.remove_intent ───────────────────────────────────────────────────
-
-class TestRemoveIntent:
- def test_remove_intent_in_memory(self) -> None:
- g = KNDLGraph()
- g.add_intent(_sample_intent("ri1"))
- assert "ri1" in g.intents
- result = g.remove_intent("ri1")
- assert result is True
- assert "ri1" not in g.intents
-
- def test_remove_missing_intent_returns_false(self) -> None:
- g = KNDLGraph()
- assert g.remove_intent("nonexistent") is False
-
- def test_remove_intent_with_storage(self) -> None:
- s = _mem_storage()
- g = KNDLGraph(storage=s)
- g.add_intent(_sample_intent("si1"))
- _, _, intents = s.load()
- assert any(i["id"] == "si1" for i in intents)
-
- g.remove_intent("si1")
- _, _, intents = s.load()
- assert not any(i["id"] == "si1" for i in intents)
- s.close()
-
-
-# ── Meta roundtrip ────────────────────────────────────────────────────────────
-
-class TestMetaRoundtrip:
- def test_full_meta_survives_storage(self) -> None:
- s = _mem_storage()
- node = GraphNode(
- id="meta_test",
- type_name="Sensor",
- fields={"v": 1},
- meta=KNDLMeta(
- confidence=0.75,
- source="agent://test",
- valid_start="2026-01-01T00:00:00Z",
- valid_end="2026-12-31T23:59:59Z",
- decay_rate=0.9,
- decay_duration_seconds=3600.0,
- tags=["iot", "temperature"],
- priority=0.8,
- ),
- )
- s.upsert_node(node)
- nodes, _, _ = s.load()
- m = nodes[0]["meta"]
- assert m["confidence"] == 0.75
- assert m["source"] == "agent://test"
- assert m["tags"] == ["iot", "temperature"]
- assert m["decay_rate"] == 0.9
- s.close()
diff --git a/packages/python/uv.lock b/packages/python/uv.lock
deleted file mode 100644
index dc10031..0000000
--- a/packages/python/uv.lock
+++ /dev/null
@@ -1,399 +0,0 @@
-version = 1
-revision = 3
-requires-python = ">=3.12"
-
-[[package]]
-name = "colorama"
-version = "0.4.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
-]
-
-[[package]]
-name = "coverage"
-version = "7.13.5"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" },
- { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" },
- { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" },
- { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" },
- { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" },
- { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" },
- { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" },
- { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" },
- { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" },
- { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" },
- { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" },
- { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" },
- { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" },
- { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" },
- { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" },
- { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" },
- { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" },
- { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" },
- { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" },
- { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" },
- { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" },
- { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" },
- { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" },
- { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" },
- { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" },
- { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" },
- { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" },
- { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" },
- { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" },
- { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" },
- { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" },
- { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" },
- { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" },
- { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" },
- { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" },
- { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" },
- { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" },
- { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" },
- { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" },
- { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" },
- { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" },
- { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" },
- { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" },
- { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" },
- { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" },
- { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" },
- { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" },
- { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" },
- { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" },
- { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" },
- { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" },
- { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" },
- { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" },
- { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" },
- { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" },
- { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" },
- { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" },
- { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" },
- { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" },
- { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" },
- { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" },
- { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" },
- { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" },
- { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" },
- { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" },
- { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" },
- { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" },
- { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" },
- { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" },
- { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" },
- { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" },
- { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" },
- { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" },
- { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" },
- { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" },
- { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" },
-]
-
-[[package]]
-name = "iniconfig"
-version = "2.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
-]
-
-[[package]]
-name = "kndl"
-version = "0.1.0"
-source = { editable = "." }
-
-[package.optional-dependencies]
-dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "pytest-cov" },
- { name = "python-dotenv" },
- { name = "ruff" },
-]
-dotenv = [
- { name = "python-dotenv" },
-]
-postgres = [
- { name = "psycopg2-binary" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.9" },
- { name = "psycopg2-binary", marker = "extra == 'postgres'", specifier = ">=2.9" },
- { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
- { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" },
- { name = "python-dotenv", marker = "extra == 'dev'", specifier = ">=1.0" },
- { name = "python-dotenv", marker = "extra == 'dotenv'", specifier = ">=1.0" },
- { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4" },
-]
-provides-extras = ["postgres", "dotenv", "dev"]
-
-[[package]]
-name = "librt"
-version = "0.9.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" },
- { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" },
- { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" },
- { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" },
- { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" },
- { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" },
- { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" },
- { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" },
- { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" },
- { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" },
- { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" },
- { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" },
- { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" },
- { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" },
- { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" },
- { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" },
- { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" },
- { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" },
- { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" },
- { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" },
- { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" },
- { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" },
- { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" },
- { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" },
- { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" },
- { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" },
- { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" },
- { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" },
- { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" },
- { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" },
- { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" },
- { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" },
- { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" },
- { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" },
- { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" },
- { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" },
- { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" },
- { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" },
- { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" },
- { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" },
- { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" },
- { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" },
- { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" },
- { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" },
- { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" },
- { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" },
- { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" },
- { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" },
- { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" },
- { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" },
- { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" },
- { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" },
-]
-
-[[package]]
-name = "mypy"
-version = "1.20.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
- { name = "mypy-extensions" },
- { name = "pathspec" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/b0089fe7fef0a994ae5ee07029ced0526082c6cfaaa4c10d40a10e33b097/mypy-1.20.0.tar.gz", hash = "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3", size = 3815028, upload-time = "2026-03-31T16:55:14.959Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/be/dd/3afa29b58c2e57c79116ed55d700721c3c3b15955e2b6251dd165d377c0e/mypy-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:002b613ae19f4ac7d18b7e168ffe1cb9013b37c57f7411984abbd3b817b0a214", size = 14509525, upload-time = "2026-03-31T16:55:01.824Z" },
- { url = "https://files.pythonhosted.org/packages/54/eb/227b516ab8cad9f2a13c5e7a98d28cd6aa75e9c83e82776ae6c1c4c046c7/mypy-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9336b5e6712f4adaf5afc3203a99a40b379049104349d747eb3e5a3aa23ac2e", size = 13326469, upload-time = "2026-03-31T16:51:41.23Z" },
- { url = "https://files.pythonhosted.org/packages/57/d4/1ddb799860c1b5ac6117ec307b965f65deeb47044395ff01ab793248a591/mypy-1.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f13b3e41bce9d257eded794c0f12878af3129d80aacd8a3ee0dee51f3a978651", size = 13705953, upload-time = "2026-03-31T16:48:55.69Z" },
- { url = "https://files.pythonhosted.org/packages/c5/b7/54a720f565a87b893182a2a393370289ae7149e4715859e10e1c05e49154/mypy-1.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9804c3ad27f78e54e58b32e7cb532d128b43dbfb9f3f9f06262b821a0f6bd3f5", size = 14710363, upload-time = "2026-03-31T16:53:26.948Z" },
- { url = "https://files.pythonhosted.org/packages/b2/2a/74810274848d061f8a8ea4ac23aaad43bd3d8c1882457999c2e568341c57/mypy-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:697f102c5c1d526bdd761a69f17c6070f9892eebcb94b1a5963d679288c09e78", size = 14947005, upload-time = "2026-03-31T16:50:17.591Z" },
- { url = "https://files.pythonhosted.org/packages/77/91/21b8ba75f958bcda75690951ce6fa6b7138b03471618959529d74b8544e2/mypy-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ecd63f75fdd30327e4ad8b5704bd6d91fc6c1b2e029f8ee14705e1207212489", size = 10880616, upload-time = "2026-03-31T16:52:19.986Z" },
- { url = "https://files.pythonhosted.org/packages/8a/15/3d8198ef97c1ca03aea010cce4f1d4f3bc5d9849e8c0140111ca2ead9fdd/mypy-1.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:f194db59657c58593a3c47c6dfd7bad4ef4ac12dbc94d01b3a95521f78177e33", size = 9813091, upload-time = "2026-03-31T16:53:44.385Z" },
- { url = "https://files.pythonhosted.org/packages/d6/a7/f64ea7bd592fa431cb597418b6dec4a47f7d0c36325fec7ac67bc8402b94/mypy-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b20c8b0fd5877abdf402e79a3af987053de07e6fb208c18df6659f708b535134", size = 14485344, upload-time = "2026-03-31T16:49:16.78Z" },
- { url = "https://files.pythonhosted.org/packages/bb/72/8927d84cfc90c6abea6e96663576e2e417589347eb538749a464c4c218a0/mypy-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:367e5c993ba34d5054d11937d0485ad6dfc60ba760fa326c01090fc256adf15c", size = 13327400, upload-time = "2026-03-31T16:53:08.02Z" },
- { url = "https://files.pythonhosted.org/packages/ab/4a/11ab99f9afa41aa350178d24a7d2da17043228ea10f6456523f64b5a6cf6/mypy-1.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f799d9db89fc00446f03281f84a221e50018fc40113a3ba9864b132895619ebe", size = 13706384, upload-time = "2026-03-31T16:52:28.577Z" },
- { url = "https://files.pythonhosted.org/packages/42/79/694ca73979cfb3535ebfe78733844cd5aff2e63304f59bf90585110d975a/mypy-1.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555658c611099455b2da507582ea20d2043dfdfe7f5ad0add472b1c6238b433f", size = 14700378, upload-time = "2026-03-31T16:48:45.527Z" },
- { url = "https://files.pythonhosted.org/packages/84/24/a022ccab3a46e3d2cdf2e0e260648633640eb396c7e75d5a42818a8d3971/mypy-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:efe8d70949c3023698c3fca1e94527e7e790a361ab8116f90d11221421cd8726", size = 14932170, upload-time = "2026-03-31T16:49:36.038Z" },
- { url = "https://files.pythonhosted.org/packages/d8/9b/549228d88f574d04117e736f55958bd4908f980f9f5700a07aeb85df005b/mypy-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:f49590891d2c2f8a9de15614e32e459a794bcba84693c2394291a2038bbaaa69", size = 10888526, upload-time = "2026-03-31T16:50:59.827Z" },
- { url = "https://files.pythonhosted.org/packages/91/17/15095c0e54a8bc04d22d4ff06b2139d5f142c2e87520b4e39010c4862771/mypy-1.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:76a70bf840495729be47510856b978f1b0ec7d08f257ca38c9d932720bf6b43e", size = 9816456, upload-time = "2026-03-31T16:49:59.537Z" },
- { url = "https://files.pythonhosted.org/packages/4e/0e/6ca4a84cbed9e62384bc0b2974c90395ece5ed672393e553996501625fc5/mypy-1.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0f42dfaab7ec1baff3b383ad7af562ab0de573c5f6edb44b2dab016082b89948", size = 14483331, upload-time = "2026-03-31T16:52:57.999Z" },
- { url = "https://files.pythonhosted.org/packages/7d/c5/5fe9d8a729dd9605064691816243ae6c49fde0bd28f6e5e17f6a24203c43/mypy-1.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:31b5dbb55293c1bd27c0fc813a0d2bb5ceef9d65ac5afa2e58f829dab7921fd5", size = 13342047, upload-time = "2026-03-31T16:54:21.555Z" },
- { url = "https://files.pythonhosted.org/packages/4c/33/e18bcfa338ca4e6b2771c85d4c5203e627d0c69d9de5c1a2cf2ba13320ba/mypy-1.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49d11c6f573a5a08f77fad13faff2139f6d0730ebed2cfa9b3d2702671dd7188", size = 13719585, upload-time = "2026-03-31T16:51:53.89Z" },
- { url = "https://files.pythonhosted.org/packages/6b/8d/93491ff7b79419edc7eabf95cb3b3f7490e2e574b2855c7c7e7394ff933f/mypy-1.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d3243c406773185144527f83be0e0aefc7bf4601b0b2b956665608bf7c98a83", size = 14685075, upload-time = "2026-03-31T16:54:04.464Z" },
- { url = "https://files.pythonhosted.org/packages/b5/9d/d924b38a4923f8d164bf2b4ec98bf13beaf6e10a5348b4b137eadae40a6e/mypy-1.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a79c1eba7ac4209f2d850f0edd0a2f8bba88cbfdfefe6fb76a19e9d4fe5e71a2", size = 14919141, upload-time = "2026-03-31T16:54:51.785Z" },
- { url = "https://files.pythonhosted.org/packages/59/98/1da9977016678c0b99d43afe52ed00bb3c1a0c4c995d3e6acca1a6ebb9b4/mypy-1.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:00e047c74d3ec6e71a2eb88e9ea551a2edb90c21f993aefa9e0d2a898e0bb732", size = 11050925, upload-time = "2026-03-31T16:51:30.758Z" },
- { url = "https://files.pythonhosted.org/packages/5e/e3/ba0b7a3143e49a9c4f5967dde6ea4bf8e0b10ecbbcca69af84027160ee89/mypy-1.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:931a7630bba591593dcf6e97224a21ff80fb357e7982628d25e3c618e7f598ef", size = 10001089, upload-time = "2026-03-31T16:49:43.632Z" },
- { url = "https://files.pythonhosted.org/packages/12/28/e617e67b3be9d213cda7277913269c874eb26472489f95d09d89765ce2d8/mypy-1.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:26c8b52627b6552f47ff11adb4e1509605f094e29815323e487fc0053ebe93d1", size = 15534710, upload-time = "2026-03-31T16:52:12.506Z" },
- { url = "https://files.pythonhosted.org/packages/6e/0c/3b5f2d3e45dc7169b811adce8451679d9430399d03b168f9b0489f43adaa/mypy-1.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39362cdb4ba5f916e7976fccecaab1ba3a83e35f60fa68b64e9a70e221bb2436", size = 14393013, upload-time = "2026-03-31T16:54:41.186Z" },
- { url = "https://files.pythonhosted.org/packages/a3/49/edc8b0aa145cc09c1c74f7ce2858eead9329931dcbbb26e2ad40906daa4e/mypy-1.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34506397dbf40c15dc567635d18a21d33827e9ab29014fb83d292a8f4f8953b6", size = 15047240, upload-time = "2026-03-31T16:54:31.955Z" },
- { url = "https://files.pythonhosted.org/packages/42/37/a946bb416e37a57fa752b3100fd5ede0e28df94f92366d1716555d47c454/mypy-1.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555493c44a4f5a1b58d611a43333e71a9981c6dbe26270377b6f8174126a0526", size = 15858565, upload-time = "2026-03-31T16:53:36.997Z" },
- { url = "https://files.pythonhosted.org/packages/2f/99/7690b5b5b552db1bd4ff362e4c0eb3107b98d680835e65823fbe888c8b78/mypy-1.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2721f0ce49cb74a38f00c50da67cb7d36317b5eda38877a49614dc018e91c787", size = 16087874, upload-time = "2026-03-31T16:52:48.313Z" },
- { url = "https://files.pythonhosted.org/packages/aa/76/53e893a498138066acd28192b77495c9357e5a58cc4be753182846b43315/mypy-1.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:47781555a7aa5fedcc2d16bcd72e0dc83eb272c10dd657f9fb3f9cc08e2e6abb", size = 12572380, upload-time = "2026-03-31T16:49:52.454Z" },
- { url = "https://files.pythonhosted.org/packages/76/9c/6dbdae21f01b7aacddc2c0bbf3c5557aa547827fdf271770fe1e521e7093/mypy-1.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c70380fe5d64010f79fb863b9081c7004dd65225d2277333c219d93a10dad4dd", size = 10381174, upload-time = "2026-03-31T16:51:20.179Z" },
- { url = "https://files.pythonhosted.org/packages/21/66/4d734961ce167f0fd8380769b3b7c06dbdd6ff54c2190f3f2ecd22528158/mypy-1.20.0-py3-none-any.whl", hash = "sha256:a6e0641147cbfa7e4e94efdb95c2dab1aff8cfc159ded13e07f308ddccc8c48e", size = 2636365, upload-time = "2026-03-31T16:51:44.911Z" },
-]
-
-[[package]]
-name = "mypy-extensions"
-version = "1.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
-]
-
-[[package]]
-name = "packaging"
-version = "26.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
-]
-
-[[package]]
-name = "pathspec"
-version = "1.0.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
-]
-
-[[package]]
-name = "pluggy"
-version = "1.6.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
-]
-
-[[package]]
-name = "psycopg2-binary"
-version = "2.9.11"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" },
- { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" },
- { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" },
- { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" },
- { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" },
- { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" },
- { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" },
- { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" },
- { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" },
- { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" },
- { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" },
- { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
- { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" },
- { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
- { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
- { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
- { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
- { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
- { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
- { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
- { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
- { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
- { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
- { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
- { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
- { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
- { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
- { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
- { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
- { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
- { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
- { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
- { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
-]
-
-[[package]]
-name = "pygments"
-version = "2.20.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
-]
-
-[[package]]
-name = "pytest"
-version = "9.0.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "iniconfig" },
- { name = "packaging" },
- { name = "pluggy" },
- { name = "pygments" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
-]
-
-[[package]]
-name = "pytest-cov"
-version = "7.1.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "coverage" },
- { name = "pluggy" },
- { name = "pytest" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
-]
-
-[[package]]
-name = "python-dotenv"
-version = "1.2.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
-]
-
-[[package]]
-name = "ruff"
-version = "0.15.10"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" },
- { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" },
- { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" },
- { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" },
- { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" },
- { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" },
- { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" },
- { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" },
- { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" },
- { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" },
- { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" },
- { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" },
- { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" },
- { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" },
- { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" },
- { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" },
- { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" },
-]
-
-[[package]]
-name = "typing-extensions"
-version = "4.15.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
-]
diff --git a/skills/kndl-memory/README.md b/skills/kndl-memory/README.md
new file mode 100644
index 0000000..673226e
--- /dev/null
+++ b/skills/kndl-memory/README.md
@@ -0,0 +1,182 @@
+# kndl-memory
+
+**Confidence-, time-, and provenance-aware memory for AI agents.**
+
+A JSON-LD vocabulary + Claude Skill + MCP server + CLI that turns any
+filesystem memory (including
+[Anthropic Memory on Managed Agents](https://www.anthropic.com/engineering/managed-agents))
+into a knowledge store that knows *when* a fact was learned, *who* said it,
+*how confident* we are, and *whether it's still trustworthy*.
+
+```
+Anthropic Memory = filesystem (where files live)
+KNDL = format (what files contain)
+Skill / CLI / MCP = conventions (how Claude reads & writes)
+```
+
+## Why
+
+Anthropic just shipped Memory: a filesystem agents can write to. They were
+deliberately unopinionated about format. Without conventions, agents fill
+that filesystem with markdown that:
+
+- can't tell when a fact has gone stale
+- can't surface contradictions
+- can't trace claims to sources
+- can't time-travel ("what did we believe last Tuesday?")
+
+KNDL is the missing convention layer. Drop it into your Memory store and
+Claude starts reasoning about *what's trustworthy*, not just *what's written*.
+
+## Repo layout
+
+```
+kndl-memory/ ← the Skill (drag-and-drop into your skills dir)
+ SKILL.md Skill instructions Claude follows
+ context/v1.jsonld JSON-LD @context (also at kndl.artdaw.com)
+ eval/questions.json 8-question eval to score KNDL vs vanilla JSON
+ examples/ 5-fact loan-decision demo dataset
+
+kndl-memory-mcp/ ← the npm package (one source of truth)
+ src/core.ts shared store: decay, query, contradictions, supersession
+ src/cli.ts `kndl` binary — the Skill calls this via bash
+ src/server.ts `kndl-memory-mcp` binary — MCP server
+ package.json exposes both binaries
+```
+
+The CLI and the MCP server share `core.ts`. One language, one decay
+implementation, one set of bugs.
+
+## Install
+
+```bash
+cd kndl-memory-mcp
+npm install
+npm run build
+npm link # makes `kndl` and `kndl-memory-mcp` available system-wide
+```
+
+That installs two binaries:
+
+- **`kndl`** — the CLI the Skill invokes from bash
+- **`kndl-memory-mcp`** — the MCP server for Claude Desktop / Claude Code / Cursor / etc.
+
+For Claude Desktop, add to `claude_desktop_config.json`:
+
+```json
+{
+ "mcpServers": {
+ "kndl-memory": {
+ "command": "kndl-memory-mcp",
+ "env": { "KNDL_MEMORY_DIR": "/absolute/path/to/your/memory" }
+ }
+ }
+}
+```
+
+For the Skill: copy `kndl-memory/` into your project's skills directory
+(or `/memory/skills/` if you're using Anthropic Memory). The Skill activates
+automatically when Claude needs to read or write facts and shells out to the
+`kndl` CLI.
+
+## Quickstart
+
+```bash
+export KNDL_MEMORY_DIR=./memory
+
+kndl add \
+ --statement "Customer 9281 has a credit score of 720" \
+ --subject customer:9281 --predicate creditScore --object 720 \
+ --confidence 0.95 --source "https://api.experian.com/9281" \
+ --decay "0.5/30d" --valid-from now
+
+kndl query --subject customer:9281 --as-of now
+kndl contradictions --subject customer:9281
+kndl provenance --id
+```
+
+## The fact shape
+
+```json
+{
+ "@context": "./context/v1.jsonld",
+ "@id": "fact:cust-9281-credit-2026-04-23",
+ "@type": "Fact",
+ "statement": "Customer 9281 has a credit score of 720",
+ "subject": "customer:9281",
+ "predicate": "creditScore",
+ "object": 720,
+ "confidence": 0.95,
+ "decay": "0.5/30d",
+ "source": "https://api.experian.com/v1/scores/9281",
+ "validFrom": "2026-04-23T10:00:00Z",
+ "recordedAt": "2026-04-23T10:00:00Z"
+}
+```
+
+## The unique fields (vs. JSON-LD baseline)
+
+These are what make KNDL more than "JSON with a schema":
+
+- **`confidence`** — scalar 0–1, epistemic certainty
+- **`decay`** — `/`, applied as `effective = confidence × rate^(elapsed/window)`
+- **`validFrom` / `validUntil` / `observedAt` / `recordedAt`** — bitemporal, three distinct clocks
+- **`supersedes`** — explicit version chain (immutable history with hidden-by-default)
+- **`derivedFrom` / `inference`** — provenance graph for inferred facts
+- **`negated`** — open-world strong negation ("known false" ≠ "absent")
+- **`classification` / `consent`** — sensitivity gating (PHI/PII)
+- **`tenant`** — multi-tenant isolation, refused without explicit override
+
+## CLI commands and matching MCP tools
+
+| CLI command | MCP tool | What it does |
+|------------------------|---------------------|-------------------------------------------------------------------------|
+| `kndl add` | `assert_fact` | Write a new fact |
+| `kndl query` | `query_facts` | Read active facts with effective confidence at as_of time |
+| `kndl contradictions` | `contradictions` | Find disagreeing active facts about the same subject/predicate |
+| `kndl supersede` | `supersede_fact` | Write a fact replacing an older one (preserves history) |
+| `kndl query --as-of` | `as_of` | Bitemporal time-travel ("what did we know on date X") |
+| `kndl provenance` | `provenance_chain` | Walk derivedFrom + supersedes backward to surface the audit trail |
+
+Same shared `core.ts` underneath. Whatever you can do with the CLI, you can
+do via MCP, with identical output.
+
+## The eval
+
+Run `kndl-memory/eval/questions.json` against
+(a) Claude with the JSON facts
+pasted in the system prompt, and
+(b) Claude with the MCP server connected.
+Score each question binary right/wrong.
+
+KNDL should clearly win on:
+
+- **decayed confidence** (vanilla trusts a 633-day-old "employed at ACME" fact)
+- **supersession** (vanilla returns the wrong credit score after an update)
+- **as-of queries** (vanilla can't time-travel reliably)
+- **contradictions** (vanilla picks arbitrarily, no provenance ranking)
+
+If KNDL doesn't win at least 5/8 questions, the architecture isn't paying
+for itself yet. Pivot or fix.
+
+## Status
+
+Hackathon-quality. These work end-to-end (verified via JSON-RPC round-trip
+and via the CLI):
+
+- `kndl add` / `query` / `contradictions` / `supersede` / `provenance` / `list` / `show`
+- All 6 MCP tools wired to the same `core.ts`
+- Decay math verified: BTC at 0.95 confidence with `0.5/4h` decay reads 0.2375 after 8 hours
+- Bitemporal `recordedAt` filtering correct
+- Supersession hides from active queries, preserves for as-of
+- Contradiction ranking: not-negated → newer recorded → higher effective_confidence → shorter chain
+
+Not yet shipped:
+
+- Cryptographic signature verification (`signature` field is read but not validated)
+- `uncertainty` distribution types (round-trippable but not used in reasoning)
+- Vector index for semantic similarity (out of scope; pair with a separate vector DB)
+
+## License
+
+MIT.
diff --git a/skills/kndl-memory/SKILL.md b/skills/kndl-memory/SKILL.md
new file mode 100644
index 0000000..e3c679f
--- /dev/null
+++ b/skills/kndl-memory/SKILL.md
@@ -0,0 +1,246 @@
+---
+name: kndl-memory
+description: |
+ Use this skill whenever you persist a fact to a memory filesystem, read facts back, or
+ answer a question that requires recall from prior sessions. The skill stores each fact as
+ a JSON-LD document with confidence, decay, bitemporal validity, provenance, and supersession,
+ so an agent's memory can reason about what is fresh, what is stale, what is contradicted,
+ and what came from where. Triggers: writing to /memory, reading from /memory, claims like
+ "I learned that…", "I'll remember…", questions like "what did we decide…", "how confident
+ are we in…", "who told us…", "is this still true". Do NOT trigger for ephemeral chat-only
+ scratchpads or single-turn arithmetic. Pairs naturally with Anthropic's Memory on Managed
+ Agents: KNDL is the file format inside the Memory store, this skill is the convention set
+ Claude follows when reading/writing those files.
+---
+
+# KNDL Memory Skill
+
+KNDL ("Knowledge Node Data Link") makes filesystem memory time-aware, source-aware, and contradiction-aware.
+Without it, agents accumulate flat markdown notes and lose the ability to tell what's
+trustworthy. This skill is a small set of conventions Claude follows whenever it reads
+or writes facts to a memory directory.
+
+## Memory layout
+
+```
+/memory/
+ facts/ # one JSON-LD file per fact
+ .fact.json
+ context/
+ v1.jsonld # vendored @context (also at https://kndl.artdaw.com/context/v1.jsonld)
+ inferences/ # optional: rules that produced derived facts
+ .rule.json
+```
+
+One fact per file. Files are immutable. To "update" a fact, write a new file with
+`supersedes` pointing at the old one's `@id` (the `kndl supersede` command does this).
+
+## The fact shape
+
+```json
+{
+ "@context": "./context/v1.jsonld",
+ "@id": "fact:cust-9281-credit-score-2026-04-23",
+ "@type": "Fact",
+
+ "statement": "Customer 9281 has a credit score of 720",
+ "subject": "customer:9281",
+ "predicate": "creditScore",
+ "object": 720,
+
+ "confidence": 0.95,
+ "decay": "0.5/30d",
+
+ "source": "https://api.experian.com/v1/scores/9281",
+ "validFrom": "2026-04-23T10:00:00Z",
+ "recordedAt": "2026-04-23T10:00:00Z"
+}
+```
+
+## Required fields when writing a fact
+
+| Field | Required | Notes |
+|---------------|----------|-------------------------------------------------------------|
+| `@id` | yes | generated automatically; globally unique |
+| `@type` | yes | usually `"Fact"` |
+| `statement` | yes | one-sentence plain-language assertion |
+| `confidence` | yes | float in [0, 1] |
+| `source` | yes | URI; for human input use `human://` |
+| `recordedAt` | yes | set automatically; when this fact entered memory |
+| `validFrom` | yes | when the fact began being true in the world |
+
+## Optional fields you should add when you have them
+
+- `subject` / `predicate` / `object` — structured triple form, useful for queries
+- `validUntil` — explicit end of validity (else valid until superseded)
+- `observedAt` — when an agent or sensor *directly saw* the fact (vs. heard about it)
+- `decay` — `"/"`, e.g. `"0.5/30d"` halves confidence every 30 days
+- `supersedes` — `@id` of the older fact this replaces
+- `derivedFrom` — array of `@id`s if this fact was inferred from others
+- `inference` — `@id` of the rule that did the inference
+- `negated` — `true` means this fact is known false (open-world, not absence)
+- `classification` — `"PII"`, `"PHI"`, `"PCI"`, etc.
+- `consent` — `@id` of the consent scope (required if classification is PHI)
+- `retention` — ISO duration or absolute date for scheduled deletion
+- `tenant` — opaque string for multi-tenant isolation
+
+## CLI installation
+
+The skill relies on the `kndl` CLI binary. Build it once per environment, then it is
+available for all bash tool calls.
+
+```bash
+# 1. Clone and build (one-time)
+git clone https://github.com/artdaw/kndl
+cd kndl/packages/kndl-memory
+pnpm install
+pnpm build
+
+# 2. Make the binary available system-wide
+npm link
+# → `kndl` is now on PATH everywhere
+
+# Verify:
+kndl help
+```
+
+If you cannot use `npm link`, prefix every command with the full path:
+```bash
+node /path/to/kndl/packages/kndl-memory/dist/cli.js add ...
+```
+
+`KNDL_STORAGE` controls where facts live. Default is `fs:./memory` (a `facts/` subdirectory
+relative to the working directory). Set it to match your memory mount:
+
+```bash
+export KNDL_STORAGE=fs:/memory # Anthropic Memory filesystem mount
+export KNDL_STORAGE=sqlite:./kndl-memory.db # SQLite (recommended for Claude Desktop)
+```
+
+## Workflow
+
+The `kndl` CLI shares its core implementation with the `kndl-memory-mcp` server — behavior
+is identical whether you invoke it via bash or via MCP tools. Always use the bash tool to
+invoke the CLI.
+
+### 1. Before answering a question that needs recall
+
+```bash
+kndl query --subject customer:9281 --as-of now
+```
+
+Returns matching facts with their **effective confidence** (decay applied to the
+`as-of` time). Use `effective_confidence` from the response, not the raw `confidence` field.
+
+### 2. Before stating a fact you already know
+
+```bash
+kndl contradictions --subject customer:9281 --predicate creditScore
+```
+
+Lists conflicting assertions about the same subject/predicate. If any contradiction has
+higher effective confidence than what you were about to say, defer to it and mention
+the conflict.
+
+### 3. When learning a new fact
+
+```bash
+kndl add \
+ --statement "Customer 9281 has a credit score of 720" \
+ --subject customer:9281 --predicate creditScore --object 720 \
+ --confidence 0.95 --source "https://api.experian.com/v1/scores/9281" \
+ --decay "0.5/30d" --valid-from now
+```
+
+Returns `{ "id": "fact:..." }`.
+
+### 4. When a fact is superseded
+
+```bash
+kndl supersede --old-id fact:cust-9281-credit-score-2026-03-01 \
+ --statement "Customer 9281 has a credit score of 740" \
+ --subject customer:9281 --predicate creditScore --object 740 \
+ --confidence 0.96 --source "https://api.experian.com/v1/scores/9281" \
+ --decay "0.5/30d" --valid-from now
+```
+
+The old fact stays on disk; it is hidden from active queries but preserved for time-travel.
+
+### 5. When the user asks "what did we believe on date X"
+
+```bash
+kndl query --subject customer:9281 --as-of 2026-03-15T00:00:00Z
+```
+
+Time-travel: filters to facts where `recordedAt <= as-of` and applies decay relative
+to the as-of time, not now.
+
+### 6. To see where a fact came from
+
+```bash
+kndl provenance --id fact:cust-9281-credit-score-2026-04-23
+```
+
+Walks `derivedFrom` and `supersedes` backward to surface the audit trail.
+
+## Reasoning rules
+
+When using facts in an answer:
+
+1. **Trust thresholds.** Treat `effective_confidence ≥ 0.7` as usable.
+ `0.3 ≤ effective < 0.7` is usable but flag uncertainty in the answer and recommend
+ re-verification. `< 0.3` is stale; do not state as fact.
+
+2. **Contradiction resolution.** When two non-superseded facts about the same
+ subject/predicate disagree, prefer (in order):
+ not negated → newer `recordedAt` → higher effective_confidence → shorter `derivedFrom` chain.
+ If still tied, surface both to the user.
+
+3. **Negation is a positive claim.** `negated: true` means *known false*. Absence
+ of a fact means *unknown*. Never substitute one for the other. (Open-world assumption.)
+
+4. **Cite your facts.** Every claim in a recall answer references the `@id` of the
+ fact it relied on. Provenance chains compound: if a derived fact's source isn't
+ trustworthy, the derived fact isn't either.
+
+5. **Classification gates.** Never include facts with `classification: "PHI"` in
+ responses unless a `consent` `@id` covers the current purpose. The query tool
+ filters these by default; do not override without explicit user instruction.
+
+## Decay formula
+
+```
+effective_confidence(t) = confidence × (rate ^ ((t − valid_from) / window))
+```
+
+Examples with `confidence = 0.9`:
+
+| decay | 1 day | 7 days | 30 days | 90 days |
+|--------------|-------|--------|---------|---------|
+| `0.5/24h` | 0.450 | 0.007 | ≈0 | ≈0 |
+| `0.5/7d` | 0.802 | 0.450 | 0.045 | 0.000 |
+| `0.5/30d` | 0.880 | 0.756 | 0.450 | 0.112 |
+| `0.5/180d` | 0.897 | 0.876 | 0.808 | 0.638 |
+| `0.5/365d` | 0.898 | 0.888 | 0.852 | 0.748 |
+
+Pick decay rates that match the natural staleness of the data:
+- Sensor readings: hours to days
+- Stock prices: minutes to hours
+- Personal status (employment, address): months to years
+- Identity, birth date, immutable identifiers: omit decay entirely
+
+## What this skill does NOT do
+
+- Multi-document transactions across facts (each write is atomic on its own file)
+- Cross-tenant queries (the query tool refuses without explicit override)
+- Embeddings or semantic similarity (use a separate vector index if you need it)
+- Editing existing fact files (always supersede)
+
+## Anti-patterns
+
+- Setting `confidence: 1.0` on anything that isn't axiomatic. Reserve 1.0 for definitions.
+- Reusing an `@id`. Every file is immutable; new fact = new id.
+- Omitting `decay` on time-sensitive data. The default is no decay, which is wrong for
+ almost any real-world observation.
+- Inferring facts and not setting `derivedFrom`. Loses the audit trail.
+- Writing to memory and not also writing the source. A fact without provenance is folklore.
diff --git a/skills/kndl-memory/context/v1.jsonld b/skills/kndl-memory/context/v1.jsonld
new file mode 100644
index 0000000..3dd88f3
--- /dev/null
+++ b/skills/kndl-memory/context/v1.jsonld
@@ -0,0 +1,48 @@
+{
+ "@context": {
+ "@version": 1.1,
+ "@vocab": "https://kndl.artdaw.com/vocab/",
+ "kndl": "https://kndl.artdaw.com/vocab/",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "prov": "http://www.w3.org/ns/prov#",
+ "schema": "https://schema.org/",
+
+ "Fact": "kndl:Fact",
+ "Source": "kndl:Source",
+ "InferenceRule": "kndl:InferenceRule",
+
+ "statement": { "@id": "kndl:statement", "@type": "xsd:string" },
+ "subject": { "@id": "kndl:subject", "@type": "@id" },
+ "predicate": { "@id": "kndl:predicate", "@type": "xsd:string" },
+ "object": { "@id": "kndl:object" },
+
+ "confidence": { "@id": "kndl:confidence", "@type": "xsd:double" },
+ "uncertainty": { "@id": "kndl:uncertainty", "@type": "@json" },
+
+ "validFrom": { "@id": "kndl:validFrom", "@type": "xsd:dateTime" },
+ "validUntil": { "@id": "kndl:validUntil", "@type": "xsd:dateTime" },
+ "observedAt": { "@id": "kndl:observedAt", "@type": "xsd:dateTime" },
+ "recordedAt": { "@id": "kndl:recordedAt", "@type": "xsd:dateTime" },
+ "lastSeen": { "@id": "kndl:lastSeen", "@type": "xsd:dateTime" },
+
+ "decay": { "@id": "kndl:decay", "@type": "xsd:string" },
+
+ "source": { "@id": "kndl:source", "@type": "xsd:anyURI" },
+ "supersedes": { "@id": "kndl:supersedes", "@type": "@id" },
+ "derivedFrom": { "@id": "kndl:derivedFrom", "@type": "@id", "@container": "@set" },
+ "inference": { "@id": "kndl:inference", "@type": "@id" },
+
+ "negated": { "@id": "kndl:negated", "@type": "xsd:boolean" },
+
+ "signature": { "@id": "kndl:signature", "@type": "@json" },
+ "attestation": { "@id": "kndl:attestation", "@type": "@id" },
+
+ "classification": { "@id": "kndl:classification", "@type": "xsd:string" },
+ "consent": { "@id": "kndl:consent", "@type": "@id" },
+ "retention": { "@id": "kndl:retention", "@type": "xsd:string" },
+ "tenant": { "@id": "kndl:tenant", "@type": "xsd:string" },
+
+ "weight": { "@id": "kndl:weight", "@type": "xsd:double" },
+ "tags": { "@id": "kndl:tags", "@container": "@set" }
+ }
+}
diff --git a/skills/kndl-memory/eval/questions.json b/skills/kndl-memory/eval/questions.json
new file mode 100644
index 0000000..335f69e
--- /dev/null
+++ b/skills/kndl-memory/eval/questions.json
@@ -0,0 +1,318 @@
+{
+ "version": "2.0",
+ "description": "KNDL eval suite — 33 binary-scored questions across 8 domains. Run each question twice: vanilla (facts in system prompt) and KNDL-MCP (tools connected). Score right/wrong. Pass threshold: ≥70% of questions KNDL wins.",
+ "archetypes": {
+ "decayed_confidence": "Agent must apply decay formula and refuse to state stale facts as current",
+ "supersession": "Agent must use the superseded fact, not the original",
+ "as_of": "Agent must time-travel to the correct belief at a past timestamp",
+ "contradiction": "Agent must rank conflicting facts and surface the conflict explicitly",
+ "provenance": "Agent must trace the audit chain and cite exact source URIs",
+ "negation": "Agent must treat negated:true as known-false, not merely absent",
+ "derivedFrom": "Agent must follow inference chains and weight by chain length",
+ "composite": "Requires combining multiple archetypes in one answer"
+ },
+ "scenarios": [
+ {
+ "id": "loan-decision",
+ "name": "Credit decision — Customer 9281",
+ "corpus_dir": "examples/loan-decision/",
+ "eval_date": "2026-04-26T12:00:00Z",
+ "questions": [
+ {
+ "id": "ld-q1",
+ "archetype": "decayed_confidence",
+ "prompt": "Is customer 9281 currently employed at ACME GmbH? How confident are you?",
+ "correct_behavior": "Refuse to assert current employment. The fact is 633 days old with 0.5/180d decay; effective confidence ~0.08. State that the fact is stale and recommend re-verification.",
+ "vanilla_failure_mode": "Reads original confidence 0.9 and says 'yes, employed at ACME' with little hedging."
+ },
+ {
+ "id": "ld-q2",
+ "archetype": "decayed_confidence",
+ "prompt": "Should we trust the credit score reading for customer 9281 today?",
+ "correct_behavior": "Yes — 3 days old, decay 0.5/30d, effective confidence ~0.88. Still fresh and actionable.",
+ "vanilla_failure_mode": "Either over-trusts or fails to differentiate freshness from the stale employment fact."
+ },
+ {
+ "id": "ld-q3",
+ "archetype": "supersession",
+ "prompt": "What is customer 9281's current credit score?",
+ "setup_note": "The 720 fact has been superseded by 740. Both are in the corpus.",
+ "correct_behavior": "740 — cite the new fact @id, note the older 720 was superseded.",
+ "vanilla_failure_mode": "Returns 720 or presents both as equally valid."
+ },
+ {
+ "id": "ld-q4",
+ "archetype": "as_of",
+ "prompt": "On 2026-04-22, what did we believe customer 9281's credit score was?",
+ "correct_behavior": "720 — the 740 fact had not been recorded yet (recordedAt after 2026-04-22).",
+ "vanilla_failure_mode": "Returns 740 (the latest value) regardless of the as-of date."
+ },
+ {
+ "id": "ld-q5",
+ "archetype": "contradiction",
+ "prompt": "There are two credit score readings — Experian 720 and SCHUFA 680. Which should we trust?",
+ "correct_behavior": "Apply ranking: not-negated, newer recordedAt, higher effective confidence. Prefer Experian 720 (eff ~0.88) over SCHUFA 680 (eff ~0.63). Flag the conflict explicitly.",
+ "vanilla_failure_mode": "Picks one arbitrarily, averages them, or presents both without ranking."
+ },
+ {
+ "id": "ld-q6",
+ "archetype": "provenance",
+ "prompt": "Where does our information about customer 9281's default event come from?",
+ "correct_behavior": "Source is SCHUFA, recorded 6 days ago, effective ~0.97. Cite the source URI exactly.",
+ "vanilla_failure_mode": "Hand-waves about 'records' without exact URI; may invent sources."
+ },
+ {
+ "id": "ld-q7",
+ "archetype": "composite",
+ "prompt": "Customer 9281 is requesting a 50,000 EUR loan. Summarize their financial profile and recommend approve / decline / escalate. Cite which facts you used.",
+ "correct_behavior": "Use credit (fresh, 0.88), default (fresh, 0.97), discount income (stale 0.46), DO NOT rely on employment (decayed 0.08). Recommend ESCALATE due to recent default + need for fresh employment and income verification.",
+ "vanilla_failure_mode": "Treats stale employment as current; may approve or decline without flagging decayed data."
+ },
+ {
+ "id": "ld-q8",
+ "archetype": "decayed_confidence",
+ "prompt": "I heard customer 9281 might be filing for bankruptcy. Should we escalate?",
+ "correct_behavior": "The rumor is in memory at confidence ~0.26 (after decay) from a forum source. Mention it as low-confidence, do not state as fact, recommend verification before action.",
+ "vanilla_failure_mode": "Ignores the rumor entirely, or treats low-confidence forum post as credible."
+ }
+ ]
+ },
+ {
+ "id": "iot-sensor",
+ "name": "IoT — Building 7 sensor telemetry",
+ "corpus_dir": "examples/iot-sensor/",
+ "eval_date": "2026-04-26T10:30:00Z",
+ "questions": [
+ {
+ "id": "iot-q1",
+ "archetype": "decayed_confidence",
+ "prompt": "Is Room 3A in Building 7 currently occupied?",
+ "correct_behavior": "The 08:55 occupancy reading has decayed with 0.5/30m — by 10:30 effective confidence is near zero. The 10:00 'not occupied' reading is also decaying. Neither should be stated as current fact without a fresh sensor read.",
+ "vanilla_failure_mode": "States 'yes, occupied' based on the 08:55 reading without applying decay."
+ },
+ {
+ "id": "iot-q2",
+ "archetype": "supersession",
+ "prompt": "What is the latest temperature reading from sensor T001?",
+ "correct_behavior": "26.1°C at 09:00 — this supersedes the 22.5°C reading from 08:00. Cite the 09:00 fact.",
+ "vanilla_failure_mode": "Returns 22.5°C (the first reading) or presents both without indicating which is current."
+ },
+ {
+ "id": "iot-q3",
+ "archetype": "provenance",
+ "prompt": "What triggered the HVAC fault alert for Room 3A, and how confident are we?",
+ "correct_behavior": "Derived from two facts: temperature 26.1°C (conf 0.99) and occupancy 0.88 (decayed further). Inference confidence 0.82. Cite both derivedFrom source @ids.",
+ "vanilla_failure_mode": "States the alert as a standalone fact without tracing the derivedFrom chain."
+ },
+ {
+ "id": "iot-q4",
+ "archetype": "decayed_confidence",
+ "prompt": "What was the energy consumption in Building 7 this morning?",
+ "correct_behavior": "47.2 kWh recorded at 09:01. Decay is 0.5/24h — after ~1.5 hours, effective confidence ~0.96. Still valid but note it decays over the day.",
+ "vanilla_failure_mode": "Reports 47.2 kWh as a definitive fact without acknowledging the decay."
+ }
+ ]
+ },
+ {
+ "id": "personal-memory",
+ "name": "Personal assistant — Alice's profile",
+ "corpus_dir": "examples/personal-memory/",
+ "eval_date": "2026-04-26T12:00:00Z",
+ "questions": [
+ {
+ "id": "pm-q1",
+ "archetype": "supersession",
+ "prompt": "What is Alice's current role?",
+ "correct_behavior": "Staff engineer on the payments team — this supersedes the senior engineer fact from January. Cite the April 15 update.",
+ "vanilla_failure_mode": "Returns 'senior engineer' (the original) or presents both without distinguishing current from superseded."
+ },
+ {
+ "id": "pm-q2",
+ "archetype": "as_of",
+ "prompt": "What was Alice's role at the start of 2026?",
+ "correct_behavior": "Senior engineer on payments — the staff engineer promotion happened April 15, after this date.",
+ "vanilla_failure_mode": "Returns 'staff engineer' (the current role) regardless of the time constraint."
+ },
+ {
+ "id": "pm-q3",
+ "archetype": "derivedFrom",
+ "prompt": "Does Alice prefer async communication or meetings?",
+ "correct_behavior": "Async (confidence 0.78). This is a derived/inferred fact — cite the source fact it was derived from and flag it as inference, not direct statement.",
+ "vanilla_failure_mode": "States 'async' as certain fact without noting it's inferred and at 0.78 confidence."
+ }
+ ]
+ },
+ {
+ "id": "threat-intel",
+ "name": "Threat intelligence — Emotet campaign",
+ "corpus_dir": "examples/threat-intel/",
+ "eval_date": "2026-04-25T12:00:00Z",
+ "questions": [
+ {
+ "id": "ti-q1",
+ "archetype": "negation",
+ "prompt": "Should we block traffic to 192.0.2.1 as a C2 server?",
+ "correct_behavior": "No — the C2 classification was retracted as a false positive. The fact is negated (negated:true) and superseded. 192.0.2.1 belongs to a CDN. Do not block.",
+ "vanilla_failure_mode": "Sees the original C2 fact and recommends blocking, missing the retraction."
+ },
+ {
+ "id": "ti-q2",
+ "archetype": "contradiction",
+ "prompt": "Is update.example.com malicious or legitimate?",
+ "correct_behavior": "Contradiction — one fact says Emotet C2 (0.85 decayed), another says legitimate CDN update server (0.7). Surface both, note the conflict, recommend manual investigation before blocking.",
+ "vanilla_failure_mode": "Returns one answer without flagging the contradiction."
+ },
+ {
+ "id": "ti-q3",
+ "archetype": "decayed_confidence",
+ "prompt": "Is the 192.0.2.1 C2 indicator still fresh enough to act on?",
+ "correct_behavior": "Irrelevant — the indicator was retracted. Even if not, 0.5/24h decay on a 24h-old IOC would reduce confidence to ~0.45, borderline. The retraction makes it moot.",
+ "vanilla_failure_mode": "Ignores the retraction and either trusts or discounts based on age alone."
+ },
+ {
+ "id": "ti-q4",
+ "archetype": "provenance",
+ "prompt": "What is the chain of evidence linking the Emotet malware hash to the C2 domain?",
+ "correct_behavior": "derivedFrom chain: C2 domain fact derived from malware hash fact (which is from the Emotet malware report). Cite both @ids and sources. Aggregate confidence ~0.83.",
+ "vanilla_failure_mode": "States the connection without tracing the derivedFrom link or aggregating confidence."
+ }
+ ]
+ },
+ {
+ "id": "clinical",
+ "name": "Clinical — Patient 9001",
+ "corpus_dir": "examples/clinical/",
+ "eval_date": "2026-04-17T14:00:00Z",
+ "questions": [
+ {
+ "id": "cl-q1",
+ "archetype": "supersession",
+ "prompt": "What is patient 9001's confirmed diagnosis?",
+ "correct_behavior": "Type 2 diabetes mellitus (confirmed March 17, confidence 0.97) — this supersedes the preliminary 'suspected' assessment from March 10. Cite the confirmed fact.",
+ "vanilla_failure_mode": "Returns 'type 2 diabetes suspected' (the preliminary fact) or presents both as equally valid."
+ },
+ {
+ "id": "cl-q2",
+ "archetype": "negation",
+ "prompt": "Does patient 9001 have a penicillin allergy?",
+ "correct_behavior": "No — this is a KNOWN ABSENCE (negated:true), explicitly stated by the patient. Do not treat it as 'unknown' or 'no information'. Cite the negated fact.",
+ "vanilla_failure_mode": "Says 'no allergy information found' or treats the absence as unknown rather than explicitly denied."
+ },
+ {
+ "id": "cl-q3",
+ "archetype": "decayed_confidence",
+ "prompt": "Is the HbA1c reading from March 17 still clinically valid today?",
+ "correct_behavior": "HbA1c reflects 3-month average glycemia; with 0.5/90d decay, after ~40 days effective confidence ~0.74. Still in the 'usable with hedge' range but approaching the 'recommend re-test' threshold.",
+ "vanilla_failure_mode": "Reports 8.2% as current fact without applying decay or noting recency."
+ },
+ {
+ "id": "cl-q4",
+ "archetype": "derivedFrom",
+ "prompt": "Why was Metformin prescribed for patient 9001?",
+ "correct_behavior": "Derived from two facts: confirmed T2 diabetes diagnosis AND HbA1c 8.2% (both PHI). Cite both @ids in the derivedFrom chain. Confidence 0.99 from prescribing clinician.",
+ "vanilla_failure_mode": "States 'because of diabetes' without tracing the derivedFrom chain or citing the HbA1c specifically."
+ }
+ ]
+ },
+ {
+ "id": "legal-ediscovery",
+ "name": "Legal — Smith v ACME contract dispute",
+ "corpus_dir": "examples/legal-ediscovery/",
+ "eval_date": "2026-02-10T12:00:00Z",
+ "questions": [
+ {
+ "id": "le-q1",
+ "archetype": "supersession",
+ "prompt": "Where was the Smith-ACME contract signed?",
+ "correct_behavior": "Per Johnson's amended testimony (Feb 3): remotely via DocuSign. This supersedes the original testimony citing Chicago ACME HQ. Cite the amendment @id.",
+ "vanilla_failure_mode": "Returns 'Chicago, ACME HQ' (original testimony) or presents both as equally valid."
+ },
+ {
+ "id": "le-q2",
+ "archetype": "as_of",
+ "prompt": "What did witness Johnson originally testify on January 15?",
+ "correct_behavior": "That the contract was signed at ACME HQ in Chicago — this is what was in the record before the February 3 amendment.",
+ "vanilla_failure_mode": "Returns the amended testimony (DocuSign), not the original, ignoring the as-of constraint."
+ },
+ {
+ "id": "le-q3",
+ "archetype": "contradiction",
+ "prompt": "Witnesses Johnson and Brown gave different locations for the contract signing. How do we resolve this?",
+ "correct_behavior": "Contradiction: Johnson says Chicago/DocuSign (amended, higher confidence), Brown says New York Smith offices (0.8, uncontested). Rank: Johnson's amended testimony is newer and higher effective confidence. Surface both. Do not resolve without additional evidence.",
+ "vanilla_failure_mode": "Picks one arbitrarily or presents them without ranking by recency and confidence."
+ }
+ ]
+ },
+ {
+ "id": "scientific-lab",
+ "name": "Scientific lab — EXP-042 buffer pH",
+ "corpus_dir": "examples/scientific-lab/",
+ "eval_date": "2026-05-02T10:00:00Z",
+ "questions": [
+ {
+ "id": "sl-q1",
+ "archetype": "negation",
+ "prompt": "One pH reading was 6.9. Should we include it in our analysis?",
+ "correct_behavior": "No — this reading has been explicitly retracted (negated:true) due to confirmed electrode contamination. Treat as known-invalid, not merely absent.",
+ "vanilla_failure_mode": "Includes the 6.9 reading in the analysis or says 'we have a reading of 6.9'."
+ },
+ {
+ "id": "sl-q2",
+ "archetype": "derivedFrom",
+ "prompt": "What is the consensus buffer pH for EXP-042?",
+ "correct_behavior": "7.40 ± 0.02 (n=2, from runs 1 and 2 at 7.41 and 7.38). The consensus fact cites both measurements in derivedFrom. The retracted 6.9 was excluded. This was subsequently published (DOI: 10.1000/jcb.2026.042).",
+ "vanilla_failure_mode": "Returns 7.41 or 7.38 without noting the consensus derivation, or includes the retracted reading."
+ },
+ {
+ "id": "sl-q3",
+ "archetype": "provenance",
+ "prompt": "Has the EXP-042 pH result been peer-reviewed and published?",
+ "correct_behavior": "Yes — published in Journal of Chemical Biology, doi:10.1000/jcb.2026.042, with confidence 0.99. Provenance chain: published result ← consensus ← run 1 + run 2.",
+ "vanilla_failure_mode": "States the result without noting the publication, or conflates the raw measurement confidence with the published result."
+ }
+ ]
+ },
+ {
+ "id": "ai-evals",
+ "name": "AI evaluations — Claude model benchmarks",
+ "corpus_dir": "examples/ai-evals/",
+ "eval_date": "2026-04-20T00:00:00Z",
+ "questions": [
+ {
+ "id": "ae-q1",
+ "archetype": "supersession",
+ "prompt": "What is the best MMLU score among Claude 4 models?",
+ "correct_behavior": "Claude Opus 4.7 at 93.8% (April 15) — this is the highest score in the bundle. Sonnet 4.5 scored 89.2% earlier. Sonnet 4.6 (1M context) shows a regression to 87.6%. Opus 4.7 is the current top performer.",
+ "vanilla_failure_mode": "Returns Sonnet 4.5's 89.2% or Sonnet 4.6's 87.6% without identifying Opus 4.7's higher score as the latest and best."
+ },
+ {
+ "id": "ae-q2",
+ "archetype": "contradiction",
+ "prompt": "Is Claude Sonnet 4.6 better or worse than Sonnet 4.5 on MMLU?",
+ "correct_behavior": "Worse — Sonnet 4.6 (1M context) scored 87.6% vs Sonnet 4.5's 89.2%. The 4.6 fact is labeled 'regression' and notes the trade-off for extended context. These are distinct model versions, not a supersession.",
+ "vanilla_failure_mode": "Treats 4.6 as superseding 4.5 (it doesn't — it's a different variant with a different context window and a lower MMLU score)."
+ },
+ {
+ "id": "ae-q3",
+ "archetype": "derivedFrom",
+ "prompt": "Is Claude Opus 4.7 considered expert-level reasoning capability?",
+ "correct_behavior": "Yes, confidence 0.91 — but this is a DERIVED fact (from MMLU 93.8% and the Constitutional AI safety eval on Sonnet 4.5). Cite derivedFrom. Note it's an inference, not a direct measurement. The safety eval source is Sonnet 4.5, not Opus 4.7 — flag this cross-model discrepancy.",
+ "vanilla_failure_mode": "States expert-level capability as a certain fact without tracing the derivation chain or flagging that the safety evaluation was conducted on a different model variant."
+ }
+ ]
+ }
+ ],
+ "cross_runtime": {
+ "description": "Manual tests requiring a running Anthropic Managed Agent + Claude Desktop with KNDL MCP connected. These CANNOT be run in automated CI.",
+ "questions": [
+ {
+ "id": "cr-q1",
+ "archetype": "cross_runtime_sync",
+ "prompt": "A managed agent writes: assert_fact('Customer 9281 credit score upgraded to 760', subject='customer:9281', predicate='creditScore', object=760, confidence=0.99, source='https://api.experian.com/v2/9281'). Within 60 seconds, ask a Claude Desktop session (connected to KNDL MCP with watch_memory_store active): 'What is customer 9281's current credit score?'",
+ "correct_behavior": "Claude Desktop returns 760 — the fact written by the managed agent is visible via the sync layer within the configured watch interval.",
+ "vanilla_failure_mode": "Claude Desktop returns a stale score or says 'I don't have information' because the local memory has not synced.",
+ "requires": ["ANTHROPIC_API_KEY", "kndl-memory-mcp --http", "watch_memory_store registered"]
+ }
+ ]
+ }
+}
diff --git a/skills/kndl-memory/eval/results.json b/skills/kndl-memory/eval/results.json
new file mode 100644
index 0000000..711c3d2
--- /dev/null
+++ b/skills/kndl-memory/eval/results.json
@@ -0,0 +1,397 @@
+{
+ "run_at": "2026-04-26T11:13:19.065Z",
+ "model": "claude-opus-4-5-20251001",
+ "total": 32,
+ "passed": 0,
+ "failed": 32,
+ "pass_rate": 0,
+ "threshold": 70,
+ "verdict": "FAIL",
+ "by_archetype": {},
+ "questions": [
+ {
+ "scenario_id": "loan-decision",
+ "question_id": "ld-q1",
+ "archetype": "decayed_confidence",
+ "prompt": "Is customer 9281 currently employed at ACME GmbH? How confident are you?",
+ "correct_behavior": "Refuse to assert current employment. The fact is 633 days old with 0.5/180d decay; effective confidence ~0.08. State that the fact is stale and recommend re-verification.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHPpmiUkmScxFJLkaeY\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "loan-decision",
+ "question_id": "ld-q2",
+ "archetype": "decayed_confidence",
+ "prompt": "Should we trust the credit score reading for customer 9281 today?",
+ "correct_behavior": "Yes — 3 days old, decay 0.5/30d, effective confidence ~0.88. Still fresh and actionable.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHPt8NZfTkoq1DgofTP\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "loan-decision",
+ "question_id": "ld-q3",
+ "archetype": "supersession",
+ "prompt": "What is customer 9281's current credit score?",
+ "correct_behavior": "740 — cite the new fact @id, note the older 720 was superseded.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHPw8TFTRFSxpbNyzTL\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "loan-decision",
+ "question_id": "ld-q4",
+ "archetype": "as_of",
+ "prompt": "On 2026-04-22, what did we believe customer 9281's credit score was?",
+ "correct_behavior": "720 — the 740 fact had not been recorded yet (recordedAt after 2026-04-22).",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHPywd3CU4R3H3gHnHb\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "loan-decision",
+ "question_id": "ld-q5",
+ "archetype": "contradiction",
+ "prompt": "There are two credit score readings — Experian 720 and SCHUFA 680. Which should we trust?",
+ "correct_behavior": "Apply ranking: not-negated, newer recordedAt, higher effective confidence. Prefer Experian 720 (eff ~0.88) over SCHUFA 680 (eff ~0.63). Flag the conflict explicitly.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQ2knsZfsJu41NE8pW\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "loan-decision",
+ "question_id": "ld-q6",
+ "archetype": "provenance",
+ "prompt": "Where does our information about customer 9281's default event come from?",
+ "correct_behavior": "Source is SCHUFA, recorded 6 days ago, effective ~0.97. Cite the source URI exactly.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQ5YDfBcsbHZVWDhc9\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "loan-decision",
+ "question_id": "ld-q7",
+ "archetype": "composite",
+ "prompt": "Customer 9281 is requesting a 50,000 EUR loan. Summarize their financial profile and recommend approve / decline / escalate. Cite which facts you used.",
+ "correct_behavior": "Use credit (fresh, 0.88), default (fresh, 0.97), discount income (stale 0.46), DO NOT rely on employment (decayed 0.08). Recommend ESCALATE due to recent default + need for fresh employment and income verification.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQ8MPHspxHVZMjG4U3\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "loan-decision",
+ "question_id": "ld-q8",
+ "archetype": "decayed_confidence",
+ "prompt": "I heard customer 9281 might be filing for bankruptcy. Should we escalate?",
+ "correct_behavior": "The rumor is in memory at confidence ~0.26 (after decay) from a forum source. Mention it as low-confidence, do not state as fact, recommend verification before action.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQBF252h7RtiM1UnK1\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "iot-sensor",
+ "question_id": "iot-q1",
+ "archetype": "decayed_confidence",
+ "prompt": "Is Room 3A in Building 7 currently occupied?",
+ "correct_behavior": "The 08:55 occupancy reading has decayed with 0.5/30m — by 10:30 effective confidence is near zero. The 10:00 'not occupied' reading is also decaying. Neither should be stated as current fact without a fresh sensor read.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQEAPEK4fi5qQgFWrR\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T10:30:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "iot-sensor",
+ "question_id": "iot-q2",
+ "archetype": "supersession",
+ "prompt": "What is the latest temperature reading from sensor T001?",
+ "correct_behavior": "26.1°C at 09:00 — this supersedes the 22.5°C reading from 08:00. Cite the 09:00 fact.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQH5W53jzvfFpAsxWM\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T10:30:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "iot-sensor",
+ "question_id": "iot-q3",
+ "archetype": "provenance",
+ "prompt": "What triggered the HVAC fault alert for Room 3A, and how confident are we?",
+ "correct_behavior": "Derived from two facts: temperature 26.1°C (conf 0.99) and occupancy 0.88 (decayed further). Inference confidence 0.82. Cite both derivedFrom source @ids.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQKxe5sFmoM4LewsWw\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T10:30:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "iot-sensor",
+ "question_id": "iot-q4",
+ "archetype": "decayed_confidence",
+ "prompt": "What was the energy consumption in Building 7 this morning?",
+ "correct_behavior": "47.2 kWh recorded at 09:01. Decay is 0.5/24h — after ~1.5 hours, effective confidence ~0.96. Still valid but note it decays over the day.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQNuzFp6EYuEBAE7iu\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T10:30:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "personal-memory",
+ "question_id": "pm-q1",
+ "archetype": "supersession",
+ "prompt": "What is Alice's current role?",
+ "correct_behavior": "Staff engineer on the payments team — this supersedes the senior engineer fact from January. Cite the April 15 update.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQSQajkJJ4cSRvuLee\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "personal-memory",
+ "question_id": "pm-q2",
+ "archetype": "as_of",
+ "prompt": "What was Alice's role at the start of 2026?",
+ "correct_behavior": "Senior engineer on payments — the staff engineer promotion happened April 15, after this date.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQW9K6QhJDc4zjezDS\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "personal-memory",
+ "question_id": "pm-q3",
+ "archetype": "derivedFrom",
+ "prompt": "Does Alice prefer async communication or meetings?",
+ "correct_behavior": "Async (confidence 0.78). This is a derived/inferred fact — cite the source fact it was derived from and flag it as inference, not direct statement.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQZHabNrM1hk6YT3Wy\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-26T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "threat-intel",
+ "question_id": "ti-q1",
+ "archetype": "negation",
+ "prompt": "Should we block traffic to 192.0.2.1 as a C2 server?",
+ "correct_behavior": "No — the C2 classification was retracted as a false positive. The fact is negated (negated:true) and superseded. 192.0.2.1 belongs to a CDN. Do not block.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQcFvQhrV7Hvo6qVH4\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-25T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "threat-intel",
+ "question_id": "ti-q2",
+ "archetype": "contradiction",
+ "prompt": "Is update.example.com malicious or legitimate?",
+ "correct_behavior": "Contradiction — one fact says Emotet C2 (0.85 decayed), another says legitimate CDN update server (0.7). Surface both, note the conflict, recommend manual investigation before blocking.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQfAndvfrii2LCeUCL\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-25T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "threat-intel",
+ "question_id": "ti-q3",
+ "archetype": "decayed_confidence",
+ "prompt": "Is the 192.0.2.1 C2 indicator still fresh enough to act on?",
+ "correct_behavior": "Irrelevant — the indicator was retracted. Even if not, 0.5/24h decay on a 24h-old IOC would reduce confidence to ~0.45, borderline. The retraction makes it moot.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQi1Srebz3UZPyMwpJ\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-25T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "threat-intel",
+ "question_id": "ti-q4",
+ "archetype": "provenance",
+ "prompt": "What is the chain of evidence linking the Emotet malware hash to the C2 domain?",
+ "correct_behavior": "derivedFrom chain: C2 domain fact derived from malware hash fact (which is from the Emotet malware report). Cite both @ids and sources. Aggregate confidence ~0.83.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQkvKBBLJcRSQv2vwy\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-25T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "clinical",
+ "question_id": "cl-q1",
+ "archetype": "supersession",
+ "prompt": "What is patient 9001's confirmed diagnosis?",
+ "correct_behavior": "Type 2 diabetes mellitus (confirmed March 17, confidence 0.97) — this supersedes the preliminary 'suspected' assessment from March 10. Cite the confirmed fact.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQouuBsnez252hCx82\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-17T14:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "clinical",
+ "question_id": "cl-q2",
+ "archetype": "negation",
+ "prompt": "Does patient 9001 have a penicillin allergy?",
+ "correct_behavior": "No — this is a KNOWN ABSENCE (negated:true), explicitly stated by the patient. Do not treat it as 'unknown' or 'no information'. Cite the negated fact.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQrj4jcs6JVr31rxvV\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-17T14:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "clinical",
+ "question_id": "cl-q3",
+ "archetype": "decayed_confidence",
+ "prompt": "Is the HbA1c reading from March 17 still clinically valid today?",
+ "correct_behavior": "HbA1c reflects 3-month average glycemia; with 0.5/90d decay, after ~40 days effective confidence ~0.74. Still in the 'usable with hedge' range but approaching the 'recommend re-test' threshold.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQuVWKRprhccZQ6uWz\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-17T14:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "clinical",
+ "question_id": "cl-q4",
+ "archetype": "derivedFrom",
+ "prompt": "Why was Metformin prescribed for patient 9001?",
+ "correct_behavior": "Derived from two facts: confirmed T2 diabetes diagnosis AND HbA1c 8.2% (both PHI). Cite both @ids in the derivedFrom chain. Confidence 0.99 from prescribing clinician.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHQxJBSUTR23hZE4ti2\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-17T14:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "legal-ediscovery",
+ "question_id": "le-q1",
+ "archetype": "supersession",
+ "prompt": "Where was the Smith-ACME contract signed?",
+ "correct_behavior": "Per Johnson's amended testimony (Feb 3): remotely via DocuSign. This supersedes the original testimony citing Chicago ACME HQ. Cite the amendment @id.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHR1677phLWXUvsQSxQ\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-02-10T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "legal-ediscovery",
+ "question_id": "le-q2",
+ "archetype": "as_of",
+ "prompt": "What did witness Johnson originally testify on January 15?",
+ "correct_behavior": "That the contract was signed at ACME HQ in Chicago — this is what was in the record before the February 3 amendment.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHR3tHPaZVym87trwUQ\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-02-10T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "legal-ediscovery",
+ "question_id": "le-q3",
+ "archetype": "contradiction",
+ "prompt": "Witnesses Johnson and Brown gave different locations for the contract signing. How do we resolve this?",
+ "correct_behavior": "Contradiction: Johnson says Chicago/DocuSign (amended, higher confidence), Brown says New York Smith offices (0.8, uncontested). Rank: Johnson's amended testimony is newer and higher effective confidence. Surface both. Do not resolve without additional evidence.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHR6tcJPk76KtNGfhg8\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-02-10T12:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "scientific-lab",
+ "question_id": "sl-q1",
+ "archetype": "negation",
+ "prompt": "One pH reading was 6.9. Should we include it in our analysis?",
+ "correct_behavior": "No — this reading has been explicitly retracted (negated:true) due to confirmed electrode contamination. Treat as known-invalid, not merely absent.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHR9y9niEFEfqPBh2oR\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-05-02T10:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "scientific-lab",
+ "question_id": "sl-q2",
+ "archetype": "derivedFrom",
+ "prompt": "What is the consensus buffer pH for EXP-042?",
+ "correct_behavior": "7.40 ± 0.02 (n=2, from runs 1 and 2 at 7.41 and 7.38). The consensus fact cites both measurements in derivedFrom. The retracted 6.9 was excluded. This was subsequently published (DOI: 10.1000/jcb.2026.042).",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHRCkptoUELVHnEYLJM\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-05-02T10:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "scientific-lab",
+ "question_id": "sl-q3",
+ "archetype": "provenance",
+ "prompt": "Has the EXP-042 pH result been peer-reviewed and published?",
+ "correct_behavior": "Yes — published in Journal of Chemical Biology, doi:10.1000/jcb.2026.042, with confidence 0.99. Provenance chain: published result ← consensus ← run 1 + run 2.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHRFexajNYMBa99RPfA\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-05-02T10:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "ai-evals",
+ "question_id": "ae-q1",
+ "archetype": "supersession",
+ "prompt": "What is GPT-5's current MMLU score?",
+ "correct_behavior": "93.1% (v2.0, April 15) — this supersedes the 91.4% score from v1.0. Note that v1.5 had a regression to 89.2% and should NOT be confused with the current v2.0 score.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHRJuuhKSJ7bJwt3CZG\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-20T00:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "ai-evals",
+ "question_id": "ae-q2",
+ "archetype": "contradiction",
+ "prompt": "Is GPT-5 v1.5 better or worse than v1.0 on MMLU?",
+ "correct_behavior": "Worse — v1.5 scored 89.2% vs v1.0's 91.4%. The v1.5 fact is labeled 'regression'. These are distinct models with distinct scores, not a supersession.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHRMmK9Kuk7yf3d29aU\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-20T00:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ },
+ {
+ "scenario_id": "ai-evals",
+ "question_id": "ae-q3",
+ "archetype": "derivedFrom",
+ "prompt": "Is GPT-5 v2.0 considered expert-level reasoning capability?",
+ "correct_behavior": "Yes, confidence 0.88 — but this is a DERIVED fact (from MMLU score 93.1% and the safety eval). Cite derivedFrom. Note it's an inference, not a direct measurement. The safety eval was on v1.0, not v2.0 — flag this discrepancy.",
+ "vanilla_answer": "[error: 404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-opus-4-5-20251001\"},\"request_id\":\"req_011CaSHRQajJR9rUxBddUgEi\"}]",
+ "vanilla_pass": false,
+ "judge_reasoning": "eval error",
+ "eval_date": "2026-04-20T00:00:00Z",
+ "model": "claude-opus-4-5-20251001"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/skills/kndl-memory/examples/ai-evals/fact-claude-opus-47-capability-20260416t000000z-q7r8s9t0.fact.json b/skills/kndl-memory/examples/ai-evals/fact-claude-opus-47-capability-20260416t000000z-q7r8s9t0.fact.json
new file mode 100644
index 0000000..2057b59
--- /dev/null
+++ b/skills/kndl-memory/examples/ai-evals/fact-claude-opus-47-capability-20260416t000000z-q7r8s9t0.fact.json
@@ -0,0 +1,18 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:model-claude-opus-47-reasoning-capability-20260416t000000z-q7r8s9t0",
+ "@type": "Fact",
+ "statement": "Claude Opus 4.7 demonstrates expert-level reasoning — derived from MMLU 93.8% and Constitutional AI safety evaluation",
+ "subject": "model:claude-opus-4-7",
+ "predicate": "reasoning_capability",
+ "object": "expert_level",
+ "confidence": 0.91,
+ "source": "agent://eval-pipeline",
+ "validFrom": "2026-04-16T00:00:00Z",
+ "recordedAt": "2026-04-16T00:00:00Z",
+ "derivedFrom": [
+ "fact:model-claude-opus-47-mmlu-20260415t000000z-e5f6g7h8",
+ "fact:model-claude-sonnet-45-safety-eval-20260301t000000z-i9j0k1l2"
+ ],
+ "tags": ["capability", "derived", "claude-opus"]
+}
diff --git a/skills/kndl-memory/examples/ai-evals/fact-claude-opus-47-mmlu-20260415t000000z-e5f6g7h8.fact.json b/skills/kndl-memory/examples/ai-evals/fact-claude-opus-47-mmlu-20260415t000000z-e5f6g7h8.fact.json
new file mode 100644
index 0000000..1c8eaa6
--- /dev/null
+++ b/skills/kndl-memory/examples/ai-evals/fact-claude-opus-47-mmlu-20260415t000000z-e5f6g7h8.fact.json
@@ -0,0 +1,15 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:model-claude-opus-47-mmlu-20260415t000000z-e5f6g7h8",
+ "@type": "Fact",
+ "statement": "Claude Opus 4.7 scores 93.8% on MMLU benchmark (5-shot), improving on Sonnet 4.5",
+ "subject": "model:claude-opus-4-7",
+ "predicate": "mmlu_5shot",
+ "object": 93.8,
+ "confidence": 0.99,
+ "decay": "0.5/365d",
+ "source": "https://anthropic.com/research/claude-4-evals",
+ "validFrom": "2026-04-15T00:00:00Z",
+ "recordedAt": "2026-04-15T00:00:00Z",
+ "tags": ["benchmark", "mmlu", "claude-opus"]
+}
diff --git a/skills/kndl-memory/examples/ai-evals/fact-claude-sonnet-45-mmlu-20260301t000000z-a1b2c3d4.fact.json b/skills/kndl-memory/examples/ai-evals/fact-claude-sonnet-45-mmlu-20260301t000000z-a1b2c3d4.fact.json
new file mode 100644
index 0000000..511d11e
--- /dev/null
+++ b/skills/kndl-memory/examples/ai-evals/fact-claude-sonnet-45-mmlu-20260301t000000z-a1b2c3d4.fact.json
@@ -0,0 +1,15 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:model-claude-sonnet-45-mmlu-20260301t000000z-a1b2c3d4",
+ "@type": "Fact",
+ "statement": "Claude Sonnet 4.5 scores 89.2% on MMLU benchmark (5-shot)",
+ "subject": "model:claude-sonnet-4-5",
+ "predicate": "mmlu_5shot",
+ "object": 89.2,
+ "confidence": 0.99,
+ "decay": "0.5/365d",
+ "source": "https://anthropic.com/research/claude-4-evals",
+ "validFrom": "2026-03-01T00:00:00Z",
+ "recordedAt": "2026-03-01T00:00:00Z",
+ "tags": ["benchmark", "mmlu", "claude-sonnet"]
+}
diff --git a/skills/kndl-memory/examples/ai-evals/fact-claude-sonnet-45-safety-20260301t000000z-i9j0k1l2.fact.json b/skills/kndl-memory/examples/ai-evals/fact-claude-sonnet-45-safety-20260301t000000z-i9j0k1l2.fact.json
new file mode 100644
index 0000000..d9e9b04
--- /dev/null
+++ b/skills/kndl-memory/examples/ai-evals/fact-claude-sonnet-45-safety-20260301t000000z-i9j0k1l2.fact.json
@@ -0,0 +1,15 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:model-claude-sonnet-45-safety-eval-20260301t000000z-i9j0k1l2",
+ "@type": "Fact",
+ "statement": "Claude Sonnet 4.5 passes Anthropic Constitutional AI safety evaluation at threshold 0.92",
+ "subject": "model:claude-sonnet-4-5",
+ "predicate": "safety_eval_constitutional",
+ "object": "pass",
+ "confidence": 0.97,
+ "source": "https://anthropic.com/safety-evals/claude-4",
+ "validFrom": "2026-03-01T00:00:00Z",
+ "recordedAt": "2026-03-05T00:00:00Z",
+ "classification": "INTERNAL",
+ "tags": ["safety", "constitutional-ai", "claude-sonnet"]
+}
diff --git a/skills/kndl-memory/examples/ai-evals/fact-claude-sonnet-46-regression-20260410t000000z-m3n4o5p6.fact.json b/skills/kndl-memory/examples/ai-evals/fact-claude-sonnet-46-regression-20260410t000000z-m3n4o5p6.fact.json
new file mode 100644
index 0000000..901f559
--- /dev/null
+++ b/skills/kndl-memory/examples/ai-evals/fact-claude-sonnet-46-regression-20260410t000000z-m3n4o5p6.fact.json
@@ -0,0 +1,14 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:model-claude-sonnet-46-mmlu-regression-20260410t000000z-m3n4o5p6",
+ "@type": "Fact",
+ "statement": "Claude Sonnet 4.6 (1M context) MMLU score is 87.6% — slight regression from 4.5, likely trade-off for extended context",
+ "subject": "model:claude-sonnet-4-6-1m",
+ "predicate": "mmlu_5shot",
+ "object": 87.6,
+ "confidence": 0.98,
+ "source": "https://anthropic.com/research/claude-4-evals",
+ "validFrom": "2026-04-10T00:00:00Z",
+ "recordedAt": "2026-04-10T00:00:00Z",
+ "tags": ["benchmark", "mmlu", "regression", "claude-sonnet"]
+}
diff --git a/skills/kndl-memory/examples/clinical/fact-patient-9001-dx-confirmed-20260317t110000z-e5f6g7h8.fact.json b/skills/kndl-memory/examples/clinical/fact-patient-9001-dx-confirmed-20260317t110000z-e5f6g7h8.fact.json
new file mode 100644
index 0000000..3cec29b
--- /dev/null
+++ b/skills/kndl-memory/examples/clinical/fact-patient-9001-dx-confirmed-20260317t110000z-e5f6g7h8.fact.json
@@ -0,0 +1,17 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:patient-9001-diagnosis-confirmed-20260317t110000z-e5f6g7h8",
+ "@type": "Fact",
+ "statement": "Patient 9001 confirmed Type 2 diabetes mellitus — HbA1c 8.2%, fasting glucose 195 mg/dL",
+ "subject": "patient:9001",
+ "predicate": "diagnosis",
+ "object": "type2_diabetes",
+ "confidence": 0.97,
+ "source": "lab://city-hospital/chemistry",
+ "validFrom": "2026-03-17T11:00:00Z",
+ "observedAt": "2026-03-17T09:00:00Z",
+ "recordedAt": "2026-03-17T11:00:00Z",
+ "supersedes": "fact:patient-9001-diagnosis-initial-20260310t140000z-a1b2c3d4",
+ "classification": "PHI",
+ "consent": "consent:patient-9001-care-team-2026"
+}
diff --git a/skills/kndl-memory/examples/clinical/fact-patient-9001-dx-initial-20260310t140000z-a1b2c3d4.fact.json b/skills/kndl-memory/examples/clinical/fact-patient-9001-dx-initial-20260310t140000z-a1b2c3d4.fact.json
new file mode 100644
index 0000000..bb63ad7
--- /dev/null
+++ b/skills/kndl-memory/examples/clinical/fact-patient-9001-dx-initial-20260310t140000z-a1b2c3d4.fact.json
@@ -0,0 +1,16 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:patient-9001-diagnosis-initial-20260310t140000z-a1b2c3d4",
+ "@type": "Fact",
+ "statement": "Patient 9001 presents with symptoms consistent with Type 2 diabetes (preliminary assessment)",
+ "subject": "patient:9001",
+ "predicate": "diagnosis",
+ "object": "type2_diabetes_suspected",
+ "confidence": 0.7,
+ "source": "clinician://dr-chen",
+ "validFrom": "2026-03-10T14:00:00Z",
+ "observedAt": "2026-03-10T13:45:00Z",
+ "recordedAt": "2026-03-10T14:00:00Z",
+ "classification": "PHI",
+ "consent": "consent:patient-9001-care-team-2026"
+}
diff --git a/skills/kndl-memory/examples/clinical/fact-patient-9001-hba1c-20260317t090000z-i9j0k1l2.fact.json b/skills/kndl-memory/examples/clinical/fact-patient-9001-hba1c-20260317t090000z-i9j0k1l2.fact.json
new file mode 100644
index 0000000..f2984b9
--- /dev/null
+++ b/skills/kndl-memory/examples/clinical/fact-patient-9001-hba1c-20260317t090000z-i9j0k1l2.fact.json
@@ -0,0 +1,17 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:patient-9001-hba1c-20260317t090000z-i9j0k1l2",
+ "@type": "Fact",
+ "statement": "Patient 9001 HbA1c is 8.2% — elevated, indicating poor glycemic control over past 3 months",
+ "subject": "patient:9001",
+ "predicate": "hba1c_percent",
+ "object": 8.2,
+ "confidence": 0.99,
+ "decay": "0.5/90d",
+ "source": "lab://city-hospital/chemistry",
+ "validFrom": "2026-03-17T09:00:00Z",
+ "observedAt": "2026-03-17T09:00:00Z",
+ "recordedAt": "2026-03-17T09:30:00Z",
+ "classification": "PHI",
+ "consent": "consent:patient-9001-care-team-2026"
+}
diff --git a/skills/kndl-memory/examples/clinical/fact-patient-9001-metformin-20260317t120000z-q7r8s9t0.fact.json b/skills/kndl-memory/examples/clinical/fact-patient-9001-metformin-20260317t120000z-q7r8s9t0.fact.json
new file mode 100644
index 0000000..25725a4
--- /dev/null
+++ b/skills/kndl-memory/examples/clinical/fact-patient-9001-metformin-20260317t120000z-q7r8s9t0.fact.json
@@ -0,0 +1,20 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:patient-9001-metformin-20260317t120000z-q7r8s9t0",
+ "@type": "Fact",
+ "statement": "Patient 9001 prescribed Metformin 500mg twice daily for Type 2 diabetes management",
+ "subject": "patient:9001",
+ "predicate": "prescription",
+ "object": "metformin_500mg_bid",
+ "confidence": 0.99,
+ "source": "clinician://dr-chen",
+ "validFrom": "2026-03-17T12:00:00Z",
+ "recordedAt": "2026-03-17T12:00:00Z",
+ "derivedFrom": [
+ "fact:patient-9001-diagnosis-confirmed-20260317t110000z-e5f6g7h8",
+ "fact:patient-9001-hba1c-20260317t090000z-i9j0k1l2"
+ ],
+ "classification": "PHI",
+ "consent": "consent:patient-9001-care-team-2026",
+ "retention": "P7Y"
+}
diff --git a/skills/kndl-memory/examples/clinical/fact-patient-9001-no-penicillin-allergy-20260310t140000z-m3n4o5p6.fact.json b/skills/kndl-memory/examples/clinical/fact-patient-9001-no-penicillin-allergy-20260310t140000z-m3n4o5p6.fact.json
new file mode 100644
index 0000000..23c2f41
--- /dev/null
+++ b/skills/kndl-memory/examples/clinical/fact-patient-9001-no-penicillin-allergy-20260310t140000z-m3n4o5p6.fact.json
@@ -0,0 +1,17 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:patient-9001-penicillin-allergy-20260310t140000z-m3n4o5p6",
+ "@type": "Fact",
+ "statement": "Patient 9001 has NO known penicillin allergy — explicitly denied by patient",
+ "subject": "patient:9001",
+ "predicate": "allergy",
+ "object": "penicillin",
+ "confidence": 0.9,
+ "source": "clinician://dr-chen",
+ "validFrom": "2026-03-10T14:00:00Z",
+ "observedAt": "2026-03-10T13:45:00Z",
+ "recordedAt": "2026-03-10T14:00:00Z",
+ "negated": true,
+ "classification": "PHI",
+ "consent": "consent:patient-9001-care-team-2026"
+}
diff --git a/skills/kndl-memory/examples/iot-sensor/fact-bldg7-energy-20260426t090000z-o1p2q3r4.fact.json b/skills/kndl-memory/examples/iot-sensor/fact-bldg7-energy-20260426t090000z-o1p2q3r4.fact.json
new file mode 100644
index 0000000..f6ede9f
--- /dev/null
+++ b/skills/kndl-memory/examples/iot-sensor/fact-bldg7-energy-20260426t090000z-o1p2q3r4.fact.json
@@ -0,0 +1,15 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:bldg7-energy-consumption-20260426t090000z-o1p2q3r4",
+ "@type": "Fact",
+ "statement": "Building 7 energy consumption this hour is 47.2 kWh",
+ "subject": "building:bldg7",
+ "predicate": "energy_kwh_hourly",
+ "object": 47.2,
+ "confidence": 0.99,
+ "decay": "0.5/24h",
+ "source": "meter://bldg7/main",
+ "validFrom": "2026-04-26T09:00:00Z",
+ "recordedAt": "2026-04-26T09:01:00Z",
+ "classification": "INTERNAL"
+}
diff --git a/skills/kndl-memory/examples/iot-sensor/fact-bldg7-hvac-alert-20260426t091500z-k7l8m9n0.fact.json b/skills/kndl-memory/examples/iot-sensor/fact-bldg7-hvac-alert-20260426t091500z-k7l8m9n0.fact.json
new file mode 100644
index 0000000..d65f891
--- /dev/null
+++ b/skills/kndl-memory/examples/iot-sensor/fact-bldg7-hvac-alert-20260426t091500z-k7l8m9n0.fact.json
@@ -0,0 +1,18 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:bldg7-hvac-alert-20260426t091500z-k7l8m9n0",
+ "@type": "Fact",
+ "statement": "HVAC in Building 7 Room 3A is likely faulted — temperature 3.6°C above setpoint while room is occupied",
+ "subject": "hvac:bldg7-r3a",
+ "predicate": "status",
+ "object": "fault_suspected",
+ "confidence": 0.82,
+ "source": "rule://bldg7/hvac-monitor",
+ "validFrom": "2026-04-26T09:15:00Z",
+ "recordedAt": "2026-04-26T09:15:00Z",
+ "derivedFrom": [
+ "fact:bldg7-t001-temperature-20260426t090000z-e5f6a7b8",
+ "fact:bldg7-r3a-occupied-20260426t085500z-c9d0e1f2"
+ ],
+ "tags": ["alert", "maintenance"]
+}
diff --git a/skills/kndl-memory/examples/iot-sensor/fact-bldg7-r3a-occupied-20260426t085500z-c9d0e1f2.fact.json b/skills/kndl-memory/examples/iot-sensor/fact-bldg7-r3a-occupied-20260426t085500z-c9d0e1f2.fact.json
new file mode 100644
index 0000000..8f99b21
--- /dev/null
+++ b/skills/kndl-memory/examples/iot-sensor/fact-bldg7-r3a-occupied-20260426t085500z-c9d0e1f2.fact.json
@@ -0,0 +1,15 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:bldg7-r3a-occupied-20260426t085500z-c9d0e1f2",
+ "@type": "Fact",
+ "statement": "Building 7 Room 3A is occupied",
+ "subject": "room:bldg7-r3a",
+ "predicate": "occupied",
+ "object": true,
+ "confidence": 0.88,
+ "decay": "0.5/30m",
+ "source": "pir://bldg7/r3a",
+ "validFrom": "2026-04-26T08:55:00Z",
+ "observedAt": "2026-04-26T08:55:00Z",
+ "recordedAt": "2026-04-26T08:55:01Z"
+}
diff --git a/skills/kndl-memory/examples/iot-sensor/fact-bldg7-r3a-occupied-20260426t100000z-g3h4i5j6.fact.json b/skills/kndl-memory/examples/iot-sensor/fact-bldg7-r3a-occupied-20260426t100000z-g3h4i5j6.fact.json
new file mode 100644
index 0000000..307dbb1
--- /dev/null
+++ b/skills/kndl-memory/examples/iot-sensor/fact-bldg7-r3a-occupied-20260426t100000z-g3h4i5j6.fact.json
@@ -0,0 +1,16 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:bldg7-r3a-occupied-20260426t100000z-g3h4i5j6",
+ "@type": "Fact",
+ "statement": "Building 7 Room 3A is NOT occupied — room cleared after meeting",
+ "subject": "room:bldg7-r3a",
+ "predicate": "occupied",
+ "object": false,
+ "confidence": 0.95,
+ "decay": "0.5/30m",
+ "source": "pir://bldg7/r3a",
+ "validFrom": "2026-04-26T10:00:00Z",
+ "observedAt": "2026-04-26T10:00:00Z",
+ "recordedAt": "2026-04-26T10:00:01Z",
+ "supersedes": "fact:bldg7-r3a-occupied-20260426t085500z-c9d0e1f2"
+}
diff --git a/skills/kndl-memory/examples/iot-sensor/fact-bldg7-t001-temp-20260426t080000z-a1b2c3d4.fact.json b/skills/kndl-memory/examples/iot-sensor/fact-bldg7-t001-temp-20260426t080000z-a1b2c3d4.fact.json
new file mode 100644
index 0000000..0bc8247
--- /dev/null
+++ b/skills/kndl-memory/examples/iot-sensor/fact-bldg7-t001-temp-20260426t080000z-a1b2c3d4.fact.json
@@ -0,0 +1,14 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:bldg7-t001-temperature-20260426t080000z-a1b2c3d4",
+ "@type": "Fact",
+ "statement": "Sensor T001 in Building 7 Room 3A reads 22.5°C",
+ "subject": "sensor:bldg7-t001",
+ "predicate": "temperature_celsius",
+ "object": 22.5,
+ "confidence": 0.99,
+ "decay": "0.5/1h",
+ "source": "mqtt://bldg7/sensors/t001",
+ "validFrom": "2026-04-26T08:00:00Z",
+ "recordedAt": "2026-04-26T08:00:03Z"
+}
diff --git a/skills/kndl-memory/examples/iot-sensor/fact-bldg7-t001-temp-20260426t090000z-e5f6a7b8.fact.json b/skills/kndl-memory/examples/iot-sensor/fact-bldg7-t001-temp-20260426t090000z-e5f6a7b8.fact.json
new file mode 100644
index 0000000..57934e6
--- /dev/null
+++ b/skills/kndl-memory/examples/iot-sensor/fact-bldg7-t001-temp-20260426t090000z-e5f6a7b8.fact.json
@@ -0,0 +1,15 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:bldg7-t001-temperature-20260426t090000z-e5f6a7b8",
+ "@type": "Fact",
+ "statement": "Sensor T001 in Building 7 Room 3A reads 26.1°C (rising, possible HVAC fault)",
+ "subject": "sensor:bldg7-t001",
+ "predicate": "temperature_celsius",
+ "object": 26.1,
+ "confidence": 0.99,
+ "decay": "0.5/1h",
+ "source": "mqtt://bldg7/sensors/t001",
+ "validFrom": "2026-04-26T09:00:00Z",
+ "recordedAt": "2026-04-26T09:00:02Z",
+ "supersedes": "fact:bldg7-t001-temperature-20260426t080000z-a1b2c3d4"
+}
diff --git a/skills/kndl-memory/examples/legal-ediscovery/fact-case-2026-cv-smith-amended-testimony-20260203t100000z-e5f6g7h8.fact.json b/skills/kndl-memory/examples/legal-ediscovery/fact-case-2026-cv-smith-amended-testimony-20260203t100000z-e5f6g7h8.fact.json
new file mode 100644
index 0000000..c853d7b
--- /dev/null
+++ b/skills/kndl-memory/examples/legal-ediscovery/fact-case-2026-cv-smith-amended-testimony-20260203t100000z-e5f6g7h8.fact.json
@@ -0,0 +1,16 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:smith-v-acme-testimony-johnson-location-amended-20260203t100000z-e5f6g7h8",
+ "@type": "Fact",
+ "statement": "Witness Johnson amended testimony: contract was signed remotely via DocuSign, not at ACME HQ",
+ "subject": "event:smith-v-acme-contract-signing",
+ "predicate": "location",
+ "object": "remote_docusign",
+ "confidence": 0.9,
+ "source": "deposition://smith-v-acme/johnson-amended-20260203",
+ "validFrom": "2026-01-10T00:00:00Z",
+ "observedAt": "2026-01-10T14:00:00Z",
+ "recordedAt": "2026-02-03T10:00:00Z",
+ "supersedes": "fact:smith-v-acme-testimony-johnson-location-20260115t140000z-a1b2c3d4",
+ "classification": "CONFIDENTIAL"
+}
diff --git a/skills/kndl-memory/examples/legal-ediscovery/fact-case-2026-cv-smith-contradicting-witness-20260118t110000z-i9j0k1l2.fact.json b/skills/kndl-memory/examples/legal-ediscovery/fact-case-2026-cv-smith-contradicting-witness-20260118t110000z-i9j0k1l2.fact.json
new file mode 100644
index 0000000..07837eb
--- /dev/null
+++ b/skills/kndl-memory/examples/legal-ediscovery/fact-case-2026-cv-smith-contradicting-witness-20260118t110000z-i9j0k1l2.fact.json
@@ -0,0 +1,15 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:smith-v-acme-testimony-brown-location-20260118t110000z-i9j0k1l2",
+ "@type": "Fact",
+ "statement": "Witness Brown testified that the contract was signed at the Smith offices in New York on January 10",
+ "subject": "event:smith-v-acme-contract-signing",
+ "predicate": "location",
+ "object": "New York, Smith offices",
+ "confidence": 0.8,
+ "source": "deposition://smith-v-acme/brown-20260118",
+ "validFrom": "2026-01-10T00:00:00Z",
+ "observedAt": "2026-01-10T14:00:00Z",
+ "recordedAt": "2026-01-18T11:00:00Z",
+ "classification": "CONFIDENTIAL"
+}
diff --git a/skills/kndl-memory/examples/legal-ediscovery/fact-case-2026-cv-smith-document-auth-20260120t090000z-m3n4o5p6.fact.json b/skills/kndl-memory/examples/legal-ediscovery/fact-case-2026-cv-smith-document-auth-20260120t090000z-m3n4o5p6.fact.json
new file mode 100644
index 0000000..691e44f
--- /dev/null
+++ b/skills/kndl-memory/examples/legal-ediscovery/fact-case-2026-cv-smith-document-auth-20260120t090000z-m3n4o5p6.fact.json
@@ -0,0 +1,18 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:smith-v-acme-docusign-log-authenticated-20260120t090000z-m3n4o5p6",
+ "@type": "Fact",
+ "statement": "DocuSign audit log for contract CON-2026-0110 is authenticated and shows signatures from New York and Chicago IPs on Jan 10",
+ "subject": "document:con-2026-0110-docusign-log",
+ "predicate": "authentication_status",
+ "object": "authenticated",
+ "confidence": 0.97,
+ "source": "docusign://audit/CON-2026-0110",
+ "validFrom": "2026-01-10T14:30:00Z",
+ "recordedAt": "2026-01-20T09:00:00Z",
+ "derivedFrom": [
+ "fact:smith-v-acme-testimony-johnson-location-amended-20260203t100000z-e5f6g7h8"
+ ],
+ "classification": "CONFIDENTIAL",
+ "retention": "P10Y"
+}
diff --git a/skills/kndl-memory/examples/legal-ediscovery/fact-case-2026-cv-smith-initial-testimony-20260115t140000z-a1b2c3d4.fact.json b/skills/kndl-memory/examples/legal-ediscovery/fact-case-2026-cv-smith-initial-testimony-20260115t140000z-a1b2c3d4.fact.json
new file mode 100644
index 0000000..9925945
--- /dev/null
+++ b/skills/kndl-memory/examples/legal-ediscovery/fact-case-2026-cv-smith-initial-testimony-20260115t140000z-a1b2c3d4.fact.json
@@ -0,0 +1,15 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:smith-v-acme-testimony-johnson-location-20260115t140000z-a1b2c3d4",
+ "@type": "Fact",
+ "statement": "Witness Johnson testified that the contract was signed at ACME HQ in Chicago on January 10",
+ "subject": "event:smith-v-acme-contract-signing",
+ "predicate": "location",
+ "object": "Chicago, ACME HQ",
+ "confidence": 0.85,
+ "source": "deposition://smith-v-acme/johnson-20260115",
+ "validFrom": "2026-01-10T00:00:00Z",
+ "observedAt": "2026-01-10T14:00:00Z",
+ "recordedAt": "2026-01-15T14:00:00Z",
+ "classification": "CONFIDENTIAL"
+}
diff --git a/skills/kndl-memory/examples/loan-decision-vanilla.json b/skills/kndl-memory/examples/loan-decision-vanilla.json
new file mode 100644
index 0000000..174f968
--- /dev/null
+++ b/skills/kndl-memory/examples/loan-decision-vanilla.json
@@ -0,0 +1,104 @@
+{
+ "today": "2026-04-26T12:00:00Z",
+ "facts": [
+ {
+ "@context": "../context/v1.jsonld",
+ "@id": "fact:customer-9281-annualincome-20260425T235231Z-09c3bb41",
+ "@type": "Fact",
+ "statement": "Customer 9281 annual income is 85000 EUR",
+ "confidence": 0.85,
+ "source": "human://self-reported",
+ "validFrom": "2025-11-15T00:00:00Z",
+ "recordedAt": "2026-04-25T23:52:31Z",
+ "subject": "customer:9281",
+ "predicate": "annualIncome",
+ "object": "85000 EUR",
+ "decay": "0.5/180d"
+ },
+ {
+ "@context": "../context/v1.jsonld",
+ "@id": "fact:customer-9281-creditscore-20260425T235231Z-cd2efb00",
+ "@type": "Fact",
+ "statement": "Customer 9281 credit score is 720",
+ "confidence": 0.95,
+ "source": "https://api.experian.com/9281",
+ "validFrom": "2026-04-23T10:00:00Z",
+ "recordedAt": "2026-04-25T23:52:31Z",
+ "subject": "customer:9281",
+ "predicate": "creditScore",
+ "object": 720,
+ "decay": "0.5/30d"
+ },
+ {
+ "@context": "../context/v1.jsonld",
+ "@id": "fact:customer-9281-creditscore-20260425T235248Z-3bf684bd",
+ "@type": "Fact",
+ "statement": "Customer 9281 credit score is 680",
+ "confidence": 0.7,
+ "source": "https://schufa.de/9281",
+ "validFrom": "2026-04-22T00:00:00Z",
+ "recordedAt": "2026-04-25T23:52:48Z",
+ "subject": "customer:9281",
+ "predicate": "creditScore",
+ "object": 680,
+ "decay": "0.5/30d"
+ },
+ {
+ "@context": "../context/v1.jsonld",
+ "@id": "fact:customer-9281-creditscore-20260425T235249Z-b6c88774",
+ "@type": "Fact",
+ "statement": "Customer 9281 credit score is 740",
+ "confidence": 0.96,
+ "source": "https://api.experian.com/9281",
+ "validFrom": "2026-04-26T00:00:00Z",
+ "recordedAt": "2026-04-25T23:52:49Z",
+ "subject": "customer:9281",
+ "predicate": "creditScore",
+ "object": 740,
+ "decay": "0.5/30d",
+ "supersedes": "fact:customer-9281-creditscore-20260425T235231Z-cd2efb00"
+ },
+ {
+ "@context": "../context/v1.jsonld",
+ "@id": "fact:customer-9281-defaultevent-20260425T235231Z-e4b6d53c",
+ "@type": "Fact",
+ "statement": "Customer 9281 defaulted on 12500 EUR with bank-abc",
+ "confidence": 0.98,
+ "source": "https://schufa.de/9281",
+ "validFrom": "2026-04-20T00:00:00Z",
+ "recordedAt": "2026-04-25T23:52:31Z",
+ "subject": "customer:9281",
+ "predicate": "defaultEvent",
+ "object": "12500 EUR with bank-abc",
+ "decay": "0.5/365d"
+ },
+ {
+ "@context": "../context/v1.jsonld",
+ "@id": "fact:customer-9281-employmentstatus-20260425T235231Z-39b12725",
+ "@type": "Fact",
+ "statement": "Customer 9281 is employed at ACME GmbH",
+ "confidence": 0.9,
+ "source": "human://self-reported",
+ "validFrom": "2024-08-01T00:00:00Z",
+ "recordedAt": "2026-04-25T23:52:31Z",
+ "subject": "customer:9281",
+ "predicate": "employmentStatus",
+ "object": "employed",
+ "decay": "0.5/180d"
+ },
+ {
+ "@context": "../context/v1.jsonld",
+ "@id": "fact:customer-9281-financialdistress-20260425T235232Z-b64190fb",
+ "@type": "Fact",
+ "statement": "Customer 9281 may have filed bankruptcy (forum rumor)",
+ "confidence": 0.3,
+ "source": "https://forum.example.com/post/12345",
+ "validFrom": "2026-04-25T00:00:00Z",
+ "recordedAt": "2026-04-25T23:52:32Z",
+ "subject": "customer:9281",
+ "predicate": "financialDistress",
+ "object": "bankruptcy-rumored",
+ "decay": "0.5/7d"
+ }
+ ]
+}
diff --git a/skills/kndl-memory/examples/loan-decision/fact-customer-9281-annualincome-20260425t235231z-09c3bb41.fact.json b/skills/kndl-memory/examples/loan-decision/fact-customer-9281-annualincome-20260425t235231z-09c3bb41.fact.json
new file mode 100644
index 0000000..8df4b2a
--- /dev/null
+++ b/skills/kndl-memory/examples/loan-decision/fact-customer-9281-annualincome-20260425t235231z-09c3bb41.fact.json
@@ -0,0 +1,14 @@
+{
+ "@context": "../context/v1.jsonld",
+ "@id": "fact:customer-9281-annualincome-20260425T235231Z-09c3bb41",
+ "@type": "Fact",
+ "statement": "Customer 9281 annual income is 85000 EUR",
+ "confidence": 0.85,
+ "source": "human://self-reported",
+ "validFrom": "2025-11-15T00:00:00Z",
+ "recordedAt": "2026-04-25T23:52:31Z",
+ "subject": "customer:9281",
+ "predicate": "annualIncome",
+ "object": "85000 EUR",
+ "decay": "0.5/180d"
+}
\ No newline at end of file
diff --git a/skills/kndl-memory/examples/loan-decision/fact-customer-9281-creditscore-20260425t235231z-cd2efb00.fact.json b/skills/kndl-memory/examples/loan-decision/fact-customer-9281-creditscore-20260425t235231z-cd2efb00.fact.json
new file mode 100644
index 0000000..5566762
--- /dev/null
+++ b/skills/kndl-memory/examples/loan-decision/fact-customer-9281-creditscore-20260425t235231z-cd2efb00.fact.json
@@ -0,0 +1,14 @@
+{
+ "@context": "../context/v1.jsonld",
+ "@id": "fact:customer-9281-creditscore-20260425T235231Z-cd2efb00",
+ "@type": "Fact",
+ "statement": "Customer 9281 credit score is 720",
+ "confidence": 0.95,
+ "source": "https://api.experian.com/9281",
+ "validFrom": "2026-04-23T10:00:00Z",
+ "recordedAt": "2026-04-25T23:52:31Z",
+ "subject": "customer:9281",
+ "predicate": "creditScore",
+ "object": 720,
+ "decay": "0.5/30d"
+}
\ No newline at end of file
diff --git a/skills/kndl-memory/examples/loan-decision/fact-customer-9281-creditscore-20260425t235248z-3bf684bd.fact.json b/skills/kndl-memory/examples/loan-decision/fact-customer-9281-creditscore-20260425t235248z-3bf684bd.fact.json
new file mode 100644
index 0000000..b43fd49
--- /dev/null
+++ b/skills/kndl-memory/examples/loan-decision/fact-customer-9281-creditscore-20260425t235248z-3bf684bd.fact.json
@@ -0,0 +1,14 @@
+{
+ "@context": "../context/v1.jsonld",
+ "@id": "fact:customer-9281-creditscore-20260425T235248Z-3bf684bd",
+ "@type": "Fact",
+ "statement": "Customer 9281 credit score is 680",
+ "confidence": 0.7,
+ "source": "https://schufa.de/9281",
+ "validFrom": "2026-04-22T00:00:00Z",
+ "recordedAt": "2026-04-25T23:52:48Z",
+ "subject": "customer:9281",
+ "predicate": "creditScore",
+ "object": 680,
+ "decay": "0.5/30d"
+}
\ No newline at end of file
diff --git a/skills/kndl-memory/examples/loan-decision/fact-customer-9281-creditscore-20260425t235249z-b6c88774.fact.json b/skills/kndl-memory/examples/loan-decision/fact-customer-9281-creditscore-20260425t235249z-b6c88774.fact.json
new file mode 100644
index 0000000..acbe360
--- /dev/null
+++ b/skills/kndl-memory/examples/loan-decision/fact-customer-9281-creditscore-20260425t235249z-b6c88774.fact.json
@@ -0,0 +1,15 @@
+{
+ "@context": "../context/v1.jsonld",
+ "@id": "fact:customer-9281-creditscore-20260425T235249Z-b6c88774",
+ "@type": "Fact",
+ "statement": "Customer 9281 credit score is 740",
+ "confidence": 0.96,
+ "source": "https://api.experian.com/9281",
+ "validFrom": "2026-04-26T00:00:00Z",
+ "recordedAt": "2026-04-25T23:52:49Z",
+ "subject": "customer:9281",
+ "predicate": "creditScore",
+ "object": 740,
+ "decay": "0.5/30d",
+ "supersedes": "fact:customer-9281-creditscore-20260425T235231Z-cd2efb00"
+}
\ No newline at end of file
diff --git a/skills/kndl-memory/examples/loan-decision/fact-customer-9281-defaultevent-20260425t235231z-e4b6d53c.fact.json b/skills/kndl-memory/examples/loan-decision/fact-customer-9281-defaultevent-20260425t235231z-e4b6d53c.fact.json
new file mode 100644
index 0000000..df52e66
--- /dev/null
+++ b/skills/kndl-memory/examples/loan-decision/fact-customer-9281-defaultevent-20260425t235231z-e4b6d53c.fact.json
@@ -0,0 +1,14 @@
+{
+ "@context": "../context/v1.jsonld",
+ "@id": "fact:customer-9281-defaultevent-20260425T235231Z-e4b6d53c",
+ "@type": "Fact",
+ "statement": "Customer 9281 defaulted on 12500 EUR with bank-abc",
+ "confidence": 0.98,
+ "source": "https://schufa.de/9281",
+ "validFrom": "2026-04-20T00:00:00Z",
+ "recordedAt": "2026-04-25T23:52:31Z",
+ "subject": "customer:9281",
+ "predicate": "defaultEvent",
+ "object": "12500 EUR with bank-abc",
+ "decay": "0.5/365d"
+}
\ No newline at end of file
diff --git a/skills/kndl-memory/examples/loan-decision/fact-customer-9281-employmentstatus-20260425t235231z-39b12725.fact.json b/skills/kndl-memory/examples/loan-decision/fact-customer-9281-employmentstatus-20260425t235231z-39b12725.fact.json
new file mode 100644
index 0000000..d44a7ef
--- /dev/null
+++ b/skills/kndl-memory/examples/loan-decision/fact-customer-9281-employmentstatus-20260425t235231z-39b12725.fact.json
@@ -0,0 +1,14 @@
+{
+ "@context": "../context/v1.jsonld",
+ "@id": "fact:customer-9281-employmentstatus-20260425T235231Z-39b12725",
+ "@type": "Fact",
+ "statement": "Customer 9281 is employed at ACME GmbH",
+ "confidence": 0.9,
+ "source": "human://self-reported",
+ "validFrom": "2024-08-01T00:00:00Z",
+ "recordedAt": "2026-04-25T23:52:31Z",
+ "subject": "customer:9281",
+ "predicate": "employmentStatus",
+ "object": "employed",
+ "decay": "0.5/180d"
+}
\ No newline at end of file
diff --git a/skills/kndl-memory/examples/loan-decision/fact-customer-9281-financialdistress-20260425t235232z-b64190fb.fact.json b/skills/kndl-memory/examples/loan-decision/fact-customer-9281-financialdistress-20260425t235232z-b64190fb.fact.json
new file mode 100644
index 0000000..b19316a
--- /dev/null
+++ b/skills/kndl-memory/examples/loan-decision/fact-customer-9281-financialdistress-20260425t235232z-b64190fb.fact.json
@@ -0,0 +1,14 @@
+{
+ "@context": "../context/v1.jsonld",
+ "@id": "fact:customer-9281-financialdistress-20260425T235232Z-b64190fb",
+ "@type": "Fact",
+ "statement": "Customer 9281 may have filed bankruptcy (forum rumor)",
+ "confidence": 0.3,
+ "source": "https://forum.example.com/post/12345",
+ "validFrom": "2026-04-25T00:00:00Z",
+ "recordedAt": "2026-04-25T23:52:32Z",
+ "subject": "customer:9281",
+ "predicate": "financialDistress",
+ "object": "bankruptcy-rumored",
+ "decay": "0.5/7d"
+}
\ No newline at end of file
diff --git a/skills/kndl-memory/examples/personal-memory/fact-alice-email-20260426t000000z-q7r8s9t0.fact.json b/skills/kndl-memory/examples/personal-memory/fact-alice-email-20260426t000000z-q7r8s9t0.fact.json
new file mode 100644
index 0000000..689dcfb
--- /dev/null
+++ b/skills/kndl-memory/examples/personal-memory/fact-alice-email-20260426t000000z-q7r8s9t0.fact.json
@@ -0,0 +1,14 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:alice-email-20260426t000000z-q7r8s9t0",
+ "@type": "Fact",
+ "statement": "Alice's work email is alice@example.com",
+ "subject": "person:alice",
+ "predicate": "email",
+ "object": "alice@example.com",
+ "confidence": 0.99,
+ "source": "human://gleb",
+ "validFrom": "2026-01-01T00:00:00Z",
+ "recordedAt": "2026-04-26T00:00:00Z",
+ "classification": "PII"
+}
diff --git a/skills/kndl-memory/examples/personal-memory/fact-alice-location-20260426t000000z-i9j0k1l2.fact.json b/skills/kndl-memory/examples/personal-memory/fact-alice-location-20260426t000000z-i9j0k1l2.fact.json
new file mode 100644
index 0000000..57162f9
--- /dev/null
+++ b/skills/kndl-memory/examples/personal-memory/fact-alice-location-20260426t000000z-i9j0k1l2.fact.json
@@ -0,0 +1,14 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:alice-location-20260426t000000z-i9j0k1l2",
+ "@type": "Fact",
+ "statement": "Alice is based in Berlin",
+ "subject": "person:alice",
+ "predicate": "city",
+ "object": "Berlin",
+ "confidence": 0.85,
+ "decay": "0.5/90d",
+ "source": "human://gleb",
+ "validFrom": "2026-01-01T00:00:00Z",
+ "recordedAt": "2026-04-26T00:00:00Z"
+}
diff --git a/skills/kndl-memory/examples/personal-memory/fact-alice-prefers-async-20260210t000000z-m3n4o5p6.fact.json b/skills/kndl-memory/examples/personal-memory/fact-alice-prefers-async-20260210t000000z-m3n4o5p6.fact.json
new file mode 100644
index 0000000..715eefb
--- /dev/null
+++ b/skills/kndl-memory/examples/personal-memory/fact-alice-prefers-async-20260210t000000z-m3n4o5p6.fact.json
@@ -0,0 +1,16 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:alice-prefers-async-20260210t000000z-m3n4o5p6",
+ "@type": "Fact",
+ "statement": "Alice prefers async communication over meetings — inferred from multiple interactions",
+ "subject": "person:alice",
+ "predicate": "communication_preference",
+ "object": "async",
+ "confidence": 0.78,
+ "source": "agent://assistant",
+ "validFrom": "2026-02-10T00:00:00Z",
+ "recordedAt": "2026-02-10T14:00:00Z",
+ "derivedFrom": [
+ "fact:alice-role-20260101t000000z-a1b2c3d4"
+ ]
+}
diff --git a/skills/kndl-memory/examples/personal-memory/fact-alice-role-20260101t000000z-a1b2c3d4.fact.json b/skills/kndl-memory/examples/personal-memory/fact-alice-role-20260101t000000z-a1b2c3d4.fact.json
new file mode 100644
index 0000000..2d97fe3
--- /dev/null
+++ b/skills/kndl-memory/examples/personal-memory/fact-alice-role-20260101t000000z-a1b2c3d4.fact.json
@@ -0,0 +1,14 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:alice-role-20260101t000000z-a1b2c3d4",
+ "@type": "Fact",
+ "statement": "Alice is a senior engineer on the payments team",
+ "subject": "person:alice",
+ "predicate": "role",
+ "object": "senior engineer, payments",
+ "confidence": 0.95,
+ "decay": "0.5/180d",
+ "source": "human://gleb",
+ "validFrom": "2026-01-01T00:00:00Z",
+ "recordedAt": "2026-01-01T10:00:00Z"
+}
diff --git a/skills/kndl-memory/examples/personal-memory/fact-alice-role-20260415t000000z-e5f6g7h8.fact.json b/skills/kndl-memory/examples/personal-memory/fact-alice-role-20260415t000000z-e5f6g7h8.fact.json
new file mode 100644
index 0000000..6e70f29
--- /dev/null
+++ b/skills/kndl-memory/examples/personal-memory/fact-alice-role-20260415t000000z-e5f6g7h8.fact.json
@@ -0,0 +1,15 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:alice-role-20260415t000000z-e5f6g7h8",
+ "@type": "Fact",
+ "statement": "Alice was promoted to staff engineer, still on payments team",
+ "subject": "person:alice",
+ "predicate": "role",
+ "object": "staff engineer, payments",
+ "confidence": 0.97,
+ "decay": "0.5/180d",
+ "source": "human://gleb",
+ "validFrom": "2026-04-15T00:00:00Z",
+ "recordedAt": "2026-04-15T09:30:00Z",
+ "supersedes": "fact:alice-role-20260101t000000z-a1b2c3d4"
+}
diff --git a/skills/kndl-memory/examples/scientific-lab/fact-exp042-ph-consensus-20260421t090000z-m3n4o5p6.fact.json b/skills/kndl-memory/examples/scientific-lab/fact-exp042-ph-consensus-20260421t090000z-m3n4o5p6.fact.json
new file mode 100644
index 0000000..43dc0a0
--- /dev/null
+++ b/skills/kndl-memory/examples/scientific-lab/fact-exp042-ph-consensus-20260421t090000z-m3n4o5p6.fact.json
@@ -0,0 +1,18 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:exp042-buffer-ph-consensus-20260421t090000z-m3n4o5p6",
+ "@type": "Fact",
+ "statement": "Experiment EXP-042 consensus buffer pH is 7.40 ± 0.02 (n=2, excluding retracted outlier)",
+ "subject": "experiment:EXP-042",
+ "predicate": "buffer_ph_consensus",
+ "object": 7.4,
+ "confidence": 0.93,
+ "source": "researcher://dr-patel",
+ "validFrom": "2026-04-20T09:58:00Z",
+ "recordedAt": "2026-04-21T09:00:00Z",
+ "derivedFrom": [
+ "fact:exp042-buffer-ph-run1-20260420t100000z-a1b2c3d4",
+ "fact:exp042-buffer-ph-run2-20260420t140000z-e5f6g7h8"
+ ],
+ "tags": ["consensus", "n2"]
+}
diff --git a/skills/kndl-memory/examples/scientific-lab/fact-exp042-ph-outlier-retracted-20260420t110000z-i9j0k1l2.fact.json b/skills/kndl-memory/examples/scientific-lab/fact-exp042-ph-outlier-retracted-20260420t110000z-i9j0k1l2.fact.json
new file mode 100644
index 0000000..7c9d952
--- /dev/null
+++ b/skills/kndl-memory/examples/scientific-lab/fact-exp042-ph-outlier-retracted-20260420t110000z-i9j0k1l2.fact.json
@@ -0,0 +1,16 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:exp042-buffer-ph-outlier-retracted-20260420t110000z-i9j0k1l2",
+ "@type": "Fact",
+ "statement": "Experiment EXP-042 pH reading of 6.9 in intermediate run retracted — electrode contamination confirmed",
+ "subject": "experiment:EXP-042",
+ "predicate": "buffer_ph",
+ "object": 6.9,
+ "confidence": 0.1,
+ "source": "researcher://dr-patel",
+ "validFrom": "2026-04-20T11:00:00Z",
+ "observedAt": "2026-04-20T10:58:00Z",
+ "recordedAt": "2026-04-20T12:30:00Z",
+ "negated": true,
+ "tags": ["retracted", "contamination"]
+}
diff --git a/skills/kndl-memory/examples/scientific-lab/fact-exp042-ph-run1-20260420t100000z-a1b2c3d4.fact.json b/skills/kndl-memory/examples/scientific-lab/fact-exp042-ph-run1-20260420t100000z-a1b2c3d4.fact.json
new file mode 100644
index 0000000..fb3ee4b
--- /dev/null
+++ b/skills/kndl-memory/examples/scientific-lab/fact-exp042-ph-run1-20260420t100000z-a1b2c3d4.fact.json
@@ -0,0 +1,14 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:exp042-buffer-ph-run1-20260420t100000z-a1b2c3d4",
+ "@type": "Fact",
+ "statement": "Experiment EXP-042 buffer pH measured at 7.41 in run 1 (electrode calibrated)",
+ "subject": "experiment:EXP-042",
+ "predicate": "buffer_ph",
+ "object": 7.41,
+ "confidence": 0.95,
+ "source": "instrument://lab-a/ph-meter-003",
+ "validFrom": "2026-04-20T10:00:00Z",
+ "observedAt": "2026-04-20T09:58:00Z",
+ "recordedAt": "2026-04-20T10:00:00Z"
+}
diff --git a/skills/kndl-memory/examples/scientific-lab/fact-exp042-ph-run2-20260420t140000z-e5f6g7h8.fact.json b/skills/kndl-memory/examples/scientific-lab/fact-exp042-ph-run2-20260420t140000z-e5f6g7h8.fact.json
new file mode 100644
index 0000000..fa76f2b
--- /dev/null
+++ b/skills/kndl-memory/examples/scientific-lab/fact-exp042-ph-run2-20260420t140000z-e5f6g7h8.fact.json
@@ -0,0 +1,14 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:exp042-buffer-ph-run2-20260420t140000z-e5f6g7h8",
+ "@type": "Fact",
+ "statement": "Experiment EXP-042 buffer pH measured at 7.38 in run 2 — slight drift, within tolerance",
+ "subject": "experiment:EXP-042",
+ "predicate": "buffer_ph",
+ "object": 7.38,
+ "confidence": 0.95,
+ "source": "instrument://lab-a/ph-meter-003",
+ "validFrom": "2026-04-20T14:00:00Z",
+ "observedAt": "2026-04-20T13:58:00Z",
+ "recordedAt": "2026-04-20T14:00:00Z"
+}
diff --git a/skills/kndl-memory/examples/scientific-lab/fact-exp042-published-20260501t000000z-q7r8s9t0.fact.json b/skills/kndl-memory/examples/scientific-lab/fact-exp042-published-20260501t000000z-q7r8s9t0.fact.json
new file mode 100644
index 0000000..f6edcf1
--- /dev/null
+++ b/skills/kndl-memory/examples/scientific-lab/fact-exp042-published-20260501t000000z-q7r8s9t0.fact.json
@@ -0,0 +1,16 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:exp042-published-result-20260501t000000z-q7r8s9t0",
+ "@type": "Fact",
+ "statement": "EXP-042 buffer pH result 7.40 ± 0.02 published in Journal of Chemical Biology, doi:10.1000/jcb.2026.042",
+ "subject": "experiment:EXP-042",
+ "predicate": "published_result_ph",
+ "object": 7.4,
+ "confidence": 0.99,
+ "source": "doi:10.1000/jcb.2026.042",
+ "validFrom": "2026-05-01T00:00:00Z",
+ "recordedAt": "2026-05-01T00:00:00Z",
+ "derivedFrom": [
+ "fact:exp042-buffer-ph-consensus-20260421t090000z-m3n4o5p6"
+ ]
+}
diff --git a/skills/kndl-memory/examples/threat-intel/fact-domain-c2-20260424t060000z-m3n4o5p6.fact.json b/skills/kndl-memory/examples/threat-intel/fact-domain-c2-20260424t060000z-m3n4o5p6.fact.json
new file mode 100644
index 0000000..3118d03
--- /dev/null
+++ b/skills/kndl-memory/examples/threat-intel/fact-domain-c2-20260424t060000z-m3n4o5p6.fact.json
@@ -0,0 +1,18 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:domain-update-example-com-c2-20260424t060000z-m3n4o5p6",
+ "@type": "Fact",
+ "statement": "update.example.com is an active Emotet C2 domain",
+ "subject": "domain:update.example.com",
+ "predicate": "indicator_type",
+ "object": "c2_emotet",
+ "confidence": 0.85,
+ "decay": "0.5/7d",
+ "source": "https://feeds.example.com/ti/emotet",
+ "validFrom": "2026-04-24T06:00:00Z",
+ "recordedAt": "2026-04-24T06:00:00Z",
+ "derivedFrom": [
+ "fact:hash-d41d8cd9-malware-20260423t080000z-i9j0k1l2"
+ ],
+ "tags": ["emotet", "c2", "domain"]
+}
diff --git a/skills/kndl-memory/examples/threat-intel/fact-domain-c2-alternative-20260424t070000z-q7r8s9t0.fact.json b/skills/kndl-memory/examples/threat-intel/fact-domain-c2-alternative-20260424t070000z-q7r8s9t0.fact.json
new file mode 100644
index 0000000..65e5585
--- /dev/null
+++ b/skills/kndl-memory/examples/threat-intel/fact-domain-c2-alternative-20260424t070000z-q7r8s9t0.fact.json
@@ -0,0 +1,13 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:domain-update-example-com-legitimate-20260424t070000z-q7r8s9t0",
+ "@type": "Fact",
+ "statement": "update.example.com is the legitimate update server for ExampleSoft products",
+ "subject": "domain:update.example.com",
+ "predicate": "indicator_type",
+ "object": "legitimate",
+ "confidence": 0.7,
+ "source": "https://vendor.examplesoft.com/domains",
+ "validFrom": "2026-04-24T07:00:00Z",
+ "recordedAt": "2026-04-24T07:00:00Z"
+}
diff --git a/skills/kndl-memory/examples/threat-intel/fact-hash-malware-20260423t080000z-i9j0k1l2.fact.json b/skills/kndl-memory/examples/threat-intel/fact-hash-malware-20260423t080000z-i9j0k1l2.fact.json
new file mode 100644
index 0000000..6aa7e64
--- /dev/null
+++ b/skills/kndl-memory/examples/threat-intel/fact-hash-malware-20260423t080000z-i9j0k1l2.fact.json
@@ -0,0 +1,13 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:hash-d41d8cd9-malware-20260423t080000z-i9j0k1l2",
+ "@type": "Fact",
+ "statement": "SHA256 d41d8cd98f00b204e9800998ecf8427e is a known loader for Emotet",
+ "subject": "hash:d41d8cd98f00b204e9800998ecf8427e",
+ "predicate": "malware_family",
+ "object": "Emotet",
+ "confidence": 0.98,
+ "source": "https://malware.example.com/reports/emotet-2026-04",
+ "validFrom": "2026-04-23T08:00:00Z",
+ "recordedAt": "2026-04-23T08:00:00Z"
+}
diff --git a/skills/kndl-memory/examples/threat-intel/fact-ip-192-0-2-1-c2-20260424t120000z-a1b2c3d4.fact.json b/skills/kndl-memory/examples/threat-intel/fact-ip-192-0-2-1-c2-20260424t120000z-a1b2c3d4.fact.json
new file mode 100644
index 0000000..c16b3d7
--- /dev/null
+++ b/skills/kndl-memory/examples/threat-intel/fact-ip-192-0-2-1-c2-20260424t120000z-a1b2c3d4.fact.json
@@ -0,0 +1,16 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:ip-192-0-2-1-c2-server-20260424t120000z-a1b2c3d4",
+ "@type": "Fact",
+ "statement": "192.0.2.1 is observed as an active C2 server for Cobalt Strike",
+ "subject": "ip:192.0.2.1",
+ "predicate": "indicator_type",
+ "object": "c2_cobalt_strike",
+ "confidence": 0.9,
+ "decay": "0.5/24h",
+ "source": "https://feeds.example.com/ti/cobalt-strike",
+ "validFrom": "2026-04-24T12:00:00Z",
+ "observedAt": "2026-04-24T11:45:00Z",
+ "recordedAt": "2026-04-24T12:00:00Z",
+ "tags": ["apt", "cobalt-strike", "c2"]
+}
diff --git a/skills/kndl-memory/examples/threat-intel/fact-ip-192-0-2-1-fp-20260425t090000z-e5f6g7h8.fact.json b/skills/kndl-memory/examples/threat-intel/fact-ip-192-0-2-1-fp-20260425t090000z-e5f6g7h8.fact.json
new file mode 100644
index 0000000..457eecb
--- /dev/null
+++ b/skills/kndl-memory/examples/threat-intel/fact-ip-192-0-2-1-fp-20260425t090000z-e5f6g7h8.fact.json
@@ -0,0 +1,16 @@
+{
+ "@context": "../../context/v1.jsonld",
+ "@id": "fact:ip-192-0-2-1-false-positive-20260425t090000z-e5f6g7h8",
+ "@type": "Fact",
+ "statement": "192.0.2.1 C2 classification was a false positive — IP belongs to a CDN edge node",
+ "subject": "ip:192.0.2.1",
+ "predicate": "indicator_type",
+ "object": "false_positive",
+ "confidence": 0.95,
+ "source": "https://abuse.example.com/whitelist",
+ "validFrom": "2026-04-25T09:00:00Z",
+ "recordedAt": "2026-04-25T09:00:00Z",
+ "supersedes": "fact:ip-192-0-2-1-c2-server-20260424t120000z-a1b2c3d4",
+ "negated": true,
+ "tags": ["retraction"]
+}
diff --git a/spec/SPECIFICATION.md b/spec/SPECIFICATION.md
deleted file mode 100644
index 1e08c86..0000000
--- a/spec/SPECIFICATION.md
+++ /dev/null
@@ -1,1168 +0,0 @@
-# KNDL Language Specification
-
-**Knowledge Node Description Language**
-Version 1.0.0 — April 2026
-
----
-
-## 1. Introduction
-
-### 1.1 Purpose
-
-KNDL (pronounced "kindle") is a language designed for AI agents to represent,
-store, query, and exchange structured knowledge. Unlike general-purpose data
-formats (JSON, YAML, XML) or presentation formats (Markdown, HTML), KNDL is
-purpose-built for agent cognition: every assertion carries confidence,
-provenance, temporal scope, and typed relationships as first-class constructs.
-
-### 1.2 Design Goals
-
-1. **Semantic-first**: Structure encodes meaning, not presentation.
-2. **Confidence-native**: Every fact carries uncertainty (scalar or distribution).
-3. **Graph-structured**: Knowledge is a directed graph with typed edges.
-4. **Bitemporal**: Valid time (when true in the world) and recorded time
- (when the system learned it) are tracked independently.
-5. **Provenance-tracked**: Every assertion traces to its source;
- cryptographic provenance is supported but optional.
-6. **Dimensionally safe**: Physical quantities carry units; money carries
- currency. The type system rejects dimensionally incoherent operations.
-7. **Agent-actionable**: Intents encode trigger-action patterns; processes
- encode sequenced workflows with preconditions and compensation.
-8. **Composable**: Types support intersection, union, optional, parameters,
- and value constraints.
-9. **Dual-format**: Human-readable text (`.kndl`) and compact binary (`.kndlb`).
-10. **Profile-friendly**: Domain profiles (IoT, FinTech, Healthcare, …) add
- conventions without changing the core language.
-
-### 1.3 Knowledge Model
-
-KNDL adopts an **open-world assumption**: absence of a fact does not imply
-its negation. To assert that something is known to be false, use the
-`~negated true` meta-annotation (§4.3.1). This matters for domains like
-medicine ("no history of diabetes" is a positive assertion of absence, not
-missing data) and security ("no observed connection" vs "not observed").
-
-Every assertion has an **epistemic component** (how sure the agent is that
-the assertion is true) represented by `~confidence`, and optionally an
-**aleatoric component** (how variable the asserted value itself is)
-represented by `~uncertainty` (§9). These are distinct: a stock price
-measurement may be 100% confidently observed yet aleatorically volatile.
-
-### 1.4 Notation Conventions
-
-- `UPPERCASE` — grammar non-terminals
-- `'literal'` — literal tokens
-- `[x]` — optional
-- `{x}` — zero or more repetitions
-- `x | y` — alternatives
-- `(x)` — grouping
-
----
-
-## 2. Lexical Structure
-
-### 2.1 Character Set
-
-KNDL source files are encoded in UTF-8. All keywords and operators use ASCII.
-String values may contain any valid Unicode.
-
-### 2.2 Whitespace and Line Terminators
-
-Whitespace characters (space U+0020, tab U+0009) are insignificant except
-within string literals. Line terminators (LF U+000A, CR U+000D, CRLF) separate
-logical lines. Blank lines are ignored.
-
-### 2.3 Comments
-
-```
-COMMENT = LINE_COMMENT | BLOCK_COMMENT
-LINE_COMMENT = '//' {any char except newline} NEWLINE
-BLOCK_COMMENT = '/*' {any char} '*/'
-```
-
-Comments are stripped during lexing and carry no semantic weight. Block
-comments may be nested.
-
-### 2.4 Identifiers
-
-```
-IDENTIFIER = LETTER { LETTER | DIGIT | '_' }
-LETTER = 'a'..'z' | 'A'..'Z'
-DIGIT = '0'..'9'
-```
-
-Identifiers are case-sensitive. Reserved keywords (§2.6) cannot be used as
-identifiers.
-
-### 2.5 Node References
-
-```
-NODE_REF = '@' IDENTIFIER { '.' IDENTIFIER }
-```
-
-Node references begin with `@` and may use dot-notation for path traversal:
-`@building_7.floor_3.sensor_01`.
-
-### 2.6 Reserved Keywords
-
-```
-node edge type intent context query process
-match where return with emit do state
-trigger cron if else in and on
-or not true false null now goto
-last within overlaps aggregate group as compensate
-import export from optional by of
-sum avg min max count
-```
-
-The keyword `group` is reserved for the query `group by` clause and is
-**not** an aggregation function (see §5.4).
-
-### 2.7 Operators
-
-```
-= Assignment
-:: Type annotation
--> Directed edge (forward)
-<-> Bidirectional edge (sugar for two directed edges)
--[T]-> Typed directed edge
-<-[T]- Reversed typed edge
--[T]- Undirected typed edge (sugar for <-[T]-> )
-.. Range operator
-*..N Path repetition lower bound N
-*N..M Path repetition range N..M
-{ Block open
-} Block close
-#{ Map literal open (disambiguates from block)
-[ Array open / typed-edge bracket
-] Array close
-( Group open
-) Group close
-, Separator
-: Type field declaration
-? Optional type marker
-& Type intersection
-| Type union
-> < >= <= == != Comparison operators
-&& || Logical operators
-+ - * / % Arithmetic operators
-```
-
-### 2.8 Literals
-
-#### 2.8.1 Integer Literals
-
-```
-INT_LITERAL = ['-'] DIGIT { DIGIT }
- | '0x' HEX_DIGIT { HEX_DIGIT }
- | '0b' BIN_DIGIT { BIN_DIGIT }
-```
-
-#### 2.8.2 Float Literals
-
-```
-FLOAT_LITERAL = ['-'] DIGIT { DIGIT } '.' DIGIT { DIGIT } [ EXPONENT ]
-EXPONENT = ('e' | 'E') ['+' | '-'] DIGIT { DIGIT }
-```
-
-#### 2.8.3 Decimal Literals
-
-Arbitrary-precision decimals, used for money and any value where binary
-floating-point rounding is unacceptable.
-
-```
-DECIMAL_LITERAL = ['-'] DIGIT { DIGIT } '.' DIGIT { DIGIT } 'd'
-```
-
-Examples: `19.99d`, `0.0001d`.
-
-#### 2.8.4 String Literals
-
-```
-STRING = '"' { STRING_CHAR | ESCAPE } '"'
-STRING_CHAR = any Unicode char except '"' and '\'
-ESCAPE = '\' ( '"' | '\' | '/' | 'n' | 'r' | 't' | 'u' HEX4 )
-HEX4 = HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT
-```
-
-Triple-quoted strings (`"""..."""`) allow embedded newlines without escapes.
-
-#### 2.8.5 Boolean Literals
-
-```
-BOOL_LITERAL = 'true' | 'false'
-```
-
-#### 2.8.6 Null Literal
-
-```
-NULL_LITERAL = 'null'
-```
-
-#### 2.8.7 Duration Literals
-
-```
-DURATION = DIGIT { DIGIT } DURATION_UNIT
-DURATION_UNIT = 'ns' | 'us' | 'ms' | 's' | 'm' | 'h' | 'd' | 'w' | 'mo' | 'y'
-```
-
-Durations `mo` and `y` are **calendar** durations (variable wall-clock
-length). `d` and shorter are **exact** durations (SI seconds). Mixing
-calendar and exact durations in arithmetic is a type error.
-
-Examples: `5s`, `30m`, `24h`, `7d`, `100ms`, `6mo`, `30y`.
-
-#### 2.8.8 Datetime Literals
-
-```
-DATETIME = DATE [ 'T' TIME [ TIMEZONE ] ]
-DATE = YEAR '-' MONTH '-' DAY
- | YEAR '-Q' QUARTER
- | YEAR '-W' WEEK
-TIME = HOUR ':' MINUTE [ ':' SECOND [ '.' FRACTION ] ]
-TIMEZONE = 'Z' | ('+' | '-') HOUR ':' MINUTE
-```
-
-Examples: `2026-04-10`, `2026-04-10T14:30:00Z`, `2026-Q1`.
-
-#### 2.8.9 Quantity Literals
-
-Physical quantities combine a numeric magnitude with a unit expression
-(§3.4). Whitespace between magnitude and unit is optional.
-
-```
-QUANTITY_LITERAL = (INT_LITERAL | FLOAT_LITERAL | DECIMAL_LITERAL) UNIT_EXPR
-UNIT_EXPR = UNIT_ATOM { ('*' | '/' | '^' INT_LITERAL) UNIT_ATOM }
-UNIT_ATOM = '°C' | '°F' | 'K'
- | 'm' | 'cm' | 'mm' | 'km' | 'ft' | 'in'
- | 'kg' | 'g' | 'mg' | 'lb'
- | 's' | 'ms' | 'min' | 'hr'
- | 'A' | 'V' | 'W' | 'Wh' | 'kWh' | 'J'
- | 'Pa' | 'kPa' | 'bar'
- | 'mol' | 'cd' | 'lm' | 'lx'
- | 'Hz' | 'kHz' | 'MHz' | 'GHz'
- | 'B' | 'KB' | 'MB' | 'GB' | 'TB'
- | 'bps' | 'kbps' | 'Mbps' | 'Gbps'
- | IDENTIFIER (* user-registered unit *)
-```
-
-Examples: `22.5 °C`, `5 m/s`, `9.81 m/s^2`, `100 kWh`, `3.14e8 m/s`.
-
-A quantity literal produces a value of type `Quantity` where `D` is the
-dimension inferred from the unit (§3.4).
-
-#### 2.8.10 Money Literals
-
-```
-MONEY_LITERAL = DECIMAL_LITERAL ' ' ISO4217
- | DECIMAL_LITERAL ISO4217
-ISO4217 = 'USD' | 'EUR' | 'GBP' | 'JPY' | ... (* ISO 4217 3-letter *)
-```
-
-Examples: `19.99d USD`, `1000.00d EUR`.
-
-Shorthand for amounts with no fractional part: `100 USD` (compiler inserts
-`.00d`).
-
-#### 2.8.11 Bytes Literals
-
-```
-BYTES_LITERAL = 'b"' { BASE64_CHAR } '"'
-```
-
-#### 2.8.12 Vector Literals
-
-```
-VECTOR_LITERAL = 'v[' FLOAT_LITERAL { ',' FLOAT_LITERAL } ']'
-```
-
-Example: `v[0.12, -0.03, 0.91]` — a 3-dimensional vector of floats.
-
-Vectors are fixed-dimension at declaration time; see §3.1 `Vector`.
-
----
-
-## 3. Type System
-
-### 3.1 Primitive Types
-
-| Type | Description | Literal Examples |
-|---------------|---------------------------------------|---------------------------|
-| `Int` | 64-bit signed integer | `42`, `-7`, `0xFF` |
-| `Float` | 64-bit IEEE 754 | `3.14`, `-0.5`, `1e6` |
-| `Decimal` | Arbitrary-precision decimal | `19.99d`, `0.0001d` |
-| `String` | UTF-8 text | `"hello"` |
-| `Bool` | Boolean | `true`, `false` |
-| `Duration` | Time span (exact) | `5s`, `30m` |
-| `CalDuration` | Calendar duration (wall-clock) | `6mo`, `1y` |
-| `DateTime` | Point in time | `2026-04-10T14:00Z` |
-| `Null` | Absent value | `null` |
-| `Bytes` | Raw binary (base64 in text form) | `b"SGVsbG8="` |
-| `Quantity` | Magnitude with dimension `D` | `22.5 °C`, `5 m/s` |
-| `Money` | Amount with ISO 4217 currency | `19.99d USD` |
-| `Vector` | Fixed-dimension vector of `Float` | `v[0.1, 0.2, 0.3]` |
-| `UUID` | 128-bit identifier | `u"0189..."` |
-
-### 3.2 Collection Types
-
-```
-Array Ordered sequence of elements of type T
-Map Key-value mapping
-Set Unordered unique collection
-```
-
-### 3.3 Parameterised Built-in Types
-
-| Type | Description |
-|-----------------------|-----------------------------------------------|
-| `Code` | Term from a coded vocabulary (ICD, SNOMED…) |
-| `Localized` | Value of type `T` keyed by BCP-47 locale tag |
-| `Frame` | Named coordinate frame (§3.5) |
-| `Pose ` | Position + orientation in a named frame |
-| `Distribution` | Probability distribution over `T` (§9) |
-
-Example:
-
-```kndl
-type Diagnosis {
- code : Code<"ICD-10">
- name : Localized
- onset : DateTime?
-}
-```
-
-### 3.4 Units & Dimensions
-
-KNDL tracks **physical dimensions** through the type system. Every
-quantity literal has an inferred dimension; operations that would produce
-a dimensionally incoherent result are rejected at parse time.
-
-The seven SI base dimensions are:
-
-| Symbol | Dimension |
-|--------|---------------------|
-| `L` | length |
-| `M` | mass |
-| `T` | time |
-| `I` | electric current |
-| `Θ` | temperature |
-| `N` | amount of substance |
-| `J` | luminous intensity |
-
-Derived dimensions are expressed as products of powers (`L*T^-1` is velocity).
-`Quantity` with dimension `D` is the canonical type for values carrying
-units. Unit conversion within the same dimension is permitted and automatic:
-`22 °C` and `295.15 K` are equal after normalisation.
-
-```kndl
-type Temperature = Quantity<Θ> // any temperature unit
-type Velocity = Quantity
-type Energy = Quantity
-```
-
-User-defined units may be registered in a module:
-
-```kndl
-type BTU = Quantity where { .unit == "BTU" }
-```
-
-### 3.5 Frames
-
-A `Frame` names a coordinate system. Positions and poses carry a frame
-reference; transforms between frames are first-class nodes.
-
-```kndl
-type Frame { id : String, parent : Frame? }
-
-type Pose {
- x : Float, y : Float, z : Float
- qx : Float, qy : Float, qz : Float, qw : Float
-}
-
-node @world :: Frame { id = "world" }
-node @base :: Frame { id = "base_link", parent -> @world }
-
-node @gripper_pose :: Pose<@base> {
- x = 0.3, y = 0.0, z = 0.5
- qx = 0.0, qy = 0.0, qz = 0.0, qw = 1.0
-}
-```
-
-### 3.6 Type Declarations
-
-```
-TYPE_DECL = 'type' TYPE_NAME [ TYPE_PARAMS ]
- [ '=' TYPE_EXPR ]
- [ '{' TYPE_BODY '}' ]
- [ 'where' '{' { CONSTRAINT } '}' ]
-TYPE_PARAMS = '<' TYPE_PARAM { ',' TYPE_PARAM } '>'
-TYPE_PARAM = IDENTIFIER [ '::' TYPE_EXPR ]
-TYPE_EXPR = TYPE_NAME [ '<' TYPE_ARG { ',' TYPE_ARG } '>' ]
- | TYPE_EXPR '&' TYPE_EXPR (* intersection *)
- | TYPE_EXPR '|' TYPE_EXPR (* union *)
- | STRING (* literal type *)
- | TYPE_EXPR '?' (* optional *)
- | '{' TYPE_BODY '}' (* anonymous struct *)
-TYPE_BODY = { FIELD_DECL }
-FIELD_DECL = IDENTIFIER ':' TYPE_EXPR
-CONSTRAINT = EXPRESSION
-```
-
-Intersection, union, optional, and constrained types (examples below):
-
-```kndl
-type SmartSensor = Device & Measurement & { firmware : SemVer }
-
-type Protocol = "knx" | "bacnet" | "modbus" | "zigbee" | "matter"
-
-type Temperature = Quantity<Θ> where {
- .value >= 0.0 K
-}
-```
-
----
-
-## 4. Core Constructs
-
-### 4.1 Node Declaration
-
-```
-NODE_DECL = 'node' NODE_REF '::' TYPE_EXPR '{' NODE_BODY '}'
-NODE_BODY = { FIELD_ASSIGN | INLINE_EDGE | META_ANNOTATION }
-FIELD_ASSIGN = IDENTIFIER '=' EXPRESSION
-INLINE_EDGE = IDENTIFIER '->' NODE_REF
-```
-
-Map literals in expression position use `#{ ... }` (§7.1) to avoid parsing
-ambiguity with node bodies and blocks.
-
-```kndl
-node @sensor_t001 :: Temperature {
- value = 22.5 °C
- location -> @building_7
- ~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
-}
-```
-
-### 4.2 Edge Declaration
-
-```
-EDGE_DECL = 'edge' NODE_REF EDGE_OP TARGET_SPEC [ '{' EDGE_BODY '}' ]
-EDGE_OP = '->' | '<->'
- | '-[' TYPE_NAME ']->'
- | '<-[' TYPE_NAME ']-'
- | '-[' TYPE_NAME ']-'
-TARGET_SPEC = NODE_REF | '[' NODE_REF { ',' NODE_REF } ']'
-```
-
-Edge direction arities are now unambiguous:
-
-- `-[T]->` — forward typed edge
-- `<-[T]-` — reverse typed edge (creates one edge from target to source)
-- `-[T]-` — undirected typed edge (sugar for two directed edges)
-- `<->` — undirected untyped edge
-
-```kndl
-edge @room_204 -[located_in]-> @floor_2
-edge @building_7 -[contains]-> [ @floor_1, @floor_2, @floor_3 ]
-edge @router_a -[peer]- @router_b // undirected BGP peering
-```
-
-### 4.3 Meta-Annotations
-
-```
-META_ANNOTATION = '~' META_KEY META_VALUE
-META_KEY = IDENTIFIER [ ':' IDENTIFIER ] (* ns:key namespace *)
-META_VALUE = EXPRESSION
- | EXPRESSION '..' EXPRESSION (* range *)
- | EXPRESSION '/' DURATION (* decay rate *)
- | '{' { META_FIELD } '}' (* structured *)
-```
-
-#### 4.3.1 Standard Meta-Annotations
-
-| Key | Value Type | Description |
-|------------------|---------------------------------|-------------------------------------------------|
-| `~confidence` | `Float` (0.0–1.0) | Epistemic certainty of the assertion |
-| `~uncertainty` | `Distribution` (§9) | Aleatoric variability of the asserted value |
-| `~source` | `String` (URI) or `NodeRef` | Asserting entity |
-| `~valid` | `DateTime .. DateTime` | When the fact is true in the world |
-| `~recorded` | `DateTime` | When the system learned the fact |
-| `~observed` | `DateTime` | When the fact was directly observed |
-| `~decay` | `Float / Duration` | Confidence decay rate over time |
-| `~supersedes` | `NodeRef` | Previous version of this knowledge |
-| `~derived` | `Array` | Nodes this was computed from |
-| `~inference` | `NodeRef` | Reference to the inference rule / activity |
-| `~negated` | `Bool` | Strong negation (open-world assumption) |
-| `~access` | `{...}` policy block (§4.3.5) | Structured access policy |
-| `~weight` | `Float` (0.0–1.0) | Relative importance (edges) |
-| `~priority` | `Float` (0.0–1.0) | Execution priority (intents / actions) |
-| `~deadline` | `DateTime` or `Duration` | Latency budget for intents |
-| `~cooldown` | `Duration` | Minimum time between intent firings |
-| `~tags` | `Array` | Free-form labels |
-| `~version` | `Int` | Schema version |
-| `~frame` | `NodeRef` (`Frame`) | Coordinate frame for spatial fields |
-| `~sample_rate` | `Quantity` | Sampling rate for streamed facts |
-| `~last_seen` | `DateTime` | Last contact from a source (liveness) |
-| `~signature` | `{alg, key, sig}` block | Detached cryptographic signature |
-| `~attestation` | `NodeRef` | Reference to an attestation node |
-| `~classification`| `String` | Data sensitivity class (PHI, PCI, PII, …) |
-| `~retention` | `Duration` or `DateTime` | Retention policy / scheduled deletion |
-| `~consent` | `NodeRef` | Consent scope node (healthcare/GDPR) |
-
-#### 4.3.2 Custom Meta-Annotations
-
-Custom annotations MUST use a namespace prefix:
-
-```kndl
-~iot:sampling_rate 1000 Hz
-~fhir:effective_period 2026-04-10 .. 2026-04-30
-~stix:confidence_label "high"
-```
-
-Reserved namespaces: `iot`, `fin`, `hl7`, `fhir`, `stix`, `isa95`, `brick`,
-`matter`, `prov`, `w3c`. These SHOULD follow the relevant external standard.
-
-#### 4.3.3 Confidence Semantics
-
-- `0.0` — known false (equivalent to `~negated true ~confidence 1.0`)
-- `0.0 < c < 0.5` — leaning false
-- `0.5` — maximum uncertainty
-- `0.5 < c < 1.0` — leaning true
-- `1.0` — axiomatic
-
-With `~decay`, effective confidence at time `t` is:
-
-```
-effective(t) = ~confidence × (rate ^ ((t - t₀) / window))
-```
-
-where `t₀` is the start of `~valid` (or `~observed` if present).
-
-#### 4.3.4 Bitemporal Semantics
-
-Three temporal annotations play distinct roles:
-
-- `~valid` — when the fact holds **in the world**.
-- `~observed` — when a sensor/agent **directly saw** the fact.
-- `~recorded` — when the fact **entered the system**.
-
-Queries may restrict over any axis. "What did we know on 2026-01-01 about
-readings from 2025-Q4?" requires both `~recorded <= 2026-01-01` and
-`~valid overlaps 2025-Q4`.
-
-#### 4.3.5 Structured Access Policy
-
-```
-~access {
- read = ["role:operators", "role:building-7-team"]
- write = ["role:admins"]
- purpose = ["operations", "billing"]
- classify = "PII"
-}
-```
-
-Policy evaluation is implementation-defined but MUST be deterministic:
-two policies with identical fields MUST yield the same decision for the
-same subject/action.
-
-#### 4.3.6 Negation and Open-World
-
-`~negated true` asserts that the fact is **known false**. Absence of a
-matching node MUST NOT be interpreted as `~negated true` — that is the
-open-world assumption (§1.3). Example:
-
-```kndl
-node @pat_001.hx_diabetes :: MedicalHistoryItem {
- condition = "diabetes_mellitus"
- ~negated true
- ~confidence 0.95
- ~source "user://dr-wong"
-}
-```
-
-### 4.4 Context Declaration
-
-Meta-annotations are inherited from parent contexts. A `~tenant` meta-annotation
-is reserved for multi-tenant isolation — a query engine MUST refuse to return
-nodes across tenants without explicit `~access` override.
-
-### 4.5 Intent Declaration
-
-```
-INTENT_DECL = 'intent' NODE_REF '::' TYPE_EXPR '{' INTENT_BODY '}'
-INTENT_BODY = TRIGGER_CLAUSE DO_CLAUSE { META_ANNOTATION }
-TRIGGER_CLAUSE = 'trigger' '=' TRIGGER_EXPR
-TRIGGER_EXPR = QUERY_DECL | EXPRESSION | 'cron' STRING
-DO_CLAUSE = 'do' '{' { ACTION } '}'
-ACTION = EMIT_ACTION | UPDATE_ACTION | DELETE_ACTION | GOTO_ACTION
-EMIT_ACTION = 'emit' NODE_DECL
-UPDATE_ACTION = 'emit' 'update' NODE_REF '{' NODE_BODY '}'
-DELETE_ACTION = 'emit' 'delete' NODE_REF
-GOTO_ACTION = 'goto' STATE_REF (* within a process *)
-```
-
-Intents remain reactive rules. Sequenced behaviour belongs in processes
-(§6).
-
-### 4.6 Node, Edge, Intent Identity
-
-Every declaration generates a stable 128-bit UUID derived from:
-
-1. The fully-qualified node reference (context path + local id), or
-2. An explicit `~id` annotation if provided.
-
-This enables distributed systems to agree on identifiers without a central
-registry.
-
----
-
-## 5. Query Language
-
-### 5.1 Query Syntax
-
-```
-QUERY_DECL = 'query' [ IDENTIFIER ] '{' QUERY_BODY '}'
-QUERY_BODY = { MATCH_CLAUSE } [ WHERE_CLAUSE ]
- [ GROUP_CLAUSE ] RETURN_CLAUSE
-MATCH_CLAUSE = [ 'optional' ] 'match' PATH_PATTERN
-WHERE_CLAUSE = 'where' EXPRESSION
-GROUP_CLAUSE = 'group' 'by' EXPRESSION { ',' EXPRESSION }
-RETURN_CLAUSE = 'return' RETURN_EXPR
-
-PATH_PATTERN = STEP { EDGE_STEP STEP }
-STEP = VAR_BIND '::' TYPE_EXPR
- | NODE_REF
-EDGE_STEP = EDGE_OP
- | '-[' EDGE_TYPE REPETITION? ']->'
- | '<-[' EDGE_TYPE REPETITION? ']-'
- | '-[' EDGE_TYPE REPETITION? ']-'
-EDGE_TYPE = IDENTIFIER | VAR_BIND
-REPETITION = '*' INT_LITERAL
- | '*' INT_LITERAL '..' INT_LITERAL
- | '*' '..' INT_LITERAL
- | '*' (* 1..∞, capped by engine *)
-```
-
-### 5.2 Multi-Hop Paths
-
-Path patterns with repetition find paths of variable length:
-
-```kndl
-// 1 to 5 contains-hops from campus to any sensor
-query campus_sensors {
- match ?s :: Sensor
- <-[contains*1..5]- @campus
- return ?s
-}
-
-// Named path variable for trace reconstruction
-query shipment_route {
- match ?p = ?origin -[ships_to*]-> ?dest
- where ?origin == @hub_frankfurt
- && ?dest == @hub_tokyo
- return { hops: len(?p), path: ?p }
-}
-```
-
-### 5.3 Variables and Optional Matches
-
-Variable binding and optional match patterns are supported.
-
-### 5.4 Return, Group, Aggregate
-
-Aggregation is no longer a sub-clause of `return`; grouping is a top-level
-`group by` clause.
-
-```
-RETURN_EXPR = EXPRESSION
- | EXPRESSION 'with' 'edges' INT_LITERAL
- | AGG_FIELD { ',' AGG_FIELD } (* implicit group *)
-AGG_FIELD = IDENTIFIER '=' AGG_FUNC '(' EXPRESSION ')'
- | IDENTIFIER '=' EXPRESSION (* passthrough *)
-AGG_FUNC = 'sum' | 'avg' | 'min' | 'max' | 'count'
-```
-
-`group` is a **clause**, not a function. Example:
-
-```kndl
-query daily_power {
- match ?m :: PowerMeasurement -[at]-> ?site :: Site
- group by ?site, day(?m.~observed)
- return {
- site = ?site,
- day = day(?m.~observed),
- total = sum(?m.value)
- }
-}
-```
-
-### 5.5 Full Example
-
-```kndl
-query hot_rooms {
- match ?sensor :: Temperature
- -[located_in]-> ?room :: Room
- optional match ?fault :: SystemFault
- -[affects]-> ?room
- where
- ?sensor.value > 26 °C
- && ?sensor.~confidence > 0.8
- && ?sensor.~valid overlaps now
- return {
- room = ?room,
- temperature = ?sensor.value,
- confidence = ?sensor.~confidence,
- has_fault = ?fault != null
- }
-}
-```
-
----
-
-## 6. Processes (Stateful Workflows)
-
-A **process** encodes an ordered workflow with states, transitions,
-preconditions, and compensation. Unlike intents (reactive, stateless), a
-process has a persistent current state per instance.
-
-```
-PROCESS_DECL = 'process' NODE_REF '::' TYPE_EXPR '{' PROCESS_BODY '}'
-PROCESS_BODY = { STATE_DECL | TRANSITION_DECL | META_ANNOTATION }
-STATE_DECL = 'state' IDENTIFIER [ '{' { META_ANNOTATION } '}' ]
-TRANSITION_DECL = 'on' EVENT_EXPR 'in' IDENTIFIER '->' IDENTIFIER
- [ 'where' EXPRESSION ]
- [ 'do' '{' { ACTION } '}' ]
- [ 'compensate' '{' { ACTION } '}' ]
-EVENT_EXPR = IDENTIFIER | QUERY_DECL
-```
-
-Example — shipment lifecycle:
-
-```kndl
-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 {
- emit node :: Alert { severity = "warn", message = "delivery rollback" }
- }
-}
-```
-
-Processes compose with intents: a transition's `do` block may emit intents
-that fire elsewhere in the graph.
-
----
-
-## 7. Expression Language
-
-### 7.1 Expression Grammar
-
-```
-EXPRESSION = LITERAL
- | NODE_REF
- | VAR_BIND
- | ACCESS_EXPR
- | BINARY_EXPR
- | UNARY_EXPR
- | FUNC_CALL
- | '(' EXPRESSION ')'
- | ARRAY_LITERAL
- | MAP_LITERAL
-ACCESS_EXPR = EXPRESSION '.' IDENTIFIER
- | EXPRESSION '[' EXPRESSION ']'
-BINARY_EXPR = EXPRESSION BINARY_OP EXPRESSION
-UNARY_EXPR = UNARY_OP EXPRESSION
-FUNC_CALL = IDENTIFIER '(' [ EXPRESSION { ',' EXPRESSION } ] ')'
-ARRAY_LITERAL = '[' [ EXPRESSION { ',' EXPRESSION } ] ']'
-MAP_LITERAL = '#{' [ KV_PAIR { ',' KV_PAIR } ] '}'
-KV_PAIR = EXPRESSION ':' EXPRESSION
-```
-
-Map literals use `#{ ... }` to remove ambiguity against node bodies,
-blocks, and `do { }` sections.
-
-### 7.2 Operator Precedence (high → low)
-
-1. `.` `[]` — access (not binary operators)
-2. `not` `-` (unary)
-3. `*` `/` `%`
-4. `+` `-`
-5. `..` — range
-6. `>` `<` `>=` `<=`
-7. `==` `!=`
-8. `in` `overlaps` `within` `matches`
-9. `&&` `and`
-10. `||` `or`
-
-### 7.3 Built-in Functions
-
-| Function | Signature | Description |
-|--------------------|------------------------------------|---------------------------------|
-| `len(x)` | `Array -> Int` | Array length |
-| `keys(x)` | `Map -> Array` | Map keys |
-| `values(x)` | `Map -> Array` | Map values |
-| `abs(x)` | `Quantity -> Quantity` | Absolute value |
-| `floor(x)` | `Float -> Int` | Floor |
-| `ceil(x)` | `Float -> Int` | Ceiling |
-| `round(x, n)` | `Decimal, Int -> Decimal` | Bankers' round |
-| `now()` | `-> DateTime` | Current timestamp |
-| `elapsed(dt)` | `DateTime -> Duration` | Time since `dt` |
-| `day(dt)` | `DateTime -> Date` | Truncate to day |
-| `convert(q, unit)` | `Quantity, UnitExpr -> Quantity` | Unit conversion |
-| `convert_money(m, ccy, rate)` | `Money, ISO4217, Decimal -> Money` | Currency conversion |
-| `uuid()` | `-> UUID` | Generate UUID v7 |
-| `hash(x)` | `Any -> Bytes` | BLAKE3-256 hash |
-| `merge(a, b)` | `Node, Node -> Node` | Merge two nodes |
-| `weighted_avg` | `Array<(Float,Float)> -> Float` | Confidence-weighted average |
-| `similarity(a, b)` | `Vector, Vector -> Float` | Cosine similarity |
-| `verify(sig, msg, key)` | `Signature, Bytes, Key -> Bool` | Verify detached signature |
-| `transform(p, fr)` | `Pose, Frame -> Pose` | Change coordinate frame |
-
----
-
-## 8. Module System
-
-Imports are URI-based (`kndl://std/...`).
-
-```kndl
-import { Temperature, Quantity } from "kndl://std/units"
-import { Money } from "kndl://std/money"
-import { Frame, Pose } from "kndl://std/frames"
-import { Diagnosis, Medication } from "kndl://std/healthcare"
-```
-
----
-
-## 9. Uncertainty Model
-
-`~confidence` remains a scalar in [0.0, 1.0]. `~uncertainty` describes the
-**distribution of the asserted value** and is parameterised by the field's
-type:
-
-```kndl
-// Gaussian — robotics pose
-~uncertainty gaussian { mean: 0.0, stddev: 0.03 }
-
-// Interval — sensor calibration bound
-~uncertainty interval { min: 21.8 °C, max: 23.2 °C }
-
-// Categorical — differential diagnosis
-~uncertainty categorical {
- "J45.9": 0.6, // asthma
- "J44.9": 0.3, // COPD
- "R05.9": 0.1 // cough, unspecified
-}
-
-// Histogram — empirical
-~uncertainty histogram {
- bins: [0 W, 100 W, 200 W, 300 W],
- counts: [12, 58, 21, 4]
-}
-```
-
-A conforming Level 3 implementation MUST support `gaussian`, `interval`,
-and `categorical`. `histogram` is optional.
-
-Aleatoric and epistemic channels compose: an agent can be 95% confident
-that a robot's pose is a Gaussian with σ=3 cm. One value, two sources of
-uncertainty, tracked separately.
-
----
-
-## 10. Serialization
-
-### 10.1 Text Format (.kndl)
-
-Human-readable format described throughout. UTF-8, `.kndl` extension.
-
-### 10.2 Binary Format (.kndlb)
-
-Compact binary encoding for wire transport and storage.
-
-#### 10.2.1 File Header
-
-```
-Offset Size Field Description
-0 4 magic ASCII "KNDL"
-4 2 version Protocol version (major.minor)
-6 2 flags Bit flags:
- bit 0: compressed (zstd)
- bit 1: encrypted
- bit 2: has type table
- bit 3: has intent table
- bit 4: compact id profile (varint ids)
- bit 5: has signature block
-8 4 node_count uint32 BE
-12 4 edge_count uint32 BE
-16 4 type_count uint32 BE
-20 4 intent_count uint32 BE
-24 4 process_count uint32 BE
-28 4 string_pool_size uint32 BE
-32 32 payload_hash BLAKE3-256 of payload (replaces CRC32)
-64 ... payload
-```
-
-Endianness is big-endian throughout the header for network ordering.
-
-#### 10.2.2 Compact ID Profile
-
-When flag bit 4 is set, node and edge ids are varint-encoded integers
-scoped to the file, rather than 128-bit UUIDs. Used for constrained IoT
-channels (LoRaWAN, BLE mesh) where 16 bytes per id is prohibitive.
-
-#### 10.2.3 String Pool, Node Block, Edge Block
-
-Node/edge blocks include an `uncertainty_type` byte after `confidence` to
-encode structured uncertainty. Quantity values are encoded as
-`(magnitude: float64, unit_ref: uint32)` pairs.
-
-#### 10.2.4 Signature Block
-
-When flag bit 5 is set, the file ends with a detached signature block:
-
-```
-Field Size Encoding
-alg_ref 4 String pool index ("ed25519", "ecdsa-p256", ...)
-key_ref 4 String pool index (key id / URI)
-sig_len 2 uint16
-signature sig_len Raw signature bytes
-```
-
-The signature covers the payload hash in the header.
-
----
-
-## 11. Source URIs
-
-Supported URI schemes:
-
-| Scheme | Description | Example |
-|-------------------|------------------------------|--------------------------------------|
-| `matter://` | Matter-protocol device | `matter://node-0x5A/cluster-0x0402` |
-| `bacnet://` | BACnet object | `bacnet://192.0.2.10/analog-input/3` |
-| `modbus://` | Modbus register | `modbus://plc-03/holding/40021` |
-| `fhir://` | FHIR resource | `fhir://hospital/Observation/123` |
-| `stix://` | STIX indicator | `stix://indicator--abc-…` |
-| `did:` | Decentralized Identifier | `did:web:example.com` |
-| `gtin:` | Global Trade Item Number | `gtin:04012345678901` |
-| `oci://` | OCI artifact | `oci://ghcr.io/foo/bar@sha256:…` |
-
----
-
-## 12. Conformance Levels
-
-### Level 1: Core
-
-Node/edge declarations, primitives, standard meta-annotations
-(`~confidence`, `~source`, `~valid`, `~recorded`, `~negated`), comments,
-text parsing & serialisation, **units & Quantity**, **Money**, **Decimal**.
-
-### Level 2: Extended
-
-Everything in Level 1 plus: type declarations (generic parameters,
-intersection, union, optional, constraints), contexts with inheritance,
-expression evaluation, query language (including multi-hop paths and
-`group by`), import/export, binary format, **Vector**, **Frame/Pose**,
-**Localized**, **Code**.
-
-### Level 3: Agent
-
-Everything in Level 2 plus: intents with all trigger types, **processes
-with states/transitions/compensation**, confidence decay computation,
-query aggregation, temporal operators, full built-in library,
-**structured uncertainty** (`gaussian`, `interval`, `categorical`),
-**cryptographic provenance** (`~signature`, `verify()`).
-
----
-
-## 13. EBNF Grammar Summary
-
-The authoritative grammar lives in `spec/grammar/kndl.ebnf`. A textual
-summary:
-
-```ebnf
-program = { top_level_decl } ;
-top_level_decl = node_decl | edge_decl | type_decl | context_decl
- | intent_decl | process_decl | query_decl
- | import_decl | export_decl ;
-
-node_decl = 'node' node_ref '::' type_expr '{' { node_member } '}' ;
-edge_decl = 'edge' node_ref edge_op target_spec
- [ '{' { edge_member } '}' ] ;
-type_decl = 'type' identifier [ type_params ] [ '=' type_expr ]
- [ '{' { field_decl } '}' ]
- [ 'where' '{' { constraint } '}' ] ;
-context_decl = 'context' node_ref '{' { context_member } '}' ;
-intent_decl = 'intent' node_ref '::' type_expr '{' trigger_clause
- do_clause { meta_annotation } '}' ;
-process_decl = 'process' node_ref '::' type_expr '{'
- { state_decl | transition_decl | meta_annotation } '}' ;
-query_decl = 'query' [ identifier ] '{' { match_clause }
- [ where_clause ] [ group_clause ] return_clause '}' ;
-
-edge_op = '->' | '<->' | '-[' identifier ']->' |
- '<-[' identifier ']-' | '-[' identifier ']-' ;
-
-meta_annotation = '~' meta_key meta_value ;
-meta_key = identifier [ ':' identifier ] ;
-meta_value = expression
- | expression '..' expression
- | expression '/' duration
- | '{' { meta_field } '}' ;
-
-path_pattern = step { edge_step step } ;
-edge_step = edge_op | '-[' edge_type repetition? ']->'
- | '<-[' edge_type repetition? ']-'
- | '-[' edge_type repetition? ']-' ;
-repetition = '*' int_literal
- | '*' int_literal '..' int_literal
- | '*' '..' int_literal
- | '*' ;
-```
-
----
-
-## Appendix A: Standard Library Types
-
-```kndl
-// kndl://std/core
-type Entity { id : String, name : String? }
-type Measurement { value : Float, unit : String }
-type Place { lat : Float?, lon : Float?, address : String? }
-type SemVer { major : Int, minor : Int, patch : Int }
-type Signature { alg : String, key : String, sig : Bytes }
-
-// kndl://std/units — re-exports Quantity with common dimensions
-type Temperature = Quantity<Θ>
-type Pressure = Quantity
-type Velocity = Quantity
-type Energy = Quantity
-type Power = Quantity
-type Frequency = Quantity
-type Mass = Quantity
-type Length = Quantity
-
-// kndl://std/agents
-type Action { }
-type ScheduledAction = Action & { schedule : String }
-type Alert { severity : "info" | "warn" | "critical", message : String }
-type Command { target : Entity, action : String }
-type Report { title : String, generated : DateTime }
-type Notification { channel : String, message : String }
-type WorkOrder { title : String, status : "open" | "in_progress" | "closed" }
-type Workflow { }
-
-// kndl://std/inference
-type InferenceRule { method : String, version : SemVer }
-type Attestation { issuer : String, claim : String, evidence : Bytes }
-```
-
----
-
-## Appendix B: Domain Profiles
-
-Each profile is an importable module that adds conventional types and
-meta-annotations without changing core semantics.
-
-### B.1 IoT / PropTech (`kndl://std/iot`)
-
-- Types: `Device`, `Sensor`, `Actuator`, `Gateway`, `Building`, `Floor`,
- `Room`, `Zone`.
-- Annotations: `iot:sampling_rate`, `iot:calibration`, `iot:last_seen`,
- `matter:cluster`, `brick:class`.
-- Source schemes: `sensor://`, `matter://`, `bacnet://`, `modbus://`,
- `knx://`, `zigbee://`.
-
-### B.2 FinTech (`kndl://std/fin`)
-
-- Types: `Account`, `Transaction`, `Instrument`, `Position`, `Quote`.
-- Constraint: `Transaction` MUST satisfy `sum(debits) == sum(credits)`
- (double-entry) with matching currencies per leg.
-- Annotations: `fin:jurisdiction`, `fin:mic` (market identifier code).
-
-### B.3 Healthcare (`kndl://std/fhir`)
-
-- Types: `Patient`, `Encounter`, `Observation`, `Condition`, `Medication`,
- `Allergy`, `Consent`.
-- Classifications: `~classification "PHI"`; `~consent` required on write.
-- Terminology: `Code<"SNOMED-CT">`, `Code<"ICD-10">`, `Code<"LOINC">`,
- `Code<"RxNorm">`.
-
-### B.4 Logistics (`kndl://std/logistics`)
-
-- Types: `Shipment`, `Package`, `Lot`, `Hub`, `Route`.
-- Processes: `shipment_sm` standard state machine.
-- Identifiers: `gtin:`, `sscc:`.
-
-### B.5 Robotics (`kndl://std/robotics`)
-
-- Types: `Robot`, `Joint`, `EndEffector`, `Trajectory`, `Obstacle`.
-- All spatial fields require `~frame`. TF tree mandatory.
-- `Pose ` as primary spatial type.
-
-### B.6 Smart Factory (`kndl://std/isa95`)
-
-- ISA-95 hierarchy: `Enterprise`, `Site`, `Area`, `WorkCenter`, `WorkUnit`.
-- Types: `Product`, `BOM`, `Operation`, `DowntimeEvent`, `QualityDefect`.
-- `BOM` uses reification (§ Appendix C) for n-ary composition.
-
-### B.7 Networking / Security (`kndl://std/net`)
-
-- Types: `Host`, `Interface`, `Link`, `Flow`, `Vulnerability`, `Indicator`.
-- Primitives: `IPv4`, `IPv6`, `MAC`, `CIDR`, `Port`.
-- STIX bridge under namespace `stix:`.
-
-### B.8 eCommerce (`kndl://std/ecom`)
-
-- Types: `Product`, `Variant`, `Price`, `Inventory`, `Cart`, `Order`.
-- `Inventory.quantity` pairs with `~decay` to model staleness.
-- Product names: `Localized`.
-
----
-
-## Appendix C: Reification Pattern for N-ary Relations
-
-Edges are binary. When a relation has more than two participants (e.g.
-"Patient received Drug at Dose via Route at Time"), reify the relation
-as a node:
-
-```kndl
-node @admin_4821 :: MedicationAdministration {
- patient -> @pat_001
- drug -> @rx_warfarin
- dose = 5 mg
- route = "oral"
- at = 2026-04-10T08:00Z
- ~source "fhir://hospital/MedicationAdministration/4821"
-}
-```
-
-This idiom keeps the graph binary-edged while supporting arbitrary arity
-and giving the relation its own identity, provenance, and temporal scope.
-
----
-
-## Appendix D: MIME Type
-
-```
-text/kndl — KNDL text format
-application/kndl+b — KNDL binary format
-```
-
-File extensions: `.kndl` (text), `.kndlb` (binary).
diff --git a/spec/grammar/kndl.ebnf b/spec/grammar/kndl.ebnf
deleted file mode 100644
index 5ff1a8b..0000000
--- a/spec/grammar/kndl.ebnf
+++ /dev/null
@@ -1,324 +0,0 @@
-(* ================================================================ *)
-(* KNDL — Knowledge Node Description Language *)
-(* EBNF Grammar v1.0.0 — derived from SPECIFICATION.md §2, §11, §13 *)
-(* Notation: UPPERCASE = lexical non-terminal, 'x' = literal token *)
-(* ================================================================ *)
-
-
-(* ── Program ──────────────────────────────────────────────────── *)
-
-program = { top_level_decl } ;
-
-top_level_decl = node_decl
- | edge_decl
- | type_decl
- | context_decl
- | intent_decl
- | process_decl
- | query_decl
- | import_decl
- | export_decl
- | COMMENT ;
-
-
-(* ── Nodes ────────────────────────────────────────────────────── *)
-
-node_decl = 'node' node_ref '::' type_expr '{' { node_member } '}' ;
-node_member = field_assign | inline_edge | meta_annotation ;
-field_assign = IDENTIFIER '=' expression ;
-inline_edge = IDENTIFIER '->' node_ref ;
-
-
-(* ── Edges ────────────────────────────────────────────────────── *)
-
-edge_decl = 'edge' node_ref edge_op target_spec
- [ '{' { edge_member } '}' ] ;
-edge_op = '->'
- | '<->'
- | '-[' IDENTIFIER ']->'
- | '<-[' IDENTIFIER ']-'
- | '-[' IDENTIFIER ']-' ;
-target_spec = node_ref
- | '[' node_ref { ',' node_ref } ']' ;
-edge_member = field_assign | meta_annotation ;
-
-
-(* ── Types ────────────────────────────────────────────────────── *)
-
-type_decl = 'type' IDENTIFIER [ type_params ]
- [ '=' type_expr ]
- [ '{' { field_decl } '}' ]
- [ 'where' '{' { constraint } '}' ] ;
-
-type_params = '<' type_param { ',' type_param } '>' ;
-type_param = IDENTIFIER [ '::' type_expr ] ;
-
-type_expr = IDENTIFIER [ '<' type_arg { ',' type_arg } '>' ]
- | type_expr '&' type_expr
- | type_expr '|' type_expr
- | STRING_LITERAL
- | type_expr '?'
- | '{' { field_decl } '}' ;
-type_arg = type_expr | node_ref | literal ;
-
-field_decl = IDENTIFIER ':' type_expr ;
-constraint = expression ;
-
-
-(* ── Contexts ─────────────────────────────────────────────────── *)
-
-context_decl = 'context' node_ref '{' { context_member } '}' ;
-context_member = meta_annotation
- | node_decl
- | edge_decl
- | intent_decl
- | process_decl
- | context_decl ;
-
-
-(* ── Intents ──────────────────────────────────────────────────── *)
-
-intent_decl = 'intent' node_ref '::' type_expr '{'
- trigger_clause
- do_clause
- { meta_annotation }
- '}' ;
-
-trigger_clause = 'trigger' '=' trigger_expr ;
-trigger_expr = query_decl
- | expression
- | 'cron' STRING_LITERAL ;
-
-do_clause = 'do' '{' { action } '}' ;
-action = 'emit' node_decl
- | 'emit' 'update' node_ref '{' { node_member } '}'
- | 'emit' 'delete' node_ref
- | 'goto' IDENTIFIER ;
-
-
-(* ── Processes ───────────────────────────────────────────────── *)
-
-process_decl = 'process' node_ref '::' type_expr '{'
- { process_member }
- '}' ;
-process_member = state_decl | transition_decl | meta_annotation ;
-
-state_decl = 'state' IDENTIFIER
- [ '{' { meta_annotation } '}' ] ;
-
-transition_decl = 'on' event_expr 'in' IDENTIFIER '->' IDENTIFIER
- [ 'where' expression ]
- [ 'do' '{' { action } '}' ]
- [ 'compensate' '{' { action } '}' ] ;
-
-event_expr = IDENTIFIER | query_decl ;
-
-
-(* ── Queries ──────────────────────────────────────────────────── *)
-
-query_decl = 'query' [ IDENTIFIER ] '{'
- { match_clause }
- [ where_clause ]
- [ group_clause ]
- return_clause
- '}' ;
-
-match_clause = [ 'optional' ] 'match' path_pattern ;
-
-path_pattern = [ VAR_BIND '=' ] step { edge_step step } ;
-step = VAR_BIND '::' type_expr
- | node_ref ;
-
-edge_step = edge_op
- | '-[' edge_type [ repetition ] ']->'
- | '<-[' edge_type [ repetition ] ']-'
- | '-[' edge_type [ repetition ] ']-' ;
-edge_type = IDENTIFIER | VAR_BIND ;
-repetition = '*' INT_LITERAL
- | '*' INT_LITERAL '..' INT_LITERAL
- | '*' '..' INT_LITERAL
- | '*' ;
-
-where_clause = 'where' expression ;
-group_clause = 'group' 'by' expression { ',' expression } ;
-
-return_clause = 'return' return_expr ;
-return_expr = expression [ 'with' 'edges' INT_LITERAL ]
- | map_literal
- | agg_field { ',' agg_field } ;
-agg_field = IDENTIFIER '=' agg_func '(' expression ')'
- | IDENTIFIER '=' expression ;
-agg_func = 'sum' | 'avg' | 'min' | 'max' | 'count' ;
-
-VAR_BIND = '?' IDENTIFIER ;
-
-
-(* ── Meta-annotations ─────────────────────────────────────────── *)
-
-meta_annotation = '~' meta_key meta_value ;
-meta_key = IDENTIFIER [ ':' IDENTIFIER ] ; (* ns:key namespace *)
-meta_value = expression
- | expression '..' expression (* temporal range *)
- | expression '/' DURATION_LITERAL (* decay rate *)
- | '{' { meta_field } '}' ; (* structured *)
-meta_field = IDENTIFIER '=' expression
- | IDENTIFIER ':' expression ;
-
-
-(* ── Temporal ranges ──────────────────────────────────────────── *)
-(* DATETIME '..' DATETIME | DATETIME '..' '*' *)
-(* '*' '..' DATETIME | '*' '..' '*' *)
-(* (expressed via meta_value range; '*' = 'now') *)
-
-
-(* ── Module system ────────────────────────────────────────────── *)
-
-import_decl = 'import' '{' IDENTIFIER { ',' IDENTIFIER } '}'
- 'from' STRING_LITERAL ;
-export_decl = 'export' ( node_decl | type_decl | context_decl
- | intent_decl | process_decl ) ;
-
-
-(* ── Expressions ──────────────────────────────────────────────── *)
-
-expression = literal
- | node_ref
- | VAR_BIND
- | access_expr
- | binary_expr
- | unary_expr
- | func_call
- | '(' expression ')'
- | array_literal
- | map_literal ;
-
-access_expr = expression '.' IDENTIFIER
- | expression '[' expression ']' ;
-
-binary_expr = expression binary_op expression ;
-unary_expr = unary_op expression ;
-
-array_literal = '[' [ expression { ',' expression } ] ']' ;
-map_literal = '#{' [ kv_pair { ',' kv_pair } ] '}' ; (* note the '#' *)
-kv_pair = expression ':' expression ;
-
-func_call = IDENTIFIER '(' [ expression { ',' expression } ] ')' ;
-
-binary_op = '*' | '/' | '%' (* precedence 3 *)
- | '+' | '-' (* precedence 4 *)
- | '..' (* precedence 5 *)
- | '>' | '<' | '>=' | '<=' (* precedence 6 *)
- | '==' | '!=' (* precedence 7 *)
- | 'in' | 'overlaps' | 'within' | 'matches' (* precedence 8 *)
- | '&&' | 'and' (* precedence 9 *)
- | '||' | 'or' ; (* precedence 10 *)
-
-unary_op = 'not' | '-' ;
-
-
-(* ── Node references ──────────────────────────────────────────── *)
-
-node_ref = '@' IDENTIFIER { '.' IDENTIFIER } ;
-
-
-(* ── Literals (composite) ─────────────────────────────────────── *)
-
-literal = INT_LITERAL
- | FLOAT_LITERAL
- | DECIMAL_LITERAL
- | STRING_LITERAL
- | BOOL_LITERAL
- | NULL_LITERAL
- | DATETIME_LITERAL
- | DURATION_LITERAL
- | QUANTITY_LITERAL
- | MONEY_LITERAL
- | BYTES_LITERAL
- | VECTOR_LITERAL
- | UUID_LITERAL ;
-
-
-(* ── Lexical rules ────────────────────────────────────────────── *)
-
-(* Identifiers — §2.4 *)
-IDENTIFIER = LETTER { LETTER | DIGIT | '_' } ;
-LETTER = 'a'..'z' | 'A'..'Z' ;
-DIGIT = '0'..'9' ;
-HEX_DIGIT = DIGIT | 'a'..'f' | 'A'..'F' ;
-BIN_DIGIT = '0' | '1' ;
-
-(* Integer literals — §2.8.1 *)
-INT_LITERAL = [ '-' ] DIGIT { DIGIT }
- | '0x' HEX_DIGIT { HEX_DIGIT }
- | '0b' BIN_DIGIT { BIN_DIGIT } ;
-
-(* Float literals — §2.8.2 *)
-FLOAT_LITERAL = [ '-' ] DIGIT { DIGIT } '.' DIGIT { DIGIT } [ EXPONENT ] ;
-EXPONENT = ( 'e' | 'E' ) [ '+' | '-' ] DIGIT { DIGIT } ;
-
-(* Decimal literals — §2.8.3 *)
-DECIMAL_LITERAL = [ '-' ] DIGIT { DIGIT } '.' DIGIT { DIGIT } 'd' ;
-
-(* String literals — §2.8.4 *)
-STRING_LITERAL = '"' { STRING_CHAR | ESCAPE } '"'
- | '"""' { any Unicode char } '"""' ;
-STRING_CHAR = (* any Unicode char except '"' and '\' *) ;
-ESCAPE = '\' ( '"' | '\' | '/' | 'n' | 'r' | 't' | 'u' HEX4 ) ;
-HEX4 = HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT ;
-
-(* Boolean & null — §2.8.5–2.8.6 *)
-BOOL_LITERAL = 'true' | 'false' ;
-NULL_LITERAL = 'null' ;
-
-(* Duration literals — §2.8.7 *)
-DURATION_LITERAL = DIGIT { DIGIT } DURATION_UNIT ;
-DURATION_UNIT = 'ns' | 'us' | 'ms' | 's' | 'm' | 'h' | 'd' | 'w'
- | 'mo' | 'y' ;
-
-(* Datetime literals — §2.8.8 *)
-DATETIME_LITERAL = DATE [ 'T' TIME [ TIMEZONE ] ] ;
-DATE = YEAR '-' MONTH '-' DAY
- | YEAR '-Q' QUARTER
- | YEAR '-W' WEEK ;
-TIME = HOUR ':' MINUTE [ ':' SECOND [ '.' FRACTION ] ] ;
-TIMEZONE = 'Z' | ( '+' | '-' ) HOUR ':' MINUTE ;
-YEAR = DIGIT DIGIT DIGIT DIGIT ;
-MONTH = DIGIT DIGIT ;
-DAY = DIGIT DIGIT ;
-QUARTER = '1' | '2' | '3' | '4' ;
-
-(* Quantity literals — §2.8.9 *)
-QUANTITY_LITERAL = ( INT_LITERAL | FLOAT_LITERAL | DECIMAL_LITERAL )
- [ ' ' ] unit_expr ;
-unit_expr = unit_atom { ( '*' | '/' | '^' INT_LITERAL ) unit_atom } ;
-unit_atom = SI_UNIT | IMPERIAL_UNIT | DATA_UNIT | IDENTIFIER ;
-SI_UNIT = '°C' | '°F' | 'K'
- | 'm' | 'cm' | 'mm' | 'km'
- | 'kg' | 'g' | 'mg'
- | 's' | 'ms' | 'min' | 'hr'
- | 'A' | 'V' | 'W' | 'Wh' | 'kWh' | 'J'
- | 'Pa' | 'kPa' | 'bar'
- | 'mol' | 'cd' | 'lm' | 'lx'
- | 'Hz' | 'kHz' | 'MHz' | 'GHz' ;
-IMPERIAL_UNIT = 'ft' | 'in' | 'lb' ;
-DATA_UNIT = 'B' | 'KB' | 'MB' | 'GB' | 'TB'
- | 'bps' | 'kbps' | 'Mbps' | 'Gbps' ;
-
-(* Money literals — §2.8.10 *)
-MONEY_LITERAL = ( DECIMAL_LITERAL | INT_LITERAL ) [ ' ' ] ISO4217 ;
-ISO4217 = LETTER LETTER LETTER ; (* validated semantically *)
-
-(* Bytes literals — §2.8.11 *)
-BYTES_LITERAL = 'b"' { BASE64_CHAR } '"' ;
-BASE64_CHAR = LETTER | DIGIT | '+' | '/' | '=' ;
-
-(* Vector literals — §2.8.12 *)
-VECTOR_LITERAL = 'v[' FLOAT_LITERAL { ',' FLOAT_LITERAL } ']' ;
-
-(* UUID literals — §3.1 *)
-UUID_LITERAL = 'u"' HEX_DIGIT { HEX_DIGIT | '-' } '"' ;
-
-(* Comments — §2.3 *)
-COMMENT = LINE_COMMENT | BLOCK_COMMENT ;
-LINE_COMMENT = '//' { (* any char except newline *) } NEWLINE ;
-BLOCK_COMMENT = '/*' { (* any char, nesting allowed *) } '*/' ;
diff --git a/v2.md b/v2.md
new file mode 100644
index 0000000..081105a
--- /dev/null
+++ b/v2.md
@@ -0,0 +1,902 @@
+# KNDL v2 — Pivot Plan
+
+> **Status:** Plan, not implementation. Captured 2026-04-26.
+> **Owner:** Gleb (artdaw).
+> **Scope:** Replace the v1 Python DSL with a TypeScript-first JSON-LD memory protocol that mounts cleanly into Anthropic Memory, ships an MCP server + CLI + Skill, and persists to SQLite by default with Supabase / DuckDB / filesystem as alternatives.
+
+---
+
+## 1. The mental model
+
+Three layers, each with one responsibility:
+
+```
+Anthropic Memory = WHERE filesystem, persistence, permissions, audit
+KNDL = WHAT the format of files Claude writes
+MCP / CLI / Skill = HOW query, CRUD, decay computation, provenance walks
+```
+
+KNDL is no longer a language, an in-memory graph, or a parser. It is a **JSON-LD vocabulary for time-aware, source-aware, contradiction-aware facts**. Everything else — the runtime, the MCP server, the CLI, the Skill, the visualizer — is *operations on facts*.
+
+A fourth layer makes the picture fully connected:
+
+```
+Memory Store sync = BRIDGE pulls Anthropic-cloud Memory Stores into the
+ local FactStore so external clients (Claude
+ Desktop, Goose, Cursor, Windsurf) can read
+ what a Managed Agent wrote — and (opt-in) push
+ local writes back up
+```
+
+The bridge means there is **one logical memory** even when Claude lives on
+both sides of the network: managed agents in Anthropic's cloud and stand-alone
+clients on the user's machine. KNDL is the wire format on both sides; sync
+keeps them coherent. See §7.
+
+This is the model already prototyped in `kndl-memory-pack/`. v2 promotes that prototype to mainline and retires the Python DSL.
+
+---
+
+## 2. The goal of v2
+
+> *"KNDL becomes the recommended format for Memory when you need provenance and time awareness."*
+
+Concretely, a developer who has just enabled Anthropic Memory on a managed agent — or who is running Claude Desktop / Goose / Cursor / Windsurf locally — should reach for KNDL when their use case is one of:
+
+- the agent must remember things **across sessions** and not contradict itself later
+- the agent must explain **where a claim came from** (chain of custody)
+- the agent must answer **"what did we believe on date X"** (bitemporal)
+- some facts go stale fast (sensor data, IOCs, prices, status) and others don't (identity, birth date)
+- two sources disagree and the agent has to **rank, not average**
+
+If the use case isn't any of those, the agent doesn't need KNDL. We are explicit about this in the docs — KNDL earns its weight only where epistemics matter.
+
+---
+
+## 3. What we keep, what we drop, what we merge
+
+### From `kndl-memory-pack/` (TypeScript prototype) — promote to mainline
+
+| Asset | Decision | Notes |
+|---|---|---|
+| `kndl-memory-mcp/src/core.ts` (382 LOC) | **Keep as the canonical implementation.** | Pure logic. Decay math, query, contradictions, supersession, provenance walk. |
+| `kndl-memory-mcp/src/cli.ts` (202 LOC) | **Keep.** | The `kndl` binary the Skill calls via bash. |
+| `kndl-memory-mcp/src/server.ts` (219 LOC) | **Keep, extend.** | Six MCP tools: `assert_fact`, `query_facts`, `contradictions`, `supersede_fact`, `as_of`, `provenance_chain`. Add: `list_facts`, `show_fact`, plus subscribe/notification hooks (see §6). Add streamable-http transport. |
+| `kndl-memory/SKILL.md` (217 LOC) | **Keep.** | Drop-in for any `/memory/skills/` directory. The convention pack Claude follows. |
+| `kndl-memory/context/v1.jsonld` (48 LOC) | **Keep.** | Vocabulary; serve from `https://kndl.artdaw.com/context/v1.jsonld`. |
+| `kndl-memory/eval/questions.json` | **Keep + extend.** | The 8-question eval is a quality bar. Extend it with one harness per domain (IoT, clinical, threat-intel, legal, scientific, AI evals). |
+| `kndl-memory/examples/loan-decision/` | **Keep.** | The first example dataset. |
+
+### From the current Python implementation — merge or retire
+
+| Asset | Decision | Notes |
+|---|---|---|
+| `packages/python/src/kndl/lexer.py`, `parser.py`, `compiler.py`, `serializer.py`, `ast_nodes.py` | **Retire (delete in v2.0).** | Custom `.kndl` DSL. The user-facing wire format is JSON-LD; nobody needs the parser. |
+| `packages/python/src/kndl/graph.py` (KNDLGraph, GraphNode, GraphEdge, GraphIntent, KNDLMeta) | **Retire (delete in v2.0).** | Replaced by the Fact shape from `core.ts`. |
+| `packages/python/src/kndl/storage.py` + `backends/{sqlite,postgres}_backend.py` | **Lift the backend ideas, port to TS.** | Keep the Protocol pattern: `FactStore` interface with `Fs`, `Sqlite`, `DuckDb`, `Supabase`, `Postgres` implementations. (See §5.) |
+| `packages/python/tests/` | **Retire.** | Replace with a single TS test runner (`vitest` or `node --test`). |
+| `packages/mcp-server/` (Python FastMCP) | **Retire.** | Replaced by `kndl-memory-mcp` (TypeScript). |
+| `spec/SPECIFICATION.md` (1,168 LOC) | **Retire (delete).** | Replaced by the JSON-LD context + `SKILL.md` + a new short `PROTOCOL.md` (~150 LOC). |
+| `spec/grammar/kndl.ebnf` | **Retire (delete).** | No DSL = no grammar. |
+| `website/public/examples/*.kndl` (7 files) | **Convert to JSON-LD facts.** | The 6 domain examples (IoT, threat-intel, clinical, legal, scientific, AI evals, personal) become fact bundles in `kndl-memory/examples//`. |
+| `website/src/pages/SpecPage.tsx`, `SpecFullPage.tsx`, `WorkflowPage.tsx` | **Retire.** | Replaced by `/protocol` (the schema), `/skill` (rendered SKILL.md), `/eval` (the question harness with results). |
+| `website/src/pages/ExplorerPage.tsx` (force-directed viewer) | **Rebuild for facts.** | Subjects + objects → nodes, predicates → edges. Reads from a fact-bundle URL. |
+| `website/src/pages/McpPage.tsx` | **Rewrite for `kndl-memory-mcp`.** | New install instructions: `npm install -g kndl-memory` (or via `npx kndl-memory-mcp`). Configs for Claude Desktop, Goose, Cursor, Windsurf, LM Studio. |
+| `Devils_Advocate.md` (root) | **Keep, extend.** | Add a "v2 retro" section explaining which weak points were finally addressed. |
+
+### From the v1.5 work (the partial pivot earlier this session)
+
+The earlier session attempted a JSON-LD pivot in Python (`packages/python/src/kndl/jsonld.py`). The linter rolled some of it back. **Discard those Python fragments.** The TypeScript `core.ts` already has a cleaner version of the same idea.
+
+---
+
+## 4. Repository layout after v2
+
+```
+KNDL/
+├── packages/
+│ └── kndl-memory/ ← the npm package, single source of truth
+│ ├── src/
+│ │ ├── core.ts shared store: decay, query, contradictions, supersession
+│ │ ├── cli.ts `kndl` binary
+│ │ ├── server.ts `kndl-memory-mcp` binary
+│ │ ├── stores/
+│ │ │ ├── fs.ts FsFactStore — filesystem (Anthropic Memory mount)
+│ │ │ ├── sqlite.ts SqliteFactStore — single-file persistent (DEFAULT)
+│ │ │ ├── duckdb.ts DuckDbFactStore — analytical workloads
+│ │ │ ├── supabase.ts SupabaseFactStore — multi-tenant cloud
+│ │ │ └── index.ts makeStore(config) factory
+│ │ ├── notify.ts filesystem watcher + change-feed bridge (see §6)
+│ │ └── types.ts Fact, FactInput, QueryOptions, etc.
+│ ├── tests/ node --test or vitest
+│ ├── package.json
+│ └── tsconfig.json
+│
+├── skills/
+│ └── kndl-memory/ ← drop-in Claude Skill bundle
+│ ├── SKILL.md the convention pack Claude follows
+│ ├── context/v1.jsonld vendored copy of the JSON-LD context
+│ ├── examples/ one fact bundle per domain
+│ │ ├── loan-decision/
+│ │ ├── iot-sensor/
+│ │ ├── threat-intel/
+│ │ ├── clinical/
+│ │ ├── legal-ediscovery/
+│ │ ├── scientific-lab/
+│ │ └── ai-evals/
+│ └── eval/
+│ └── questions.json extended quality bar across domains
+│
+├── schema/
+│ ├── kndl-context.jsonld canonical, served at kndl.artdaw.com/context/v1.jsonld
+│ └── fact.schema.json JSON Schema for Fact shape (validation)
+│
+├── website/ docs site (React + Vite, unchanged stack)
+│ └── src/pages/
+│ ├── LandingPage.tsx refactored: Anthropic-Memory + KNDL pitch
+│ ├── ProtocolPage.tsx replaces SpecPage — the JSON-LD context, field-by-field
+│ ├── SkillPage.tsx replaces WorkflowPage — rendered SKILL.md
+│ ├── ExplorerPage.tsx rebuilt for facts (subject/predicate/object → graph)
+│ ├── ExamplesPage.tsx lists example bundles, links to Explorer
+│ ├── McpPage.tsx install + configure across clients
+│ └── EvalPage.tsx new — show eval results KNDL vs vanilla
+│
+├── PROTOCOL.md ~150-line spec, no grammar
+├── README.md repo-level intro
+├── CLAUDE.md repo working memory
+├── Devils_Advocate.md kept; v2-retro section added
+└── v2.md this file
+```
+
+The Python tree disappears. The `packages/python/` and `packages/mcp-server/` directories are removed in the v2.0 commit. (We keep the deletion in git history so anyone who needs the v1 implementation can `git checkout v1.x`.)
+
+---
+
+## 5. The Fact shape and storage layers
+
+### Fact shape — exactly the kndl-memory-pack version
+
+```jsonc
+{
+ "@context": "https://kndl.artdaw.com/context/v1.jsonld",
+ "@id": "fact:customer-9281-creditscore-20260425T235231Z-cd2efb00",
+ "@type": "Fact",
+
+ "statement": "Customer 9281 has a credit score of 720",
+ "subject": "customer:9281", // optional triple form for queries
+ "predicate": "creditScore",
+ "object": 720,
+
+ "confidence": 0.95, // [0, 1]
+ "decay": "0.5/30d", // string: rate/window — easy for LLMs
+
+ "source": "https://api.experian.com/9281",
+ "validFrom": "2026-04-23T10:00:00Z",
+ "recordedAt": "2026-04-25T23:52:31Z",
+
+ // optional
+ "validUntil": "2027-04-23T00:00:00Z",
+ "observedAt": "2026-04-25T23:52:31Z",
+ "supersedes": "fact:customer-9281-creditscore-20260301T000000Z-aaa",
+ "derivedFrom": ["fact:..."],
+ "negated": false,
+ "classification": "PII",
+ "consent": "consent:loan-eval-2026",
+ "retention": "P7Y",
+ "tenant": "acme-bank"
+}
+```
+
+**Why one shape ("Fact") and not Node + Edge + Intent?**
+
+The old Python data model had three primitives. In practice they were all the same: a typed assertion with provenance and confidence. A Fact with `predicate: "type"` and `object: "Person"` is what used to be a Node. A Fact with `predicate: "reports_to"` and `object: ""` is what used to be an Edge. An Intent is just a Fact of `@type: "Action"`. **Collapsing to one primitive is a 30% spec reduction with zero capability loss.**
+
+The website's force-directed Explorer renders a fact-bundle by treating subjects + objects (when `object` is an `@id`) as nodes and predicates as edges. The visualization story is unchanged.
+
+### Storage layers — `FactStore` is an interface, four implementations
+
+```ts
+export interface FactStore {
+ assertFact(input: FactInput, supersedesId?: string): Promise<{id: string; fact: Fact}>;
+ supersedeFact(oldId: string, input: FactInput): Promise<{id: string; supersedes: string; fact: Fact}>;
+ query(opts: QueryOptions): Promise;
+ contradictions(opts: {subject?: string; predicate?: string}): Promise<...>;
+ provenanceChain(rootId: string, maxDepth?: number): Promise<...>;
+ list(subject?: string): Promise;
+ show(id: string): Promise;
+ subscribe(filter: SubscribeFilter, handler: (event) => void): () => void;
+}
+```
+
+| Implementation | Status | When to use |
+|---|---|---|
+| **`FsFactStore`** | already in core.ts | Anthropic Memory mount; tiny / portable; one JSON-LD file per fact. |
+| **`SqliteFactStore`** | new | **Default for stand-alone use.** Single file. Real indexed columns for confidence/source/recordedAt/subject/predicate. WAL mode. JSON-LD round-trip on demand. |
+| **`DuckDbFactStore`** | new | Analytical workloads. Fast `AVG(effective_confidence) GROUP BY predicate` and bulk imports. |
+| **`SupabaseFactStore`** | new | Multi-tenant cloud. Realtime change feed for free (Postgres LISTEN/NOTIFY). RLS for tenant isolation. |
+
+The factory chooses by `KNDL_STORAGE` env var (or `--store` CLI flag, or `storage:` MCP config option):
+
+```
+KNDL_STORAGE=fs:./memory # filesystem, default
+KNDL_STORAGE=sqlite:./kndl.db # single-file SQLite, persistent (RECOMMENDED)
+KNDL_STORAGE=duckdb:./kndl.duckdb # analytical
+KNDL_STORAGE=supabase://?key=... # multi-tenant cloud
+```
+
+**Filesystem-canonical mode vs DB-canonical mode.** Both modes round-trip cleanly via JSON-LD export/import. We don't pick one — we let the user pick:
+
+- **FS-canonical (Anthropic Memory):** filesystem is source of truth. SQLite is an optional read cache that auto-rebuilds. This is what the user gets when running inside an Anthropic Managed Agent.
+- **DB-canonical (stand-alone):** SQLite/DuckDB/Supabase is source of truth. JSON-LD export is on demand. This is the default for Claude Desktop / Goose / Cursor when there is no surrounding Anthropic Memory.
+
+---
+
+## 6. Notifications
+
+Subscriptions are part of v2 from day one (the user asked for them last session, and the architecture supports them naturally).
+
+The MCP server exposes:
+
+- `subscribe(filter)` tool — register interest in a subject/predicate/tenant. Returns a subscription id.
+- `unsubscribe(id)` tool.
+- `kndl://fact/{id}` resource — live snapshot, MCP clients re-read on `notifications/resources/updated`.
+
+How the MCP server detects changes:
+
+- **`FsFactStore`**: `chokidar` watches the facts dir, fires on new files.
+- **`SqliteFactStore`**: SQLite `update_hook` for in-process writes; ATTACH+poll mtime for cross-process.
+- **`DuckDbFactStore`**: poll a small change-counter table.
+- **`SupabaseFactStore`**: subscribe to Postgres `LISTEN/NOTIFY` channel via the Supabase realtime client.
+
+All four implementations adapt their native change feed into the same internal `EventEmitter` shape; the server's broadcast loop is store-agnostic.
+
+For multi-client broadcast (Claude Desktop *and* Goose seeing each other's writes), the answer is the same as v1's last fix: run `kndl-memory-mcp --http` once and point all clients at it. The server keeps a session registry and broadcasts to every active session. This also avoids the SQLite single-writer lock contention.
+
+---
+
+## 7. Remote Memory Store sync
+
+> **Why:** an Anthropic Managed Agent has a Memory Store living in Anthropic's
+> cloud (`https://platform.claude.com/docs/en/api/go/beta/memory_stores`). Local
+> clients — Claude Desktop, Goose, Cursor, Windsurf — don't have direct access
+> to it. The MCP server bridges the gap: it pulls remote Memory Store contents
+> down into the local FactStore so every connected client can read what the
+> Anthropic-side agent has written, and (opt-in) pushes local writes back up so
+> the next API session sees what the desktop session learned.
+
+### What it solves
+
+Today an Anthropic Managed Agent writes a fact to its Memory Store via the
+beta API. Tomorrow the user opens Claude Desktop and asks "what did we
+discuss about customer 9281?" — Claude Desktop has no idea, because Memory
+Stores aren't exposed to MCP clients. With remote sync turned on, the local
+KNDL store mirrors the relevant Memory Store(s); every MCP client sees the
+same memory regardless of which agent populated it.
+
+This is the *cross-runtime memory* feature the v2 mental model implies. KNDL
+is the WHAT; sync is the wire that makes it shared across WHEREs.
+
+### Architecture
+
+A new module, `packages/kndl-memory/src/remote/anthropic.ts`, wraps the
+Anthropic Memory Stores API:
+
+```
+┌─────────────────────────┐ ┌──────────────────────────┐
+│ Anthropic Memory Store │ ←──── │ RemoteSync (this module)│
+│ (cloud, beta API) │ ────→ │ │
+└─────────────────────────┘ │ pull / push / watch │
+ │ conflict policy │
+ └────────────┬─────────────┘
+ │
+ ▼
+ ┌──────────────────────────┐
+ │ Local FactStore │
+ │ (fs / sqlite / duckdb / │
+ │ supabase) │
+ └──────────────────────────┘
+```
+
+Sync is **store-agnostic**: it writes to whatever `FactStore` the local server
+is configured with, using the existing `assertFact` / `supersedeFact` API.
+
+### Translation: Memory Store items ↔ KNDL Facts
+
+Anthropic Memory Stores hold free-form text/markdown items, not structured
+facts. v2.0 ships the simplest mapping that doesn't lose information:
+
+**Pull (Anthropic → local):** each Memory Store item becomes one Fact:
+
+```jsonc
+{
+ "@id": "fact:claude-store--",
+ "@type": "Fact",
+ "statement": "",
+ "confidence": 0.85, // configurable default
+ "source": "claude-memory:///",
+ "validFrom": "",
+ "observedAt": "",
+ "recordedAt": "",
+ "tags": ["from-anthropic-memory", ""]
+}
+```
+
+The `source` URI is canonical and unique per Memory Store item, so re-pulling
+the same item is idempotent (becomes a no-op or, if the item changed,
+auto-supersedes the previous fact via the existing supersession path).
+
+**Push (local → Anthropic):** opt-in. Facts whose `tags` include
+`push-to-anthropic` (or a configurable label) are serialized to a compact
+text representation and POSTed to the Memory Store. Default: opt-out, because
+most local writes are scratch and shouldn't pollute the agent's long-term
+memory.
+
+**LLM-based extraction** (turn one Memory Store item into N structured facts
+with subject/predicate/object) is **explicitly out of scope for v2.0**. Listed
+as a v2.1 candidate alongside calibration tooling.
+
+### MCP tools
+
+Three new tools, off by default, gated on `ANTHROPIC_API_KEY`:
+
+| Tool | What it does |
+|---|---|
+| `sync_memory_store` | One-shot pull (or pull+push) of a specific store. Args: `store_id`, `direction: "pull"|"push"|"both"` (default `pull`), `since: ISO datetime` (default last-sync watermark). Returns counts + any contradictions detected. |
+| `list_memory_stores` | Lists configured remote stores and their last-sync timestamp. |
+| `watch_memory_store` | Registers a periodic background pull on the server. Args: `store_id`, `interval_seconds` (default 300). Returns a watcher id; `unwatch_memory_store(id)` cancels. |
+
+Manual ops (CLI) for the two operations the agent shouldn't do:
+
+| CLI | What it does |
+|---|---|
+| `kndl remote add --provider anthropic --store-id --label personal` | Register a remote store in `~/.kndl/remotes.json`. |
+| `kndl remote pull [--since ]` | Manual pull. |
+| `kndl remote push ` | Manual push. |
+| `kndl remote ls` / `kndl remote rm ` | Maintenance. |
+
+Watchers run on the MCP server process. When a watcher pulls a new item, it
+writes through the FactStore — which fires the existing notification machinery
+from §6, so every connected client gets `notifications/resources/updated` for
+the new fact's URI. **The "remote agent wrote, my desktop saw it instantly"
+flow is just remote-sync + local notifications composed.**
+
+### Configuration
+
+```bash
+# environment
+export ANTHROPIC_API_KEY=sk-ant-...
+export KNDL_REMOTE_STORES="anthropic:store_abc123:personal,anthropic:store_xyz789:work"
+
+# or ~/.kndl/remotes.json
+{
+ "remotes": [
+ { "label": "personal", "provider": "anthropic", "store_id": "store_abc123",
+ "default_confidence": 0.85, "pull_interval_seconds": 300, "push": false },
+ { "label": "work", "provider": "anthropic", "store_id": "store_xyz789",
+ "default_confidence": 0.9, "pull_interval_seconds": 60, "push": true,
+ "push_tag": "share-with-agent" }
+ ]
+}
+```
+
+### Conflict resolution
+
+The same machinery KNDL already has:
+
+- **Supersession** if the remote item id already exists locally with a
+ different `statement`: pull writes a new fact with `supersedes` pointing at
+ the older one. History preserved.
+- **Contradictions** if a local fact and a remote fact disagree on
+ `subject`/`predicate` but neither supersedes the other: the contradiction
+ walker surfaces both. The agent reads `kndl_contradictions` and decides.
+- **Tenant isolation** is enforced on the local side: facts pulled from a
+ remote store labeled `personal` are tagged with `tenant: personal` and
+ filtered out of `work` queries. No cross-tenant accidental bleed.
+
+### Why this isn't just "another FactStore backend"
+
+Tempting to add `kndl_storage://anthropic-memory:store_id` as a 5th backend
+and treat it like SQLite. Reasons not to:
+
+1. **Latency.** A pull-on-every-query against a remote API is unusable —
+ sub-100ms local query becomes 500ms-5s remote.
+2. **Rate limits.** The Memory Stores API has quotas; serving every
+ `kndl_query_nodes` from it would burn them.
+3. **Multi-client broadcast** wants a single canonical local copy, not N
+ clients each pulling independently.
+4. **Decay computation** wants `effective_confidence` to be cheap; pulling
+ raw items every time defeats the in-memory math.
+
+Sync-to-local is the right shape: cloud is the *upstream source of truth*
+for facts the Anthropic agent wrote; local is the *operational store*
+everyone reads from.
+
+---
+
+## 8. The Skill — what changes, what doesn't
+
+`SKILL.md` is already well-written for v2. Net additions:
+
+- A **trust-threshold table** that ties `effective_confidence` to user-facing language ("usable", "use with hedge", "stale; do not state as fact").
+- A **citation rule**: every recall claim references the `@id` of the fact it relied on.
+- A **classification gate**: `PHI` / `PII` / `PCI` facts are filtered by default; the agent must explicitly request `allow_phi: true` and the user must have signed off.
+- An **Anthropic Memory addendum**: the Skill auto-detects whether it's running inside Anthropic Memory (look for `/memory/` mount) and adjusts pathing.
+
+---
+
+## 9. The website (post-pivot)
+
+| Old route | New route | Status |
+|---|---|---|
+| `/` LandingPage | `/` LandingPage | rewrite — repositioned around "Memory format for Anthropic agents + everywhere else" |
+| `/spec` SpecPage | **gone** | redirect → `/protocol` |
+| `/spec/full` SpecFullPage | **gone** | redirect → `/protocol` |
+| `/workflow` WorkflowPage | `/skill` SkillPage | rendered `SKILL.md` with anchors |
+| `/explorer` ExplorerPage | `/explorer` ExplorerPage | rebuilt for fact bundles |
+| `/mcp` McpPage | `/mcp` McpPage | rewrite for `kndl-memory-mcp` |
+| — | `/protocol` | new — JSON-LD context, Fact JSON Schema, field-by-field reference |
+| — | `/examples` | new — gallery of fact bundles |
+| — | `/eval` | new — eval scoreboard (KNDL vs vanilla) per question, per domain |
+
+All pages link upward to `/protocol` for "how is this fact-shape defined?" and downward to `/explorer` to inspect fact bundles visually.
+
+The old Python-flavored `llms.txt` and `llms-full.txt` are rewritten.
+
+---
+
+## 10. Migration path for v1 users
+
+We will publish a one-shot migration:
+
+```bash
+npx kndl-memory migrate --from sqlite:///./kndl-v1.db --to ./memory
+```
+
+This reads a v1 SQLite database (the old Python schema), maps each `Node`/`Edge`/`Intent` to a `Fact`, and writes JSON-LD files into `./memory/facts/`. Edges become facts where `predicate` = the edge_type and `object` = ``. Intents become facts of `@type: "Action"`.
+
+We do **not** ship a TS port of the v1 KNDL parser. Anyone with `.kndl` text files runs a (one-time) Python `kndl serialize` → JSON dump → `kndl-memory migrate` step.
+
+---
+
+## 11. Implementation phases
+
+Each phase ends in a green build and a tag. Aim for ~1 day per phase.
+
+### Phase 1 — Promote and consolidate
+
+- Move `kndl-memory-pack/kndl-memory-mcp/` → `packages/kndl-memory/`.
+- Move `kndl-memory-pack/kndl-memory/` → `skills/kndl-memory/`.
+- Delete `kndl-memory-pack/` (the wrapper directory).
+- Delete `packages/python/`, `packages/mcp-server/`, `spec/`.
+- `packages/kndl-memory/package.json` → published as `@kndl/memory` on npm with two binaries.
+- CI workflow updated: drop the python jobs, add a `node-test` job.
+- Tag `v2.0.0-alpha.1`.
+
+### Phase 2 — Storage layer refactor
+
+- Define `FactStore` interface in `packages/kndl-memory/src/types.ts`.
+- Move current filesystem code into `src/stores/fs.ts`.
+- Add `src/stores/sqlite.ts` (`better-sqlite3`).
+- Add `src/stores/duckdb.ts` (`@duckdb/node-api`).
+- Add `src/stores/supabase.ts` (`@supabase/supabase-js`).
+- `makeStore({type, …})` factory dispatching on `KNDL_STORAGE`.
+- Tests for each backend on the same fact corpus (the loan-decision dataset).
+- Tag `v2.0.0-alpha.2`.
+
+### Phase 3 — Subscriptions + HTTP transport
+
+- `src/notify.ts` with `chokidar` watcher for FS, `update_hook` for SQLite, polling for DuckDB, `pg_notify` for Supabase.
+- Add `subscribe`/`unsubscribe` MCP tools.
+- Add streamable-http transport to `server.ts`.
+- Add `kndl://fact/{id}` resource with `notifications/resources/updated` broadcast on writes.
+- Tag `v2.0.0-alpha.3`.
+
+### Phase 4 — Domain example bundles
+
+- For each of the seven domains we already have v1 examples for, write a fact bundle under `skills/kndl-memory/examples//`.
+- Each bundle has 5–10 facts that exercise: confidence, decay, supersession, contradiction, derivedFrom, classification.
+- The IoT bundle and the personal-memory bundle become the README examples.
+- Tag `v2.0.0-alpha.4`.
+
+### Phase 5 — Anthropic Memory Store sync
+
+- `packages/kndl-memory/src/remote/anthropic.ts` — thin wrapper over the
+ Memory Stores beta API: `listItems`, `getItem`, `createItem`, watermark
+ tracking, exponential backoff on 429.
+- `packages/kndl-memory/src/remote/sync.ts` — pull/push driver, idempotent
+ on Memory-Store-item-id, supersession on changed content, conflict detection
+ routes through the existing `contradictions` walker.
+- MCP tools: `sync_memory_store`, `list_memory_stores`, `watch_memory_store`,
+ `unwatch_memory_store`. Gated on `ANTHROPIC_API_KEY`.
+- CLI: `kndl remote add | pull | push | ls | rm`.
+- Background watcher loop in the server (one async task per registered store)
+ that fires through the §6 notification machinery on every new fact pulled.
+- Loopback test harness: a fake Memory Store that the test runner pre-populates,
+ so we don't burn real API quota in CI.
+- Tag `v2.0.0-alpha.5`.
+
+### Phase 6 — Eval scoreboard
+
+- Extend `eval/questions.json` from 8 to ~30 questions (8 cross-domain + 3–4 per domain).
+- Write a runner that drives Claude with and without the MCP server and scores binary.
+- Add a *cross-runtime* eval question: "facts written via the Anthropic API
+ Memory Store appear in a Claude Desktop session within N seconds." Tests the
+ Phase 5 sync end-to-end.
+- Publish results on `/eval`.
+- If KNDL doesn't beat vanilla on ≥70% of eval questions, **stop and fix the protocol before shipping v2.0**.
+- Tag `v2.0.0-beta.1`.
+
+### Phase 7 — Website rewrite
+
+- New `LandingPage` around the three-layer mental model (Memory / KNDL / Skill+CLI+MCP).
+- New `ProtocolPage` rendering the JSON-LD context + JSON Schema + field reference.
+- New `SkillPage` rendering `SKILL.md`.
+- New `ExamplesPage` listing fact bundles.
+- Rewrite `ExplorerPage` to render fact bundles.
+- Rewrite `McpPage` for the new TypeScript install path.
+- `EvalPage` with the scoreboard.
+- Sitemap, robots.txt, llms.txt all rewritten.
+- Tag `v2.0.0-rc.1`.
+
+### Phase 8 — Migration tool + release
+
+- `kndl migrate --from sqlite:///./kndl-v1.db --to ./memory` for v1 users.
+- Publish `@kndl/memory@2.0.0` to npm.
+- Publish the schema files to `kndl.artdaw.com/context/v1.jsonld` and `kndl.artdaw.com/schema/fact.schema.json`.
+- README + Devils_Advocate.md "v2 retro" section.
+- Tag `v2.0.0`.
+
+Total estimated effort: 8 working days for a single developer focused on this. Each phase is independently revertible.
+
+---
+
+## 12. Open questions and risks
+
+**Q1. Does Anthropic Memory actually allow agents to mount a filesystem path and treat it as `/memory/`, or is the API more abstract?**
+
+The Skill assumes a `/memory/` mount. We need to verify against the current Anthropic Memory product surface before locking in. If it's API-shaped instead of filesystem-shaped, the `FsFactStore` becomes a `MemoryApiFactStore`. Same Fact shape, different glue.
+
+**Action:** read the current `anthropic-sdk-python` and `@anthropic-ai/sdk` docs for the Memory product before Phase 1 starts.
+
+**Q2. Do we really kill the Python implementation entirely?**
+
+There are users on `pip install kndl` (small but non-zero). Two options:
+
+- **(a) Hard-cut at v2.0.** Tell them v1 is archived, point at the migration tool, move on. Cleanest but loses adoption momentum.
+- **(b) Keep a thin Python *client* — read-only.** A `pip install kndl` package that can `query`, `as_of`, `provenance` against a running `kndl-memory-mcp` server. ~200 LOC, no parser, no DSL. Lets Python developers use KNDL without us maintaining a second implementation.
+
+Recommend **(b)**. Keep Python as a read-only API client for one minor version, then evaluate whether anyone uses it.
+
+**Q3. SQLite as default makes filesystem look secondary — does that conflict with the Anthropic Memory pitch?**
+
+No. The mental model is honest: if you have Anthropic Memory, use `KNDL_STORAGE=fs:/memory`. If you don't, use `KNDL_STORAGE=sqlite:./kndl.db`. The Skill, CLI, and MCP behave identically. The choice is about *where the bytes live*, not *what they are*.
+
+The website's main pitch can be: **"KNDL works two ways — drop into Anthropic Memory as JSON-LD files, or run stand-alone with SQLite. Same protocol, same tools, same skill."**
+
+**Q4. The 6 MCP tools (assert/query/contradictions/supersede/as_of/provenance) vs the v1 12 CRUD-shaped tools — are we losing power?**
+
+We lose `add_node`/`add_edge`/`add_intent`/`update_node`/`remove_node`/`load_jsonld`/`export_jsonld`/`reset`/`graph_stats`. Of those:
+
+- `add_node`/`edge`/`intent` collapse into `assert_fact`.
+- `update_node` becomes `supersede_fact`.
+- `remove_node` becomes assert with `negated: true` *or* a separate `forget_fact`.
+- `load_jsonld` / `export_jsonld` become CLI ops (`kndl import`, `kndl export`), not MCP tools — agents shouldn't bulk-load.
+- `reset` becomes a CLI op (`kndl wipe --confirm`). Too dangerous for an MCP tool.
+- `graph_stats` becomes a resource (`kndl://graph/summary`), not a tool.
+
+Net: 6 tools instead of 12, no capability loss for the agent's actual reasoning workflow.
+
+**Q5. How does the Explorer visualize a flat fact bundle?**
+
+Group by subject. Each unique `@id` value (subject *or* object when object is an `@id`) becomes a node. Each fact with both `subject` and `object: <@id>` becomes an edge from subject to object, labeled with `predicate`. Facts where `object` isn't an `@id` are rendered as fields on the subject node (with `predicate: value` rows).
+
+This means the v1 force-directed layout code transfers cleanly. We just swap the loader.
+
+**Q6. What's the calibration story?**
+
+Still unaddressed. v2 doesn't fix LLM confidence calibration — but the protocol surface (`confidence` + `effective_confidence`) is right; calibration tooling can ship in v2.1 against an unchanged wire format. Add as a sequel item, not a v2.0 blocker.
+
+**Q7. Memory Stores beta API stability + cost.**
+
+The Anthropic Memory Stores API is in beta. Two real risks:
+
+- **Surface drift.** The API may change before v2.0 ships. Mitigation: keep
+ `remote/anthropic.ts` thin and isolated; pin the SDK version; gate the
+ feature behind an explicit env var so users on a broken SDK can disable it
+ without rebuilding.
+- **API quota / cost.** Watcher loops poll. With a 60-second interval per
+ store, a single user with two stores is 2,880 list-calls/day. We need to
+ document cost back-of-envelope and use `If-None-Match` / `since` watermarks
+ to make the calls cheap (only return new items). If the API doesn't expose
+ a watermark, default the interval much higher (1h+) and rely on manual
+ `sync_memory_store` for fresh-pull use cases.
+
+**Action:** verify the Memory Stores API actually has a "list items since
+watermark" call before Phase 5. If not, the polling cost story is bad and we
+ship Phase 5 as pull-on-demand only (no `watch_memory_store` tool).
+
+**Q8. Push direction — should we even ship it in v2.0?**
+
+Push (local → Anthropic) has subtler edge cases than pull:
+
+- Which local facts qualify? (Tag-based opt-in is the proposal.)
+- How to compress a Fact down to the text Anthropic Memory expects without
+ losing the structured fields?
+- What to do if push fails partway — retry queue vs. drop?
+- Authorization: should pushing PII facts up to Anthropic be possible at all?
+
+Recommend **pull-only in v2.0**. Push moves to v2.1 once we've watched real
+users use pull and we know what they actually want pushed back up.
+
+---
+
+## 13. Success criteria for v2.0
+
+We ship v2.0 when **all** of these are true:
+
+1. `npm install -g @kndl/memory` works on macOS, Linux, and a recent Windows + Node 18+.
+2. `kndl add` / `query` / `contradictions` / `supersede` / `provenance` round-trip on `fs`, `sqlite`, `duckdb`, and `supabase` stores.
+3. `kndl-memory-mcp` connects to: Claude Desktop (stdio), Goose (stdio), Cursor (stdio), Windsurf (stdio), `mcp-remote` bridged HTTP. Each tested manually with a fact insert + recall.
+4. The Skill bundle drops cleanly into a managed-agent skills directory and activates on the trigger phrases.
+5. The eval scoreboard shows KNDL ≥ 70% binary-correct vs vanilla baseline across the cross-domain question set.
+6. **A fact written via the Anthropic API to a Memory Store appears in a Claude Desktop session** (with the local KNDL MCP connected and `watch_memory_store` registered) **within 60 seconds**, with full source attribution. This is the cross-runtime promise; it must work.
+7. The website's `/`, `/protocol`, `/skill`, `/examples`, `/explorer`, `/mcp`, `/eval` are live and match the package's behavior.
+8. The migration tool reads at least one real v1 SQLite database and produces a usable JSON-LD fact bundle.
+9. CI is green on the new `node-test` job, including the Memory-Store loopback harness (no real API calls, but full pull/supersede/contradiction round-trip).
+
+If criteria 5 fails, that's the kill switch described in the strategic analysis (Appendix A) — the architecture doesn't earn its complexity, and we either rework the protocol (e.g. better calibration, cheaper queries) or shelve the project. **No quiet shipping with a failing eval.**
+
+If criterion 6 fails, sync ships disabled by default and Phase 5's tools are documented as experimental until the next release.
+
+---
+
+## 14. What this plan does NOT do
+
+- **Solve LLM confidence calibration.** The contract preserves the value; calibration tooling is a v2.1 item.
+- **Replace vector search.** Pair KNDL with a vector index for semantic recall; KNDL is for structured, time-aware recall. Documented as a "what KNDL is not" in the README.
+- **Ship cryptographic provenance.** `signature` is round-trippable but not validated. v2.1 candidate.
+- **Build a native UI.** The website has a viewer; there is no desktop app and won't be one.
+- **Solve cross-tenant federation.** Multi-tenant via Supabase yes; cross-tenant joins no. Out of scope.
+- **LLM-extract structured facts from Memory Store free text.** Memory Store items become one Fact each (the wrap-as-fact mapping in §7); we don't run an extraction pass that turns "Customer 9281 has a credit score of 720, recorded by Experian" into a structured triple. v2.1 candidate alongside calibration.
+- **Bidirectional sync conflict resolution beyond supersession.** Push lands in v2.1 once we've observed real pull usage. v2.0 is pull-only.
+- **Sync from non-Anthropic memory providers.** OpenAI's response state, Google's Vertex Memory, etc. The architecture admits other providers (`provider: "anthropic"` is a tagged union), but only the Anthropic adapter ships in v2.0.
+
+---
+
+## 15. Branding decision (carry-over from earlier)
+
+KNDL stays as the acronym. The expansion was renamed last session from "Knowledge Node Description Language" to **"Knowledge Node Data Link"** — that decision still stands and is now load-bearing: it's a *data* format / *link* between systems, not a *description language*. The name maps directly to the new mental model.
+
+Domain stays at `kndl.artdaw.com`. The JSON-LD context lives at `kndl.artdaw.com/context/v1.jsonld` (already aligned in the memory-pack prototype).
+
+---
+
+## End of plan
+
+All 8 phases shipped. See Appendix B for the v2 retro.
+
+---
+
+---
+
+# Appendix A — Strategic Analysis (Devil's Advocate)
+
+> **Captured:** 2026-04-25
+> **Context:** Strategic analysis written before the v2 pivot. Many of the
+> weak points listed below were what motivated the JSON-LD / "drop the
+> language" pivot that ships in v2. Annotations marked **[Addressed in v2]**
+> note where the project has already moved on; the rest stand.
+
+## The strategic problem in one sentence
+
+KNDL is a **1,168-line spec, ~5,000 lines of Python parser/compiler, a custom DSL, and a binary format** built to solve a problem (giving an LLM a memory) that **most users solve with a 50-line vector DB call or a 500-line JSON-blob memory MCP**. The cost/value ratio of the format itself is the main risk.
+
+> **[Addressed in v2]** The DSL, parser, lexer, compiler, serializer, AST,
+> language tests, and EBNF grammar were all deleted. The wire format is now
+> JSON-LD against a published context. The 1,168-line spec is gone; the
+> JSON Schema and JSON-LD context replace it (~150 lines combined).
+
+---
+
+## Weak points (specific, not generic)
+
+### 1. The DSL is mostly invisible to actual usage
+
+Open `server.py` — the MCP tools take JSON dicts (`fields`, `meta`, `confidence`). When Claude Desktop "uses" KNDL, it never emits or reads `.kndl` text; it calls `kndl_add_node({...})`. The 666-line lexer, 1,075-line parser, 479-line compiler, and 200-line serializer exist for a code path (`kndl_parse`) that almost no real session will use. That's ~2,400 lines of liability surface for a minority feature.
+
+> **[Addressed in v2]** All ~2,400 lines deleted. The MCP tools are still
+> structured kwargs; the bulk-load path is `kndl_load_jsonld` against a
+> JSON document.
+
+### 2. Confidence scores are theatre unless they're calibrated
+
+LLMs do not produce calibrated probabilities. "0.95 confidence" from Claude is a verbalized hedge, not a Brier-scored estimate. The downstream consequence — `query_nodes(min_confidence=0.8)` — is a filter on numbers that don't mean what they look like they mean. If a user trusts these thresholds, they're trusting a vibe. The literature on LLM calibration (Kadavath et al., Tian et al.) is unkind.
+
+> **[Open]** The contract preserves the value, but calibration tooling is
+> not yet shipped. This is the highest-leverage open critique.
+
+### 3. The exponential decay formula is arbitrary
+
+`confidence × decay_rate^(elapsed/duration)` is operationally simple but epistemically lazy. Real "this fact may be stale" reasoning is a Bayesian update against new evidence, not a fixed half-life. For a sensor reading, exponential decay is OK; for "Alice is a senior engineer," it's nonsense (the right model is "no decay until contradicted"). One formula doesn't fit both, but the spec applies it uniformly.
+
+> **[Open]** Decay is still per-fact via the `decay` string (`"0.5/30d"`).
+> Users who shouldn't apply decay simply omit it; the contract is
+> permissive. But the formula itself is unchanged.
+
+### 4. No semantic interop = every graph is an island
+
+RDF/OWL/JSON-LD won the semantic-data war in part because anyone's `foaf:Person` matches anyone else's `foaf:Person`. KNDL's `Person` is local to the file. Two agents using KNDL to "share knowledge" still need a manual mapping pass. The "agent ecosystem" pitch dies on that.
+
+> **[Addressed in v2]** The JSON-LD context aligns provenance fields with
+> W3C PROV-O (`source` → `prov:wasAttributedTo`, `recorded` →
+> `prov:generatedAtTime`, `supersedes` → `prov:invalidates`,
+> `derived_from` → `prov:wasDerivedFrom`). User-defined types
+> (`Person`, `Indicator`, etc.) are still local to the document, but the
+> framework provenance vocabulary is now interoperable with the wider
+> semantic-web ecosystem.
+
+### 5. Knowledge + behavior in one DSL serves neither well
+
+Intents (`when X, do Y`) and processes (state machines) are real things, but they're rules-engine and BPMN territory respectively. Drools, CLIPS, XState, BPMN have decades of refinement around evaluation order, conflict resolution, compensation, durability. KNDL gestures at all of this in syntax with no execution model. Users who actually need rules will outgrow it in a week; users who don't won't use those blocks at all.
+
+> **[Partially addressed in v2]** Processes (state machines) were dropped
+> with the rest of the language layer. Intents remain as a structured
+> data type (`@type: "Action"`) — they're stored, but the agent (not KNDL)
+> is responsible for firing them. This is more honest about the boundary.
+
+### 6. Storage is a thin demo
+
+SQLite with three JSON-blob columns means: no per-field indexing, no SQL-level filtering, single-writer lock, no concurrent agents, no replication, no migrations. The spec does not mention CRDT semantics, last-write-wins, or vector clocks. Two agents writing concurrently corrupt each other silently. That's fine for a personal Claude Desktop memory; it dies the moment two agents share a graph.
+
+> **[Addressed in v2]** Four storage backends ship: `fs:` (filesystem),
+> `sqlite:` (WAL mode, indexed columns, cross-process polling),
+> `duckdb:` (columnar), `supabase:` (Postgres + RLS). Concurrent writers
+> are handled via the HTTP transport (single server process) or the
+> SQLite change-log polling loop.
+
+### 7. Brand and discoverability
+
+"KNDL" pronounced "Kindle" guarantees Amazon SEO collisions forever. Search "kindle knowledge graph" — you get e-readers. This is recoverable but real.
+
+> **[Open]** Renamed to "Knowledge Node Data Link" in v2 (was
+> "Knowledge Node Description Language"). The acronym still collides
+> with Kindle in search, but the new expansion is at least
+> mechanically honest about what v2 is.
+
+### 8. Spec scope is huge and probably premature
+
+1,168 lines of spec, parameterized types, dimensional analysis (`Quantity`), uncertainty distributions, processes, imports from `kndl://std/units` — before there's a single deployed user. Worse Is Better is an underrated essay; KNDL has the opposite disease.
+
+> **[Addressed in v2]** Spec deleted. Replaced by a JSON-LD context (~50
+> lines) and a JSON Schema (~120 lines). Parameterized types, dimensional
+> analysis, processes, and imports are gone. Uncertainty distributions
+> remain as a JSON sub-shape. Net surface reduction: ~95%.
+
+---
+
+## Competitors (honest comparisons)
+
+| What | Where it wins | Where KNDL could win |
+|---|---|---|
+| **Anthropic's `mcp-server-memory`** | Already shipped. Simple entity/relation/observation model. ~500 lines. | KNDL has confidence/decay/provenance; the official server has none. **Realistic head-to-head.** |
+| **Graphiti / Zep** | Production agent memory. Bi-temporal graph, real-time updates, Python SDK, MCP adapter. | Graphiti has no confidence scalar, no uncertainty distributions, no in-format intents. |
+| **Mem0** | Most popular OSS "memory layer for LLMs." Vector + structured. | Mem0 is RAG-shaped; structure is shallow. KNDL has real graph semantics. |
+| **RDF + JSON-LD + schema.org** | Decades of tooling, ontology reuse, browser/SEO/training-corpus support. PROV-O for provenance. | KNDL is much friendlier to write by hand. RDF is famously hostile. |
+| **Neo4j / KuzuDB / MemGraph** | Real graph DB. Cypher. Indexes, ACID, path queries. | KNDL is a *format*, not a DB — apples vs oranges. |
+| **XTDB / Datomic** | Bi-temporal as a first-class database property. Datalog. Production-grade. | Closed-source (Datomic) or smaller community (XTDB). KNDL is friendlier as a serialization format. |
+| **Vector DB (Pinecone/Weaviate/Qdrant/Chroma)** | What 90% of production "agent memory" actually is. | KNDL has structure RAG can't represent. |
+
+---
+
+## Alternative paths (with outcomes)
+
+**A. Drop the DSL, keep the protocol.**
+> **[Done in v2]** This is exactly the pivot that shipped.
+
+**B. Become a JSON-LD profile.**
+> **[Done in v2]** The published JSON-LD context is the new wire format.
+
+**C. Pivot to "calibrated memory MCP."** Add LLM-side calibration tooling (reliability diagrams, Brier loss against user feedback, recalibration curves).
+> **[Open]** Still the strongest unfunded opportunity post-pivot.
+
+**D. Embrace KuzuDB or DuckDB underneath.**
+> **[Done in v2]** Four storage backends now ship.
+
+**E. Narrow the use case to IoT/sensor telemetry.**
+> **[Rejected in v2]** Multi-domain positioning with 8 worked example bundles.
+
+---
+
+## What is genuinely unique (and survives scrutiny)
+
+1. **A typed memory contract designed around the failure modes of LLMs**: confidence (because LLMs hallucinate), provenance (because we need attribution), decay (because LLM-asserted facts go stale faster than human-asserted ones), and uncertainty distributions (because LLMs are stochastic). No other agent-memory project frames the *contract* this way.
+
+2. **Aleatoric vs epistemic separation** (`confidence` vs `uncertainty`) — a real distinction from probabilistic-ML that almost nobody else surfaces in a serialization format.
+
+3. **In-format trigger-action intents alongside data** — co-locating "X is true" with "if X is true, do Y" is unusual.
+
+4. **Single-file portable graph with provenance baked in** — you can share a `.fact.json` file and the recipient knows where every fact came from, how confident the asserter was, and when it expires.
+
+---
+
+## Blunt recommendation (written pre-pivot)
+
+The strongest version of KNDL ditches the language and keeps the contract.
+
+The MCP server is the actual product; the language is identity theater. If you keep the language, narrow the spec by ~70% and put that effort into calibration tooling and a real query engine. If you pivot, become a JSON-LD profile or a Graphiti competitor with confidence as the wedge — not a new DSL competing with RDF.
+
+> **[Outcome]** v2 ships the JSON-LD-profile pivot. Calibration tooling
+> remains the open frontier.
+
+---
+
+## Use cases beyond IoT
+
+The pattern that matters: **a domain where every fact has a source, a confidence, a "valid when," and may go stale or be superseded.** Wherever those four are load-bearing, KNDL's contract earns its weight.
+
+### Stronger fits than IoT
+
+**Clinical / healthcare** — `confidence` for differential weights, `negated:true` for "no history of X", bitemporal for EHR audit trails, `classification` for PHI lifecycle. Highest fit, slowest sales cycle.
+
+**Threat intelligence / OSINT** — IOCs have a known half-life; that's literally what `decay` was built for. Provenance (which feed/analyst), `supersedes` for false-positive retractions, `negated` for "no observed C2 traffic."
+
+**Legal / e-discovery** — bitemporal is *the* feature law cares about: "what did the company know, and when did it know it?" `supersedes` for amended depositions, chain-of-custody via `source`.
+
+**Scientific data / lab notebooks** — `uncertainty: { _type: "gaussian", mean, std }` is what scientific measurement needs and what JSON/RDF can't express natively.
+
+**AI safety / evals** — a red-team finding is: a model output (provenance), at a confidence (the eval grader's score), valid for a model version (`validFrom`/`validUntil`), with a classification level. Findings get superseded as models improve.
+
+### Honest ranking by wedge potential
+
+1. **Threat intelligence** — best feature fit, fastest validation cycle, natural buyers.
+2. **AI safety evals** — meta-relevant to the distribution channel; deep-pocketed buyers.
+3. **Clinical knowledge** — highest potential, slowest sales cycle, regulatory landmines.
+4. **IoT** — original instinct, still valid but more crowded.
+5. **Legal / financial bitemporal** — strongest *technical* fit but enterprise sales is brutal for a one-person shop.
+
+The pattern across the strong fits: they're all domains where *getting the epistemics wrong is a real problem someone is paid to prevent.* That's the customer profile worth hunting.
+
+---
+
+---
+
+# Appendix B — v2 Retro
+
+> **Written:** 2026-04-26, after v2.0.0 tagged.
+
+## What the plan said vs. what shipped
+
+| Phase | Planned | Shipped |
+|---|---|---|
+| 1. Promote | Move kndl-memory-pack → mainline, delete Python | ✓ Done |
+| 2. Storage layer | FactStore interface + fs/sqlite/duckdb/supabase | ✓ Done |
+| 3. Subscriptions + HTTP | chokidar, SQLite polling, StreamableHTTP transport | ✓ Done |
+| 4. Domain examples | 5–10 facts per domain × 7 domains | ✓ 42 facts across 8 domains |
+| 5. Memory Store sync | Pull-only, fake client for CI | ✓ Done (push deferred to v2.1 per plan) |
+| 6. Eval scoreboard | 33 questions, Claude-as-judge runner | ✓ Done (eval/results.json needs ANTHROPIC_API_KEY to populate) |
+| 7. Website rewrite | 7 new pages, 3 deleted | ✓ Done |
+| 8. Migration + release | kndl migrate, schema files, README | ✓ Done |
+
+## Resolved vs open from the v1 weak points
+
+**Fully resolved:**
+- DSL/parser/compiler/lexer → deleted. Wire format is JSON-LD.
+- 1,168-line spec → JSON Schema (60 lines) + JSON-LD context (48 lines).
+- Storage is a thin demo → four backends, WAL mode, indexed columns.
+- No domain examples → 42 facts across 8 domains, 7 archetypes each.
+- Concurrency story → SQLite cross-process polling + HTTP transport.
+- 406/400 HTTP issue → `StreamableHTTPServerTransport` (TypeScript SDK).
+
+**Partially resolved:**
+- Calibration → `confidence` field is right; tooling is v2.1.
+- `watch_memory_store` → stubbed pending watermark API verification (Q7).
+- Push to Anthropic Memory → explicitly deferred to v2.1 (Q8).
+
+**Still open:**
+- Decay formula assumed, not calibrated to domain data.
+- Brand collision with Kindle — acronym unchanged.
+- Eval scoreboard not yet run — runner needs API credits + human review.
+
+## The one-line verdict
+
+The contract (confidence + provenance + bitemporal + decay + supersession) survives intact. The implementation went from 10,892 lines of Python DSL machinery to 684 lines of TypeScript core logic that does more. That's the right trade.
diff --git a/website/index.html b/website/index.html
index 4eb0181..d55793d 100644
--- a/website/index.html
+++ b/website/index.html
@@ -6,37 +6,36 @@
- KNDL — Knowledge Node Description Language
-
-
+ KNDL — The Format Anthropic Memory Was Waiting For
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
-
-
-
@@ -46,7 +45,7 @@
rel="stylesheet"
/>
-
+