From 797ff2ecc26416fc09cdd1c95a3e2b773fab8bb0 Mon Sep 17 00:00:00 2001 From: Gleb Galkin Date: Sun, 26 Apr 2026 11:59:45 +0200 Subject: [PATCH 01/14] =?UTF-8?q?feat(v2):=20Phase=201=20=E2=80=94=20promo?= =?UTF-8?q?te=20kndl-memory-pack=20to=20mainline,=20retire=20Python=20DSL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move kndl-memory-pack/kndl-memory-mcp/ → packages/kndl-memory/ (@kndl/memory 2.0.0-alpha.1) - Move kndl-memory-pack/kndl-memory/ → skills/kndl-memory/ (drop-in Claude Skill bundle) - Delete packages/python/, packages/mcp-server/ (Python DSL retired) - Delete spec/ (SPECIFICATION.md and kndl.ebnf — no DSL = no grammar) - Delete kndl-memory-pack/ wrapper directory - CI: drop python + mcp-server jobs, add node-test job - Build verified: dist/ generates clean, both binaries (kndl, kndl-memory-mcp) present --- .github/workflows/kndl-workflow.yml | 33 +- Devils_Advocate.md | 256 ++ packages/kndl-memory/package-lock.json | 2634 +++++++++++++++++ packages/kndl-memory/package.json | 41 + packages/kndl-memory/src/cli.ts | 202 ++ packages/kndl-memory/src/core.ts | 382 +++ packages/kndl-memory/src/server.ts | 219 ++ packages/kndl-memory/tsconfig.json | 14 + packages/mcp-server/.python-version | 1 - packages/mcp-server/README.md | 125 - packages/mcp-server/pyproject.toml | 48 - packages/mcp-server/src/kndl_mcp/__init__.py | 3 - packages/mcp-server/src/kndl_mcp/__main__.py | 3 - packages/mcp-server/src/kndl_mcp/_meta.py | 19 - packages/mcp-server/src/kndl_mcp/server.py | 555 ---- packages/mcp-server/tests/__init__.py | 0 packages/mcp-server/tests/test_tools.py | 627 ---- packages/mcp-server/uv.lock | 1075 ------- packages/python/.python-version | 1 - packages/python/README.md | 236 -- packages/python/pyproject.toml | 50 - packages/python/src/kndl/__init__.py | 64 - packages/python/src/kndl/ast_nodes.py | 341 --- packages/python/src/kndl/backends/__init__.py | 1 - .../src/kndl/backends/postgres_backend.py | 231 -- .../src/kndl/backends/sqlite_backend.py | 167 -- packages/python/src/kndl/compiler.py | 479 --- packages/python/src/kndl/graph.py | 474 --- packages/python/src/kndl/lexer.py | 666 ----- packages/python/src/kndl/parser.py | 1075 ------- packages/python/src/kndl/py.typed | 0 packages/python/src/kndl/serializer.py | 195 -- packages/python/src/kndl/storage.py | 85 - packages/python/tests/test_advanced_types.py | 376 --- packages/python/tests/test_kndl.py | 461 --- packages/python/tests/test_kndl_extended.py | 642 ---- packages/python/tests/test_processes.py | 665 ----- packages/python/tests/test_storage.py | 313 -- packages/python/uv.lock | 399 --- skills/kndl-memory/README.md | 180 ++ skills/kndl-memory/SKILL.md | 217 ++ skills/kndl-memory/context/v1.jsonld | 48 + skills/kndl-memory/eval/questions.json | 65 + .../examples/loan-decision-vanilla.json | 104 + ...income-20260425t235231z-09c3bb41.fact.json | 14 + ...tscore-20260425t235231z-cd2efb00.fact.json | 14 + ...tscore-20260425t235248z-3bf684bd.fact.json | 14 + ...tscore-20260425t235249z-b6c88774.fact.json | 15 + ...tevent-20260425t235231z-e4b6d53c.fact.json | 14 + ...status-20260425t235231z-39b12725.fact.json | 14 + ...stress-20260425t235232z-b64190fb.fact.json | 14 + spec/SPECIFICATION.md | 1168 -------- spec/grammar/kndl.ebnf | 324 -- v2.md | 676 +++++ 54 files changed, 5147 insertions(+), 10892 deletions(-) create mode 100644 Devils_Advocate.md create mode 100644 packages/kndl-memory/package-lock.json create mode 100644 packages/kndl-memory/package.json create mode 100644 packages/kndl-memory/src/cli.ts create mode 100644 packages/kndl-memory/src/core.ts create mode 100644 packages/kndl-memory/src/server.ts create mode 100644 packages/kndl-memory/tsconfig.json delete mode 100644 packages/mcp-server/.python-version delete mode 100644 packages/mcp-server/README.md delete mode 100644 packages/mcp-server/pyproject.toml delete mode 100644 packages/mcp-server/src/kndl_mcp/__init__.py delete mode 100644 packages/mcp-server/src/kndl_mcp/__main__.py delete mode 100644 packages/mcp-server/src/kndl_mcp/_meta.py delete mode 100644 packages/mcp-server/src/kndl_mcp/server.py delete mode 100644 packages/mcp-server/tests/__init__.py delete mode 100644 packages/mcp-server/tests/test_tools.py delete mode 100644 packages/mcp-server/uv.lock delete mode 100644 packages/python/.python-version delete mode 100644 packages/python/README.md delete mode 100644 packages/python/pyproject.toml delete mode 100644 packages/python/src/kndl/__init__.py delete mode 100644 packages/python/src/kndl/ast_nodes.py delete mode 100644 packages/python/src/kndl/backends/__init__.py delete mode 100644 packages/python/src/kndl/backends/postgres_backend.py delete mode 100644 packages/python/src/kndl/backends/sqlite_backend.py delete mode 100644 packages/python/src/kndl/compiler.py delete mode 100644 packages/python/src/kndl/graph.py delete mode 100644 packages/python/src/kndl/lexer.py delete mode 100644 packages/python/src/kndl/parser.py delete mode 100644 packages/python/src/kndl/py.typed delete mode 100644 packages/python/src/kndl/serializer.py delete mode 100644 packages/python/src/kndl/storage.py delete mode 100644 packages/python/tests/test_advanced_types.py delete mode 100644 packages/python/tests/test_kndl.py delete mode 100644 packages/python/tests/test_kndl_extended.py delete mode 100644 packages/python/tests/test_processes.py delete mode 100644 packages/python/tests/test_storage.py delete mode 100644 packages/python/uv.lock create mode 100644 skills/kndl-memory/README.md create mode 100644 skills/kndl-memory/SKILL.md create mode 100644 skills/kndl-memory/context/v1.jsonld create mode 100644 skills/kndl-memory/eval/questions.json create mode 100644 skills/kndl-memory/examples/loan-decision-vanilla.json create mode 100644 skills/kndl-memory/examples/loan-decision/fact-customer-9281-annualincome-20260425t235231z-09c3bb41.fact.json create mode 100644 skills/kndl-memory/examples/loan-decision/fact-customer-9281-creditscore-20260425t235231z-cd2efb00.fact.json create mode 100644 skills/kndl-memory/examples/loan-decision/fact-customer-9281-creditscore-20260425t235248z-3bf684bd.fact.json create mode 100644 skills/kndl-memory/examples/loan-decision/fact-customer-9281-creditscore-20260425t235249z-b6c88774.fact.json create mode 100644 skills/kndl-memory/examples/loan-decision/fact-customer-9281-defaultevent-20260425t235231z-e4b6d53c.fact.json create mode 100644 skills/kndl-memory/examples/loan-decision/fact-customer-9281-employmentstatus-20260425t235231z-39b12725.fact.json create mode 100644 skills/kndl-memory/examples/loan-decision/fact-customer-9281-financialdistress-20260425t235232z-b64190fb.fact.json delete mode 100644 spec/SPECIFICATION.md delete mode 100644 spec/grammar/kndl.ebnf create mode 100644 v2.md 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/Devils_Advocate.md b/Devils_Advocate.md new file mode 100644 index 0000000..f51278e --- /dev/null +++ b/Devils_Advocate.md @@ -0,0 +1,256 @@ +# KNDL — Devil's Advocate Analysis + +> **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 `decay_rate` / `decay_duration_seconds`. +> 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 — 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. + +> **[Partially addressed in v2]** KuzuDB is now the recommended backend +> via `kuzu:///path` URLs. Edges are real graph-DB relationships, indexed +> node IDs, and Cypher-shaped queries become possible. But concurrent +> writers across multiple MCP clients still hit a lock — a fix the +> backend now surfaces explicitly with a helpful error pointing at the +> HTTP-mode workaround. + +### 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 (named, honest comparisons) + +| What | Where it wins | Where KNDL could win | +|---|---|---| +| **Anthropic's `mcp-server-memory`** (official reference) | Already shipped. Already what people install. Simple entity/relation/observation model. ~500 lines. | KNDL has confidence/decay/provenance; the official server has none. **This is the realistic head-to-head.** | +| **Graphiti / Zep** | Production agent memory. Bi-temporal graph, real-time updates, custom entities, Python SDK, MCP adapter. Already deployed at scale. **The most direct technical competitor.** | Graphiti has no confidence scalar, no uncertainty distributions, no in-format intents. | +| **Mem0** | Most popular OSS "memory layer for LLMs." Python lib + REST API. 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/LLM-training-corpus support. PROV-O for provenance. RDF-star for confidence. | KNDL is much friendlier to write by hand. RDF is famously hostile. | +| **Neo4j / KuzuDB / MemGraph** | Real graph DB. Cypher (or GQL when ratified). Indexes, ACID, path queries. KuzuDB embeds like SQLite. | KNDL is a *format*, not a DB — apples vs oranges, but this is what most "graph for agents" projects actually pick. | +| **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 + metadata (Pinecone/Weaviate/Qdrant/Chroma)** | What 90% of production "agent memory" actually is. Semantic recall + filters. | KNDL has structure RAG can't represent. | + +--- + +## Alternative paths (concrete pivots) + +**A. Drop the DSL, keep the protocol.** The unique value isn't the syntax — it's the *contract* (every fact carries confidence + provenance + decay + uncertainty). Repackage as a JSON Schema + Python types + MCP server. Lose 4,000 lines of parser/compiler. The website becomes a doc for the schema, not a language tour. + +> **[Done in v2]** This is exactly the pivot that shipped. + +**B. Become a JSON-LD profile.** Define KNDL as a JSON-LD context with a confidence/provenance/decay vocabulary. Inherit the entire semantic-web ecosystem for free. Your `Person` becomes `schema:Person`. This kills the island problem instantly. + +> **[Done in v2]** The published JSON-LD context is the new wire format. + +**C. Pivot to "calibrated memory MCP."** Instead of competing on syntax, compete on a real story for confidence calibration: keep the wire format simple, but add LLM-side calibration tooling (reliability diagrams, Brier loss against user feedback, recalibration curves). That's a defensible technical moat that nobody else has. + +> **[Open]** Still the strongest unfunded opportunity post-pivot. + +**D. Embrace KuzuDB or DuckDB underneath.** Replace the JSON-blob SQLite with a real embedded graph or columnar store. Get path queries, indexes, and concurrency essentially free. You keep the MCP interface; you stop maintaining a query engine you can't optimize. + +> **[Done in v2]** KuzuDB is now the recommended backend. + +**E. Narrow the use case to IoT/sensor telemetry.** Sensor data is the *one* domain where exponential decay, dimensional types, and uncertainty distributions all genuinely matter and where there isn't a dominant "agent memory" incumbent. Stop pitching KNDL as general agent memory; pitch it as "RDF for IoT agents." Smaller market but defensible. + +> **[Rejected in v2]** Followed up by the "use cases beyond IoT" +> analysis (next section). The current positioning is multi-domain +> (six worked examples), with IoT as one of seven, not the focus. + +--- + +## What is genuinely unique (and would survive scrutiny) + +Strip everything and what remains is: + +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. Graphiti is bi-temporal but not confidence-aware; Mem0 is RAG-shaped; the official memory MCP has none of this. + +2. **Aleatoric vs epistemic separation** (`~confidence` vs `~uncertainty`) — that's a real distinction from probabilistic-ML and almost nobody else surfaces it in a serialization format. For sensor and scientific use cases this is unique. + +3. **In-format trigger-action intents alongside data** — co-locating "X is true" with "if X is true, do Y" is unusual. It's also a footgun (see weak point 5), but the *idea* is unique. + +4. **Single-file portable graph with provenance baked in** — you can email a `.kndl` file and the recipient knows where every fact came from, how confident the asserter was, and when it expires. RDF/Turtle does this too, but KNDL is far friendlier to read. + +> **[v2 update]** Replace ".kndl file" with ".jsonld document" — same +> property, more interop. + +--- + +## Blunt recommendation (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% (drop processes, drop dimensional types, drop the binary format, drop imports) 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. + +The one thing not to do: keep building horizontally (more language features, more profiles, more spec surface) before any user has answered "do confidence scores from an LLM actually help me." That question is more important than any feature on your roadmap, and you can answer it this week with a 50-line experiment instead of a 1,168-line spec. + +> **[Outcome]** v2 ships the JSON-LD-profile pivot. Calibration tooling +> remains the open frontier. + +--- + +# Use cases beyond IoT + +The pattern that matters is: **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 + +### 1. Clinical / healthcare knowledge graphs + +This is the highest-fit, highest-value match. Map the spec to the domain: + +| KNDL feature | Clinical use | +|---|---| +| `~confidence` | Provisional vs confirmed diagnosis, differential weights | +| `~negated true` | "No history of diabetes" — a positive assertion of absence (the spec literally calls this out) | +| `~recorded` vs `~observed` | When the clinician learned vs when the patient says symptom started — bitemporal is *legally required* in EHR audit | +| `~source` | Patient-reported vs lab-confirmed vs imaging-derived | +| `~supersedes` | Retracted findings, corrected lab values | +| `~retention`, `~classification` | HIPAA/PHI lifecycle | +| `~uncertainty Gaussian` | Lab values with reference range and measurement error | + +Incumbents are FHIR, SNOMED CT, RxNorm — comprehensive but famously hostile. KNDL could plausibly become "the FHIR you can hand-write in a chart note." The risk is regulatory: nobody in healthcare adopts a non-standardized format without HL7 blessing. + +### 2. Threat intelligence / OSINT + +IOCs (IPs, hashes, domains) have a *known half-life* — that's literally what decay was built for. Provenance (which feed/analyst), confidence (single-source vs corroborated), supersedes (false positive retractions), `~negated` ("no observed C2 traffic") all map directly. STIX/TAXII is the standard but it's a 200+ page spec; KNDL is friendlier to write by hand or LLM-emit. Bellingcat-style investigative work is the same shape. + +### 3. Legal / e-discovery / case files + +Bitemporal is *the* feature law cares about: "what did the company know, and when did it know it?" `~recorded` (when discovery produced this) vs `~observed` (when the event happened) is legally meaningful in a way most data formats don't capture. `~source` for chain of custody, `~supersedes` for amended depositions, `~negated` for "no responsive documents." Almost no existing format models this cleanly. + +### 4. Scientific data / lab notebooks + +The spec's `~uncertainty Gaussian { mean = X stddev = Y }` is exactly what scientific measurement *needs* and what JSON/RDF can't express natively. Confidence + provenance + supersedes (retracted papers) + bitemporal recorded/observed all map. FAIR data principles want this. Existing solutions: ELNs (electronic lab notebooks) are mostly proprietary; HDF5 has no semantics; RDF-based science platforms exist but are academic-only. + +### 5. AI safety, evals, red-team findings + +Eat your own dog food. A red-team finding is: a model output (provenance: `agent://claude-...`), at a confidence (the eval grader's score, with calibration), valid for a model version (`~valid_start`/`~valid_end`), with a classification level (sensitive eval). Findings get superseded as models improve. This is a memo to Anthropic specifically: KNDL fits internal eval infra unusually well. + +### 6. Supply chain provenance / track-and-trace + +Custody chain *is* provenance. Certifications expire (`~valid_end`). Inspection findings have confidence. `~source` is the auditor. EU's Digital Product Passport mandates and IBM Food Trust are existing players; both are heavy. KNDL is light enough to run on a phone. + +### 7. Financial bitemporal reporting + +SOX/IFRS require "what did the books show at quarter end" *and* "what do we know now after restatement." That's literally bitemporal. XTDB/Datomic dominate technically; a portable file format on top would be net-new. + +## Decent fits worth mentioning + +- **Genealogy / family history** — primary vs secondary sources, confidence varies, supersedes corrects bad records +- **Insurance claims** — multi-source fact-finding with confidence +- **Content moderation appeals** — `~supersedes` is the appeal mechanism +- **Robotics SLAM / sensor fusion** — uncertainty distributions, frame-of-reference types (already in the spec via `Pose`) +- **Personal AI assistant memory** — the most obvious one; the original pitch +- **Battlefield situational awareness** — exactly the spec, plus classification levels + +## Honest ranking by wedge potential + +If forced to pick one to bet on, I'd reorder my earlier IoT recommendation: + +1. **Threat intelligence** — best feature fit, fastest validation cycle (security teams adopt new formats quickly), natural buyers (CISO budgets), small enough community to shape standards. +2. **AI safety evals (internal Anthropic / labs)** — meta-relevant to your distribution channel; deep-pocketed buyers. +3. **Clinical knowledge** — highest potential, slowest sales cycle, regulatory landmines. +4. **IoT** — your 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. IoT engineers shrug at confidence scores; threat analysts, doctors, and lawyers do not. + +The DSL still doesn't earn its keep in any of these (the JSON-LD pivot still applies), but the *contract* — confidence + provenance + bitemporal + decay + supersedes — is genuinely valuable, and these domains are where it's most expensive to live without. + +--- + +# What v2 actually shipped + +For the record, the v2 pivot resolved the following items from the analysis: + +| Critique | Status | +|---|---| +| DSL is invisible to actual usage | **Done** — DSL deleted, JSON-LD wire format | +| 1,168-line spec is huge / premature | **Done** — spec deleted; JSON-LD context + JSON Schema replace it | +| No semantic interop | **Done** — PROV-O alignment in the JSON-LD context | +| Storage is a thin demo | **Partial** — KuzuDB embedded backend added; concurrency story still incomplete | +| Knowledge + behavior conflated | **Partial** — processes deleted; intents kept as data with no execution promise | +| Brand collision with Kindle | **Open** — renamed expansion to "Data Link"; acronym unchanged | +| LLM confidence scores aren't calibrated | **Open** — biggest remaining frontier | +| Decay formula is arbitrary | **Open** — formula unchanged | +| No domain examples beyond IoT | **Done** — seven worked examples shipped (IoT, personal, threat-intel, clinical, legal, scientific, AI evals) | + +The contract — confidence + provenance + bitemporal + decay + supersedes + uncertainty — is the part that survives every alternative pivot in the analysis above. v2 doubles down on it. diff --git a/packages/kndl-memory/package-lock.json b/packages/kndl-memory/package-lock.json new file mode 100644 index 0000000..21c05f5 --- /dev/null +++ b/packages/kndl-memory/package-lock.json @@ -0,0 +1,2634 @@ +{ + "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", + "zod": "^3.23.0" + }, + "bin": { + "kndl": "dist/cli.js", + "kndl-memory-mcp": "dist/server.js" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "tsup": "^8.0.0", + "tsx": "^4.0.0", + "typescript": "^5.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/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/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "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/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/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": "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/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/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/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/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/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/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/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/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/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/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-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/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/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/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/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/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/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/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/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/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/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/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/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-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/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/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==", + "dev": true, + "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/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..a756e6a --- /dev/null +++ b/packages/kndl-memory/package.json @@ -0,0 +1,41 @@ +{ + "name": "@kndl/memory", + "version": "2.0.0-alpha.1", + "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 src/*.test.ts || tsx scripts/smoke.ts" + }, + "files": [ + "dist", + "context" + ], + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "tsx": "^4.0.0", + "tsup": "^8.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..5230e34 --- /dev/null +++ b/packages/kndl-memory/src/cli.ts @@ -0,0 +1,202 @@ +#!/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_MEMORY_DIR (default ./memory) + +import { FactStore, type FactInput } from "./core.js"; + +const MEMORY_DIR = process.env.KNDL_MEMORY_DIR ?? "./memory"; + +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 { + // collect contiguous non-flag values into an array if more than one + 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 require_(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: require_(s(flags.statement), "statement"), + confidence: conf, + source: require_(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 + help Show this message + +Env: + KNDL_MEMORY_DIR Memory root directory (default ./memory) + +Run \`kndl --help\` for command-specific options, or read the SKILL.md. +`; + +function main(argv: string[]): number { + const cmd = argv[0]; + const { flags } = parseArgs(argv.slice(1)); + + if (!cmd || cmd === "help" || flags.help) { + process.stdout.write(HELP); + return 0; + } + + const store = new FactStore(MEMORY_DIR); + + try { + switch (cmd) { + case "add": { + const r = store.assertFact(buildInput(flags)); + out({ id: r.id, path: r.path }); + return 0; + } + case "supersede": { + const oldId = require_(s(flags["old-id"]), "old-id"); + const r = store.supersedeFact(oldId, buildInput(flags)); + out({ id: r.id, supersedes: r.supersedes, path: r.path }); + return 0; + } + case "query": { + const r = 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"]), + }); + out(r); + return 0; + } + case "contradictions": { + out(store.contradictions({ subject: s(flags.subject), predicate: s(flags.predicate) })); + return 0; + } + case "provenance": { + const id = require_(s(flags.id), "id"); + const maxDepth = n(flags["max-depth"]); + out(store.provenanceChain(id, maxDepth)); + return 0; + } + case "list": { + out(store.list(s(flags.subject))); + return 0; + } + case "show": { + const id = require_(s(flags.id), "id"); + const f = store.show(id); + if (!f) { + process.stderr.write(`not found: ${id}\n`); + return 1; + } + out(f); + return 0; + } + default: + fail(`unknown command: ${cmd}. Run \`kndl help\` for usage.`); + } + } catch (e) { + fail((e as Error).message); + } +} + +process.exit(main(process.argv.slice(2))); diff --git a/packages/kndl-memory/src/core.ts b/packages/kndl-memory/src/core.ts new file mode 100644 index 0000000..11f7565 --- /dev/null +++ b/packages/kndl-memory/src/core.ts @@ -0,0 +1,382 @@ +// core.ts — KNDL fact store: shared between CLI and MCP server. +// +// Pure logic, except for the FS layer at the bottom. Decay math is in `effectiveConfidence`. +// Storage format: one JSON-LD file per fact under $KNDL_MEMORY_DIR/facts/, immutable +// once written; updates happen via `supersedeFact`. + +import { + existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, +} from "node:fs"; +import { join } from "node:path"; +import { createHash } from "node:crypto"; + +// ───────────────────────── types ────────────────────────── + +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; +} + +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; +} + +// ───────────────────────── 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 utils ────────────────────────── + +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"); +} + +// ───────────────────────── filesystem layer ────────────────────────── + +export class FactStore { + readonly memoryDir: string; + readonly factsDir: string; + readonly contextRel: string; + + constructor(memoryDir: string, contextRel = "../context/v1.jsonld") { + this.memoryDir = memoryDir; + this.factsDir = join(memoryDir, "facts"); + this.contextRel = contextRel; + } + + ensureDirs(): void { + mkdirSync(this.factsDir, { recursive: true }); + } + + loadAll(): Fact[] { + if (!existsSync(this.factsDir)) return []; + const files = readdirSync(this.factsDir).filter((f) => f.endsWith(".fact.json")); + const out: Fact[] = []; + for (const f of files) { + 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; + } + + supersededIds(facts: Fact[] = this.loadAll()): Set { + const out = new Set(); + for (const f of facts) if (f.supersedes) out.add(f.supersedes); + return out; + } + + // ─── write ── + + assertFact(input: FactInput, supersedesId?: string): { id: string; path: string; fact: Fact } { + this.ensureDirs(); + if (!(input.confidence >= 0 && input.confidence <= 1)) { + throw new Error(`confidence must be in [0,1]: ${input.confidence}`); + } + if (input.decay) parseDecay(input.decay); + + const recordedAt = nowIso(); + const validFrom = normalizeTime(input.validFrom, recordedAt); + const id = makeId(input.subject, input.predicate, input.statement); + + const fact: Fact = { + "@context": this.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; + + const fname = factFilename(fact["@id"]); + const path = join(this.factsDir, fname); + if (existsSync(path)) { + throw new Error(`refusing to overwrite ${path}; facts are immutable`); + } + writeFileSync(path, JSON.stringify(fact, null, 2)); + return { id, path, fact }; + } + + supersedeFact(oldId: string, input: FactInput): { id: string; path: string; supersedes: string; fact: Fact } { + const out = this.assertFact(input, oldId); + return { ...out, supersedes: oldId }; + } + + // ─── read ── + + query(opts: QueryOptions = {}): QueryResult { + const facts = this.loadAll(); + const superseded = this.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 }; + } + + contradictions(opts: { subject?: string; predicate?: string } = {}): { count: number; conflicts: ContradictionEntry[] } { + const facts = this.loadAll(); + const superseded = this.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 }; + } + + provenanceChain(rootId: string, maxDepth = 8): { root: string; depth: number; chain: ProvenanceNode[] } { + const facts = this.loadAll(); + const byId = new Map(facts.map((f) => [f["@id"], f])); + 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 }; + } + + list(subject?: string): string[] { + const facts = this.loadAll(); + const filtered = subject ? facts.filter((f) => f.subject === subject) : facts; + return filtered.map((f) => f["@id"]); + } + + show(id: string): Fact | null { + return this.loadAll().find((f) => f["@id"] === id) ?? null; + } +} + +// ───────────────────────── helpers ────────────────────────── + +function slugify(s: string): string { + return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || "fact"; +} + +function makeId(subject: string | undefined, predicate: string | undefined, statement: string): string { + const ts = nowIso().replace(/[-:]/g, ""); + 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}`; +} + +function factFilename(id: string): string { + return slugify(id.replace(/:/g, "-")) + ".fact.json"; +} + +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; +} diff --git a/packages/kndl-memory/src/server.ts b/packages/kndl-memory/src/server.ts new file mode 100644 index 0000000..93f2d0b --- /dev/null +++ b/packages/kndl-memory/src/server.ts @@ -0,0 +1,219 @@ +#!/usr/bin/env node +// kndl-memory-mcp — Model Context Protocol server for KNDL JSON-LD facts. +// +// Tools exposed: +// 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 new fact that replaces an old one +// as_of — bitemporal time-travel query +// provenance_chain — walk derivedFrom + supersedes backward +// +// Storage: $KNDL_MEMORY_DIR/facts/*.fact.json (default ./memory). +// Designed to mount into Anthropic Memory on Managed Agents. + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; +import { z } from "zod"; +import { FactStore } from "./core.js"; + +const MEMORY_DIR = process.env.KNDL_MEMORY_DIR ?? "./memory"; +const store = new FactStore(MEMORY_DIR); + +// ───────── zod schemas ───────── + +const AssertSchema = z.object({ + statement: z.string(), + confidence: z.number().min(0).max(1), + source: z.string(), + subject: z.string().optional(), + predicate: z.string().optional(), + object: z.unknown().optional(), + decay: z.string().optional(), + valid_from: z.string().optional(), + valid_until: z.string().optional(), + observed_at: z.string().optional(), + classification: z.string().optional(), + consent: z.string().optional(), + tenant: z.string().optional(), + derived_from: z.array(z.string()).optional(), + negated: z.boolean().optional(), +}); + +const QuerySchema = z.object({ + subject: z.string().optional(), + predicate: z.string().optional(), + as_of: z.string().optional(), + 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(), + subject: z.string().optional(), + predicate: z.string().optional(), +}); + +const ProvenanceSchema = z.object({ + id: z.string(), + max_depth: z.number().int().positive().optional(), +}); + +// ───────── helpers ───────── + +function toFactInput(args: z.infer) { + 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, + }; +} + +// minimal Zod -> JSON Schema. Replace with `zod-to-json-schema` for prod. +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) }; + return {}; +} + +// ───────── server wiring ───────── + +const server = new Server( + { name: "kndl-memory", version: "0.1.0" }, + { capabilities: { tools: {} } }, +); + +const TOOLS = [ + { + name: "assert_fact", + description: "Write a new fact to memory. Always include source, confidence, validFrom. Add decay for time-sensitive data (e.g. '0.5/30d' halves confidence every 30 days).", + inputSchema: AssertSchema, + }, + { + name: "query_facts", + description: "Read active (non-superseded) facts with effective confidence applied at as_of time. Filter by subject/predicate. Defaults to now.", + inputSchema: QuerySchema, + }, + { + name: "contradictions", + description: "Find disagreeing active facts about the same subject/predicate. Returns preferred fact and conflicts ranked by recency, confidence, and chain length.", + inputSchema: ContradictionsSchema, + }, + { + name: "supersede_fact", + description: "Write a new fact that replaces an older one. Preserves history (the old fact is hidden from queries but available for as_of time-travel).", + inputSchema: SupersedeSchema, + }, + { + name: "as_of", + description: "Bitemporal time-travel: what did memory believe at the given timestamp.", + inputSchema: AsOfSchema, + }, + { + name: "provenance_chain", + description: "Walk derivedFrom + supersedes backward to surface the audit trail of a fact.", + inputSchema: ProvenanceSchema, + }, +] as const; + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: TOOLS.map((t) => ({ + name: t.name, + description: t.description, + inputSchema: zodToJson(t.inputSchema), + })), +})); + +server.setRequestHandler(CallToolRequestSchema, async (req) => { + const { name, arguments: args } = req.params; + try { + let result: unknown; + switch (name) { + case "assert_fact": { + const a = AssertSchema.parse(args); + result = store.assertFact(toFactInput(a)); + break; + } + case "query_facts": { + const a = QuerySchema.parse(args); + result = store.query({ + subject: a.subject, + predicate: a.predicate, + asOf: a.as_of, + minConfidence: a.min_confidence, + tenant: a.tenant, + allowPhi: a.allow_phi, + }); + break; + } + case "contradictions": { + const a = ContradictionsSchema.parse(args); + result = store.contradictions({ subject: a.subject, predicate: a.predicate }); + break; + } + case "supersede_fact": { + const a = SupersedeSchema.parse(args); + const { old_id, ...rest } = a; + result = store.supersedeFact(old_id, toFactInput(rest as z.infer)); + break; + } + case "as_of": { + const a = AsOfSchema.parse(args); + result = store.query({ subject: a.subject, predicate: a.predicate, asOf: a.as_of }); + break; + } + case "provenance_chain": { + const a = ProvenanceSchema.parse(args); + result = store.provenanceChain(a.id, a.max_depth); + break; + } + default: + throw new Error(`unknown tool: ${name}`); + } + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; + } catch (e) { + return { + isError: true, + content: [{ type: "text", text: `error: ${(e as Error).message}` }], + }; + } +}); + +const transport = new StdioServerTransport(); +await server.connect(transport); +process.stderr.write("kndl-memory MCP server ready\n"); 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. - -[![PyPI](https://img.shields.io/pypi/v/kndl)](https://pypi.org/project/kndl/) -[![Python](https://img.shields.io/pypi/pyversions/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..52dee9e --- /dev/null +++ b/skills/kndl-memory/README.md @@ -0,0 +1,180 @@ +# 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..fc428ef --- /dev/null +++ b/skills/kndl-memory/SKILL.md @@ -0,0 +1,217 @@ +--- +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 ("kindle") 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 + +## Workflow + +The skill ships with the `kndl` CLI. Always invoke it via the bash tool. The CLI shares +its core implementation with the `kndl-memory-mcp` server, so behavior is identical +whether you're reading via bash or via MCP tools. + +> **Setup once per environment:** in the package directory, `npm install`, then either +> `npm link` (system-wide `kndl`) or use `npx kndl ...` from anywhere with the package +> installed. `KNDL_MEMORY_DIR` controls where facts live (default `./memory`). + +### 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 the `@id` of the written file. + +### 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's just hidden from active queries. + +### 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..e021559 --- /dev/null +++ b/skills/kndl-memory/eval/questions.json @@ -0,0 +1,65 @@ +{ + "scenario": "credit_decision", + "today": "2026-04-26T12:00:00Z", + "fixtures": "examples/loan-decision.example.jsonld", + "instructions_for_eval": "Run each question twice: once with the JSON dump pasted into the system prompt, once with the kndl-memory MCP server connected. Score binary right/wrong. KNDL should win on at least 5/8 small + 2/3 at scale.", + "questions": [ + { + "id": "q1-decay-employment", + "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. Recommend re-verification.", + "vanilla_failure_mode": "Reads original confidence 0.9 and says 'yes, employed at ACME' or hedges only weakly." + }, + { + "id": "q2-decay-credit", + "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 ~0.88. Still very fresh.", + "vanilla_failure_mode": "May either over-trust or fail to differentiate from older facts." + }, + { + "id": "q3-supersession", + "archetype": "supersession", + "prompt": "What is customer 9281's current credit score?", + "setup_note": "Run after supersede_fact: original 720 superseded by 740.", + "correct_behavior": "740. Cite the new fact's id, mention the older 720 was superseded today.", + "vanilla_failure_mode": "Returns 720 (or both, confused about which is current)." + }, + { + "id": "q4-as-of", + "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.", + "vanilla_failure_mode": "Returns the latest value (740) regardless of as-of date." + }, + { + "id": "q5-contradiction", + "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 recorded, higher effective confidence. Surface the conflict explicitly. Prefer Experian 720 (eff ~0.88) over SCHUFA 680 (eff ~0.63), but flag.", + "vanilla_failure_mode": "Picks one arbitrarily or averages them, no provenance distinction." + }, + { + "id": "q6-provenance", + "archetype": "provenance", + "prompt": "Where does our information about customer 9281's default come from? Who is the original source?", + "correct_behavior": "Walk provenance: SCHUFA, recorded 6 days ago, effective ~0.97. Cite source URI exactly.", + "vanilla_failure_mode": "Hand-waves about 'records' without citing exact URI; may invent sources." + }, + { + "id": "q7-loan-decision", + "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 to 0.08). Recommend ESCALATE due to recent default + need for fresh employment+income verification.", + "vanilla_failure_mode": "Treats 633-day-old employment as current, may decline outright on the default without context, or approve without flagging stale data." + }, + { + "id": "q8-rumor-handling", + "archetype": "low_confidence", + "prompt": "I heard customer 9281 might be filing for bankruptcy. Should we escalate?", + "correct_behavior": "The rumor exists in memory at confidence 0.3 / effective ~0.26 from a forum source. Mention it as low-confidence, do not state as fact, recommend verification before action.", + "vanilla_failure_mode": "Either ignores the rumor entirely, or treats it as credible because the system 'knows about it'." + } + ] +} 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/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..f7abf8e --- /dev/null +++ b/v2.md @@ -0,0 +1,676 @@ +# 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