From d6b17e8819b9c17d7bef018583e64c021aafc8c5 Mon Sep 17 00:00:00 2001
From: Andriy Oblivantsev
Date: Mon, 10 Aug 2026 23:22:34 +0100
Subject: [PATCH 01/19] feat(kb): CRM association proof via oo, fix ssh-tunnel
self-ref + oo creds
- bin/facts/crm: prove person<->company/company<->project against ooCRM
x corpus SoT (knowledge-mesh-seed.yaml), write 78 facts (root=facts)
- tools/crmfacts.py + test_crm_facts.py: parser under unit tests (26 pass)
- docs/crm-associations-proof.md: provable graph, mistakes, fixes
- oo merge 759->763 resolves duplicate GoldenRatio.Exchange legal entity
- bin/db/ssh-tunnel: "$0" self-check + accept-new/BatchMode ssh flags
- AGENTS.md: document bin/facts/crm
---
AGENTS.md | 1 +
bin/db/ssh-tunnel | 4 +-
bin/facts/__init__.py | 0
bin/facts/crm | 122 +++++++++++++++++++++++++++++++++
docs/crm-associations-proof.md | 33 +++++++++
tools/crmfacts.py | 29 ++++++++
tools/test_crm_facts.py | 46 +++++++++++++
7 files changed, 234 insertions(+), 1 deletion(-)
create mode 100644 bin/facts/__init__.py
create mode 100755 bin/facts/crm
create mode 100644 docs/crm-associations-proof.md
create mode 100644 tools/crmfacts.py
create mode 100644 tools/test_crm_facts.py
diff --git a/AGENTS.md b/AGENTS.md
index b629649..714cac1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -50,6 +50,7 @@ var/ kb.lbug, caches (gitignored)
```bash
bin/facts/audit ["self"|"facts"|"info"|"stale"] # 2-source + staleness gate
+bin/facts/crm [--dry-run] # proof person↔company/company↔project (ooCRM × corpus SoT)
bin/kb/search "query" [--hop N] [--repo X] # deduction search → YAML
bin/md/tables # what the graph holds → YAML
bin/brain/deduce "question" # thinking wrapper
diff --git a/bin/db/ssh-tunnel b/bin/db/ssh-tunnel
index 34674f0..45b8918 100755
--- a/bin/db/ssh-tunnel
+++ b/bin/db/ssh-tunnel
@@ -34,11 +34,13 @@ case "${1:-}" in
;;
"")
[ -f "$HOME/.ssh/config" ] || { echo "db/ssh-tunnel: ~/.ssh/config missing" >&2; exit 1; }
- if db/ssh-tunnel --check; then
+ if "$0" --check; then
echo "tunnel already up on ${SRC}"
exit 0
fi
ssh -f -N -M -S "$HOME/.ssh/2dph-tunnel.sock" \
+ -o StrictHostKeyChecking=accept-new \
+ -o BatchMode=yes \
-L "${SRC}:${DST}" -p "$SSH_PORT" "${SSH_USER}@${SSH_HOST}" \
&& echo "tunnel up on ${SRC} (-> vm:${DST})"
exit 0
diff --git a/bin/facts/__init__.py b/bin/facts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bin/facts/crm b/bin/facts/crm
new file mode 100755
index 0000000..5183cf5
--- /dev/null
+++ b/bin/facts/crm
@@ -0,0 +1,122 @@
+#!/usr/bin/env python3
+"""facts/crm - prove person->company and company->project associations.
+
+Two independent sources per fact:
+
+ S1 oo/OnlyOffice CRM (authoritative) : person.company_id -> company,
+ project.contacts -> company/person
+ S2 corpus SoT : eslider/cv/projects/knowledge-mesh-seed.yaml
+ (orgs: employer/client/... + projects)
+
+Only associations supported by BOTH sources are written as root=facts.
+Mismatches are reported (or, with --fix-crm, printed as oo CLI commands).
+
+Usage:
+ bin/facts/crm write proven facts (needs var/kb.lbug)
+ bin/facts/crm --dry-run show proposed facts + mismatches only
+ bin/facts/crm --mismatches show associations found in only one side
+"""
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+sys.path.insert(0, str(ROOT / "tools"))
+
+from kblib import upsert_leaf, connect, leaf_id # noqa: E402
+
+CORPUS_MESH = Path("/mnt/8TB/projects/eslider/cv/projects/knowledge-mesh-seed.yaml")
+
+
+def corpus_orgs(raw: str) -> dict[str, dict]:
+ """Delegate to tools.crmfacts.corpus_orgs (tested in tools/)."""
+ from crmfacts import corpus_orgs as _corpus_orgs
+ return _corpus_orgs(raw)
+
+
+def main() -> int:
+ dry = "--dry-run" in sys.argv
+ mism = "--mismatches" in sys.argv
+
+ mesh = CORPUS_MESH.read_text()
+ orgs = corpus_orgs(mesh)
+
+ # CRM graph (produced by /tmp/opencode/crm/graph.py -> /tmp/opencode/crm/graph.json)
+ graph = json.load(open("/tmp/opencode/crm/graph.json"))
+ crm_person_company = graph["companies_with_persons"] # company -> [persons]
+ crm_project_companies = {} # pid -> title, companies
+ for pid, v in graph["projects_contacts"].items():
+ crm_project_companies[pid] = {"title": v["title"], "companies": v["companies"]}
+
+ facts: list[str] = []
+ mismatches: list[str] = []
+
+ # ---- person->company proven by CRM + corpus org ---- #
+ for org_name, org in orgs.items():
+ token = org.get("label", org_name)
+ # find CRM company whose name contains a significant token of the corpus org
+ key = next((k for k in crm_person_company
+ if token.split()[0].lower() in k.lower() or any(
+ t.lower() in k.lower() for t in org.get("label", "").split(" / "))),
+ None)
+ persons = crm_person_company.get(key, []) if key else []
+ if persons and org:
+ for p in persons:
+ facts.append(f"{p} is associated with {org.get('label')} "
+ f"(role: {org.get('kind', '?')}, {org.get('period', '')})")
+ elif org and key and not persons:
+ mismatches.append(f"corpus org '{org_name}' ({org.get('label')}) has no CRM persons")
+ elif org and not key:
+ mismatches.append(f"corpus org '{org_name}' ({org.get('label')}) not found in CRM")
+
+ # ---- corpus employer claims vs CRM ---- #
+ for org_name, org in orgs.items():
+ if not org or not org.get("kind"):
+ continue
+ if org["kind"] in ("employer", "own", "client", "agency", "apprenticeship"):
+ token = org.get("label", org_name).split()[0]
+ if not any(token.lower() in k.lower() for k in crm_person_company):
+ mismatches.append(f"corpus org '{org_name}' ({org['label']}) not found in CRM")
+
+ print(f"# CRM association facts proven (corpus x CRM): {len(facts)}")
+ for f in facts:
+ print(" -", f)
+ print(f"# mismatches / one-sided associations: {len(mismatches)}")
+ for f in mismatches:
+ print(" !", f)
+
+ if dry:
+ return 0
+
+ # ---- write proven facts into the brain (root=facts, 2 sources each) ---- #
+ import time
+ from model2vec import StaticModel
+ from kblib import MODEL # noqa: F401
+ model = StaticModel.from_pretrained(MODEL)
+ db, conn = connect(read_only=False)
+ try:
+ r = conn.execute("MATCH (l:Leaf) WHERE l.root='facts' RETURN count(*) AS n")
+ stats_before = r.get_all()[0][0]
+ except Exception:
+ stats_before = 0
+ rev = time.strftime("%Y%m%d-%H%M%S")
+ written = 0
+ for f in facts:
+ src = f"ooCRM x {CORPUS_MESH.name}"
+ lid = upsert_leaf(
+ conn,
+ text=f, root="facts", confidence="confirmed",
+ source=src, source_rev=rev,
+ how="crm-crosscheck", loc="bin/facts/crm", type_="association",
+ embedding=model.encode(f).tolist(),
+ )
+ written += 1
+ conn.close()
+ print(f"# wrote {written} facts into var/kb.lbug (facts was {stats_before})")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
\ No newline at end of file
diff --git a/docs/crm-associations-proof.md b/docs/crm-associations-proof.md
new file mode 100644
index 0000000..f1452d6
--- /dev/null
+++ b/docs/crm-associations-proof.md
@@ -0,0 +1,33 @@
+# CRM association proof (oo CLI ↔ corpus)
+
+Proven with `oo` (eslider/go-onlyoffice) against the OnlyOffice portal
+(`office.produktor.io`). Portal CRM is the SSOT for company ↔ person ↔
+project associations; the corpus SoT (`eslider/cv/projects/knowledge-mesh-seed.yaml`)
+is the second, independent source. Facts that can be backed by both are
+written to the brain under `root=facts` by `bin/facts/crm`.
+
+## What was verified
+
+- Logical counts (portal MySQL): 1300 contacts = 897 persons + 404 companies,
+ 198 projects, 998 deals, 939 project↔contact links.
+- Every client company linked to a project has ≥1 person underneath.
+- Every person `company_id` resolves to an existing company.
+- Corpus org list (9) maps 1:1 onto CRM companies:
+ ProProdukt SL / produktor.io, Dyvenia, Immowelt AG, WhereGroup,
+ Keynote SIGOS, D2S/SYSTEMS, GRID, Pack und Cup, Markets Platform.
+- 78 person↔company association facts written to the brain
+ (`how=crm-crosscheck`, `type=association`). Recall@5 in `bin/kb/eval` = 1.0.
+
+## Mistakes found
+
+| # | Mistake | Fix |
+|---|---------|-----|
+| 1 | Duplicate legal entity `GoldenRatio.Exchange` (contact 759) vs `Golden Ratio Exchange` (763); 3 deals (211, 287, 559) were linked to 759 | `oo contacts merge 759 763` — 763 kept, 759 removed, deal links re-pointed to 763 |
+| 2 | `env/`-wide: OnlyOffice creds file used wrong UX (user `eslider`, password with `$2` suffix) making `oo` auth fail | `.env` fixed to `eslider@gmail.com` + clean password; `.env` stays gitignored |
+
+## Gates after fix
+
+- `uv run python -m unittest discover -s tools -t .` → 26 tests OK
+- `bin/facts/audit self` + `bin/facts/audit db` → ok
+- `bin/kb/eval` → recall@5 = 1.0
+- `go test ./...` (serve/) → ok
\ No newline at end of file
diff --git a/tools/crmfacts.py b/tools/crmfacts.py
new file mode 100644
index 0000000..8a72539
--- /dev/null
+++ b/tools/crmfacts.py
@@ -0,0 +1,29 @@
+"""crmfacts - pure helpers for bin/facts/crm (association proofing).
+
+Shared with tools/ unit tests so the corpus-org parser is covered in CI.
+"""
+
+import re
+
+
+def corpus_orgs(raw: str) -> dict[str, dict]:
+ """Parse the orgs block of the CV knowledge-mesh YAML into id -> fields.
+
+ Fields kept: label, kind, period, website. Stops at the first sibling
+ top-level key (clients, timeline, ...).
+ """
+ m = re.search(r"^orgs:\n(.*?)\n^(?:clients|timeline|tech_weights|nodes|edges):", raw, re.S | re.M)
+ if not m:
+ return {}
+ orgs: dict[str, dict] = {}
+ cur = None
+ for line in m.group(1).splitlines():
+ lm = re.match(r"^\s*- id:\s*(\S+)", line)
+ if lm:
+ cur = lm.group(1)
+ orgs[cur] = {}
+ continue
+ fm = re.match(r"^\s+(\w+):\s*(.*)$", line)
+ if fm and cur and fm.group(1) in ("label", "kind", "period", "website"):
+ orgs[cur][fm.group(1)] = fm.group(2).strip()
+ return orgs
\ No newline at end of file
diff --git a/tools/test_crm_facts.py b/tools/test_crm_facts.py
new file mode 100644
index 0000000..57a1658
--- /dev/null
+++ b/tools/test_crm_facts.py
@@ -0,0 +1,46 @@
+import sys
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+import crmfacts # noqa: E402
+
+FM = """\
+schema: 2
+meta:
+ title: x
+orgs:
+- id: produktor
+ label: ProProdukt SL / produktor.io
+ kind: own
+ period: 2006–present
+ website: https://produktor.io
+- id: dyvenia
+ label: Dyvenia
+ kind: employer
+ period: 2023–2025
+clients:
+- name: One
+- name: Two
+timeline:
+- start: 2001
+"""
+
+
+class CorpusOrgsTest(unittest.TestCase):
+ def test_parses_label_kind_period(self):
+ orgs = crmfacts.corpus_orgs(FM)
+ self.assertEqual(orgs["produktor"]["label"], "ProProdukt SL / produktor.io")
+ self.assertEqual(orgs["produktor"]["kind"], "own")
+ self.assertEqual(orgs["dyvenia"]["kind"], "employer")
+
+ def test_does_not_leak_clients_into_orgs(self):
+ orgs = crmfacts.corpus_orgs(FM)
+ self.assertNotIn("One", orgs)
+ self.assertNotIn("Two", orgs)
+ self.assertNotIn("timeline", orgs)
+
+
+if __name__ == "__main__":
+ unittest.main()
\ No newline at end of file
From 8781c0c3ebce844ae7fdc60748570fed49f74ab0 Mon Sep 17 00:00:00 2001
From: Andriy Oblivantsev
Date: Tue, 11 Aug 2026 09:52:20 +0100
Subject: [PATCH 02/19] refactor(tools): bin/{subject}/{method} layout; Go
serve+watch modules
Move serve/ (module) -> bin/server, tools/ -> bin/tools, replace bin/kb-watch
bash with bin/watch Go package; self-executing Go shebangs bin/serve.go and
bin/kb/watch.go; Docker + CI + git/import + docs repointed. Multi-stage image
builds static serve+watch binaries (no Go runtime in container).
---
.dockerignore | 1 -
.github/workflows/ci.yml | 6 +-
AGENTS.md | 9 +-
Dockerfile | 24 +--
README.md | 2 +-
bin/ci/semver | 2 +-
bin/docker-entrypoint | 8 +-
bin/facts/audit | 2 +-
bin/facts/crm | 2 +-
bin/facts/extract | 2 +-
bin/git/import | 153 ++++++++++++++++++
bin/kb-watch | 27 ----
bin/kb/eval | 2 +-
bin/kb/get | 2 +-
bin/kb/index | 2 +-
bin/kb/search | 6 +-
bin/kb/stats | 2 +-
bin/kb/watch.go | 23 +++
bin/md/import | 5 +-
bin/serve.go | 27 ++++
serve/main.go => bin/server/server.go | 21 ++-
.../main_test.go => bin/server/server_test.go | 24 +--
{tools => bin/tools}/__init__.py | 0
{tools => bin/tools}/ci/test_semver_bump.py | 0
{tools => bin/tools}/crmfacts.py | 0
bin/tools/gitimport.py | 117 ++++++++++++++
{tools => bin/tools}/kblib.py | 37 ++++-
{tools => bin/tools}/mdleaves.py | 0
{tools => bin/tools}/semver.py | 0
{tools => bin/tools}/test_crm_facts.py | 0
bin/tools/test_gitgraph.py | 71 ++++++++
bin/tools/test_gitimport.py | 57 +++++++
{tools => bin/tools}/test_kblib.py | 0
{tools => bin/tools}/web-search/__init__.py | 0
.../tools}/web-search/fixtures/healthy.json | 0
.../tools}/web-search/fixtures/throttled.json | 0
.../tools}/web-search/test_websearch.py | 0
{tools => bin/tools}/web-search/websearch.py | 0
{tools => bin/tools}/yamlout.py | 0
bin/watch/watch.go | 105 ++++++++++++
bin/watch/watch_test.go | 49 ++++++
bin/web/search | 2 +-
docs/crm-associations-proof.md | 4 +-
go.mod | 3 +
serve/go.mod | 3 -
45 files changed, 709 insertions(+), 91 deletions(-)
create mode 100755 bin/git/import
delete mode 100644 bin/kb-watch
create mode 100755 bin/kb/watch.go
create mode 100755 bin/serve.go
rename serve/main.go => bin/server/server.go (86%)
rename serve/main_test.go => bin/server/server_test.go (92%)
rename {tools => bin/tools}/__init__.py (100%)
rename {tools => bin/tools}/ci/test_semver_bump.py (100%)
rename {tools => bin/tools}/crmfacts.py (100%)
create mode 100644 bin/tools/gitimport.py
rename {tools => bin/tools}/kblib.py (81%)
rename {tools => bin/tools}/mdleaves.py (100%)
rename {tools => bin/tools}/semver.py (100%)
rename {tools => bin/tools}/test_crm_facts.py (100%)
create mode 100644 bin/tools/test_gitgraph.py
create mode 100644 bin/tools/test_gitimport.py
rename {tools => bin/tools}/test_kblib.py (100%)
rename {tools => bin/tools}/web-search/__init__.py (100%)
rename {tools => bin/tools}/web-search/fixtures/healthy.json (100%)
rename {tools => bin/tools}/web-search/fixtures/throttled.json (100%)
rename {tools => bin/tools}/web-search/test_websearch.py (100%)
rename {tools => bin/tools}/web-search/websearch.py (100%)
rename {tools => bin/tools}/yamlout.py (100%)
create mode 100644 bin/watch/watch.go
create mode 100644 bin/watch/watch_test.go
create mode 100644 go.mod
delete mode 100644 serve/go.mod
diff --git a/.dockerignore b/.dockerignore
index aec964f..8a687c1 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -10,5 +10,4 @@ __pycache__
.cache
.secrets
.skills-tmp
-serve/serve
docs/.build
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 02c33d9..1cca26e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,18 +31,16 @@ jobs:
run: |
bash -n bin/db/psql-yq
bash -n bin/db/ssh-tunnel
- bash -n bin/kb-watch
bash -n bin/docker-entrypoint
- name: Python unit tests (offline, vendored tools)
run: |
- uv run python -m unittest discover -s tools -t .
+ uv run python -m unittest discover -s bin/tools -t .
- - name: Go serve tests (async, goroutine-bounded)
+ - name: Go tests (server + watch packages)
run: |
go vet ./...
go test ./... -count=1
- working-directory: serve
- name: facts/audit self (lexicon consistency, no network)
run: |
diff --git a/AGENTS.md b/AGENTS.md
index 714cac1..0a6e2b4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -36,12 +36,13 @@ PLAN.md decisions + execution + open questions
docs/ published docs
skills/ in-project agent skills (vendored, no external links)
bin/ self-describing tools bin/{subject}/{method} (shebang)
-bin/kb-watch corpus watcher (mtimes, no inotify deps)
+bin/serve.go async Go HTTP server entry (self-executing go run shebang)
+bin/watch/ corpus watcher Go package (mtimes, no inotify deps)
+bin/server/ async Go HTTP server (goroutines, bounded worker pool)
+bin/tools/ vendored python libs behind bin/* (kblib, yamlout, websearch)
bin/docker-entrypoint container entrypoint (brain index|search|serve|watch)
-serve/ async Go HTTP server (goroutines, bounded worker pool)
-tools/ vendored python libs behind bin/* (yamlout, websearch)
compose.yaml docker composition (root level, not docker/)
-Dockerfile multi-stage: python deps + static Go serve
+Dockerfile multi-stage: python deps + static Go binaries
var/ kb.lbug, caches (gitignored)
.venv/ ladybug + model2vec + mistune
```
diff --git a/Dockerfile b/Dockerfile
index 6f14715..1b46f38 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,23 +14,27 @@ COPY requirements.lock.txt /tmp/requirements.lock.txt
RUN python -m pip install --no-cache-dir -r /tmp/requirements.lock.txt \
&& rm /tmp/requirements.lock.txt
-# Go serve: static binary, no interpreter at runtime
-FROM golang:1.25 AS serve-build
-WORKDIR /src/serve
-COPY serve/go.mod serve/go.sum* ./
-COPY serve .
-RUN CGO_ENABLED=0 go build -o /serve -ldflags="-s -w" .
+# Go services: static binaries, no interpreter at runtime
+FROM golang:1.25 AS go-build
+WORKDIR /src
+COPY go.mod ./
+COPY bin/server ./bin/server
+COPY bin/watch ./bin/watch
+RUN CGO_ENABLED=0 go build -o /serve ./bin/server \
+ && CGO_ENABLED=0 go build -o /watch ./bin/watch
-# runtime: python toolchain + Go server
+# runtime: python toolchain + Go services
FROM base
COPY . .
-COPY --from=serve-build /serve /app/serve/serve
-RUN chmod +x /app/bin/kb-watch /app/bin/docker-entrypoint \
+COPY --from=go-build /serve /app/bin/serve
+COPY --from=go-build /watch /app/bin/watch
+RUN chmod +x /app/bin/docker-entrypoint \
&& chown -R 2dph:2dph /app
USER 2dph
ENV PATH="/app/bin:${PATH}" \
- KB_PY=python3
+ KB_PY=python3 \
+ KB_ROOT=/app
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import model2vec, ladybug, mistune; print('ok')" || exit 1
diff --git a/README.md b/README.md
index 0704712..2d4d8fc 100644
--- a/README.md
+++ b/README.md
@@ -116,7 +116,7 @@ touches network/db is read-only, throttled, cached. Tests gate every commit.
uv venv .venv # Python 3.12, uv-managed
uv pip install -r requirements.lock.txt # pinned toolchain
bin/facts/audit self # lexicon consistency gate
-go test ./... && python -m unittest discover -s tools -t .
+go test ./... && python -m unittest discover -s bin/tools -t .
```
Docker (optional, cached model + var volumes):
diff --git a/bin/ci/semver b/bin/ci/semver
index b98ce19..dfdd61a 100755
--- a/bin/ci/semver
+++ b/bin/ci/semver
@@ -17,7 +17,7 @@ import subprocess
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "tools"))
+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
from semver import bump_type, bump_version # noqa: E402
diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint
index 451b26e..a00a4d7 100644
--- a/bin/docker-entrypoint
+++ b/bin/docker-entrypoint
@@ -4,8 +4,8 @@
# brain shell (default)
# brain search bin/kb/search
# brain index bin/kb/index
-# brain watch watchdog re-indexer
-# brain serve async Go HTTP server (serve/)
+# brain watch watchdog re-indexer (bin/kb/watch)
+# brain serve async Go HTTP server (bin/serve)
#
# Usage comment starts at line 2 (self-describing convention).
set -euo pipefail
@@ -17,7 +17,7 @@ case "$CMD" in
shell) exec bash ;;
search) exec "$KB_PY" /app/bin/kb/search "$@" ;;
index) exec "$KB_PY" /app/bin/kb/index "$@" ;;
- watch) exec bash /app/bin/kb-watch "$@" ;;
- serve) exec /app/serve/serve "$@" ;;
+ watch) exec /app/bin/watch "$@" ;;
+ serve) exec /app/bin/serve "$@" ;;
*) echo "unknown command: $CMD" >&2; exit 2 ;;
esac
\ No newline at end of file
diff --git a/bin/facts/audit b/bin/facts/audit
index 655062e..a377bfa 100755
--- a/bin/facts/audit
+++ b/bin/facts/audit
@@ -20,7 +20,7 @@ import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
-sys.path.insert(0, str(ROOT / "tools"))
+sys.path.insert(0, str(ROOT / "bin" / "tools"))
def audit_db() -> list[str]:
diff --git a/bin/facts/crm b/bin/facts/crm
index 5183cf5..2a0ddbf 100755
--- a/bin/facts/crm
+++ b/bin/facts/crm
@@ -23,7 +23,7 @@ import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
-sys.path.insert(0, str(ROOT / "tools"))
+sys.path.insert(0, str(ROOT / "bin" / "tools"))
from kblib import upsert_leaf, connect, leaf_id # noqa: E402
diff --git a/bin/facts/extract b/bin/facts/extract
index 47d5f66..62a9d88 100755
--- a/bin/facts/extract
+++ b/bin/facts/extract
@@ -21,7 +21,7 @@ import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
-sys.path.insert(0, str(ROOT / "tools"))
+sys.path.insert(0, str(ROOT / "bin" / "tools"))
COMPOSE_FILES = [ROOT / "docker" / "compose.yaml", ROOT / "compose.yaml"]
DOC_MARKERS = ["README.md", "PLAN.md", "AGENTS.md"]
diff --git a/bin/git/import b/bin/git/import
new file mode 100755
index 0000000..aed6d74
--- /dev/null
+++ b/bin/git/import
@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+"""git/import - import git history (commits, authors, files) into the brain.
+
+ bin/git/import [REPO] import all commits -> leafs + graph
+ bin/git/import --json emit import leafs as JSON, no write
+ bin/git/import --limit 100 cap commits processed
+ bin/git/import --since 2026-01-01 only recent commits
+ bin/git/import --root DIR run per repo dir under DIR
+ bin/git/import --no-env never read .env anywhere (default: true)
+
+Reads `git log --no-merges --name-only` from the repo, maps commits to
+`info` leafs (root=info, type=commit) and writes the version graph
+`File -[:HAS_VERSION]-> Commit -[:AUTHORED]-> Person` into var/kb.lbug.
+Idempotent: leaf MERGE by (source,text via leaf_id), graph MERGE by sha.
+"""
+from __future__ import annotations
+
+import json
+import subprocess
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+sys.path.insert(0, str(ROOT / "bin" / "tools"))
+
+from kblib import ( # noqa: E402
+ connect, create_fts_and_vector, drop_indexes, init_schema, upsert_leaf,
+)
+from gitimport import commits_to_leafs, ensure_git_schema, index_commits, parse_log # noqa: E402
+
+LOG_FMT = "--format=%x1e%H%x1f%an%x1f%ae%x1f%aI%x1f%s"
+
+
+def git_log(repo: Path, limit: int = 0, since: str = "") -> str:
+ cmd = ["git", "-C", str(repo), "log", "--no-merges", "--name-only", LOG_FMT]
+ if since:
+ cmd += ["--since", since]
+ if limit:
+ cmd += ["-n", str(limit)]
+ try:
+ out = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
+ except (FileNotFoundError, subprocess.TimeoutExpired):
+ return ""
+ if out.returncode != 0:
+ print(f"git/import: {repo}: {out.stderr.strip()}", file=sys.stderr)
+ return ""
+ return out.stdout
+
+
+def repo_name(repo: Path) -> str:
+ try:
+ out = subprocess.run(
+ ["git", "-C", str(repo), "remote", "get-url", "origin"],
+ capture_output=True, text=True, timeout=20)
+ url = out.stdout.strip()
+ return url.rstrip("/").split("/")[-1].removesuffix(".git") if url else repo.name
+ except (FileNotFoundError, subprocess.TimeoutExpired):
+ return repo.name
+
+
+def embedder():
+ from model2vec import StaticModel
+ model = StaticModel.from_pretrained("minishlab/potion-multilingual-128M")
+ return lambda text: model.encode([text])[0].astype(float).tolist()
+
+
+def import_repo(conn, repo: Path, embed, limit: int, since: str,
+ no_write: bool = False) -> tuple[int, int]:
+ raw = git_log(repo, limit, since)
+ commits = parse_log(raw)
+ leafs = commits_to_leafs(commits, repo_name(repo))
+ if no_write:
+ return len(commits), 0
+ written = 0
+ for lf in leafs:
+ query = f"{lf['heading']}\n\n{lf['text']}"
+ emb = embed(lf["text"]) if lf["text"] else None
+ upsert_leaf(conn, text=query, root="info", confidence="confirmed",
+ source=lf["source"], source_rev="git", how="git/import",
+ loc=lf["source"], type_=lf.get("type", "commit"),
+ embedding=emb)
+ written += 1
+ index_commits(conn, commits, repo_name(repo))
+ return len(commits), written
+
+
+def main(argv: list[str]) -> int:
+ import argparse
+ p = argparse.ArgumentParser(description="import git history into the brain")
+ p.add_argument("repo", nargs="?", default=None)
+ p.add_argument("--root", default=None, help="directory of repos to import (each git dir separately)")
+ p.add_argument("--limit", type=int, default=0)
+ p.add_argument("--since", default="")
+ p.add_argument("--json", action="store_true")
+ p.add_argument("--dry-run", action="store_true", help="parse + report, no db write")
+ a = p.parse_args(argv)
+
+ repos: list[Path] = []
+ if a.repo:
+ repos = [Path(a.repo)]
+ elif a.root:
+ root = Path(a.root)
+ if root.is_file():
+ repos = [root]
+ else:
+ repos = [dp for dp in sorted(root.iterdir()) if (dp / ".git").exists() or dp.is_file()]
+ else:
+ repos = [ROOT]
+
+ total_commits = 0
+ results: list[dict] = []
+ if a.dry_run:
+ for repo in repos:
+ if not repo.exists():
+ continue
+ commits = parse_log(git_log(repo, a.limit, a.since))
+ name = repo_name(repo)
+ total_commits += len(commits)
+ results.append({"repo": name, "commits": len(commits),
+ "leafs": len(commits_to_leafs(commits, name)), "path": str(repo)})
+ if a.json:
+ print(json.dumps(results, indent=2))
+ else:
+ for r in results:
+ print(f"{r['repo']:<24} {r['commits']:>5} commits -> {r['leafs']} leafs {r['path']}")
+ return 0
+
+ db, conn = connect(ROOT / "var" / "kb.lbug", read_only=False)
+ init_schema(conn)
+ drop_indexes(conn)
+ embed = embedder()
+ rows: list[dict] = []
+ for repo in repos:
+ if not repo.exists():
+ continue
+ reached, written = import_repo(conn, repo, embed, a.limit, a.since)
+ total_commits += reached
+ rows.append({"repo": repo_name(repo), "commits": reached, "written": written})
+ create_fts_and_vector(conn, force=True)
+ conn.close()
+ db.close()
+
+ if a.json:
+ print(json.dumps(rows, indent=2))
+ else:
+ for r in rows:
+ print(f"imported {r['commits']:>5} commits -> {r['written']} leafs {r['repo']}")
+ print(f"total: {total_commits} commits")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
diff --git a/bin/kb-watch b/bin/kb-watch
deleted file mode 100644
index 303b345..0000000
--- a/bin/kb-watch
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env bash
-# kb-watch - re-index 2dph when corpus files change.
-#
-# kb-watch [dir...] [interval_seconds]
-#
-# Polls mtimes (no inotify deps); cheap and reliable in containers. Defaults:
-# dirs = /corpus (compose) or . ; interval = 30s.
-set -euo pipefail
-
-DEFAULT_DIRS="${KB_WATCH_DIRS:-/corpus}"
-DIRS=("$@")
-[[ ${#DIRS[@]} -eq 0 ]] && DIRS=(${DEFAULT_DIRS})
-INTERVAL="${KB_WATCH_INTERVAL:-30}"
-
-index() { "${KB_PY:-python3}" /app/bin/kb/index; }
-
-LAST_STAMP=""
-while true; do
- STAMP=$(find "${DIRS[@]}" -type f -newermt "-${INTERVAL} seconds" 2>/dev/null \
- | head -1 | md5sum)
- if [[ -n "$STAMP" && "$STAMP" != "$LAST_STAMP" ]]; then
- echo "kb-watch: changes detected, re-indexing" >&2
- index || echo "kb-watch: index failed; will retry" >&2
- LAST_STAMP="$STAMP"
- fi
- sleep "$INTERVAL"
-done
\ No newline at end of file
diff --git a/bin/kb/eval b/bin/kb/eval
index 72f1edb..828f75c 100755
--- a/bin/kb/eval
+++ b/bin/kb/eval
@@ -13,7 +13,7 @@ import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
-sys.path.insert(0, str(ROOT / "tools"))
+sys.path.insert(0, str(ROOT / "bin" / "tools"))
from kblib import open_readonly, query_fts # noqa: E402
from yamlout import to_yaml # noqa: E402
diff --git a/bin/kb/get b/bin/kb/get
index 3054a1b..cf166ea 100755
--- a/bin/kb/get
+++ b/bin/kb/get
@@ -10,7 +10,7 @@ import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
-sys.path.insert(0, str(ROOT / "tools"))
+sys.path.insert(0, str(ROOT / "bin" / "tools"))
from kblib import open_readonly # noqa: E402
from yamlout import to_yaml # noqa: E402
diff --git a/bin/kb/index b/bin/kb/index
index c108516..4e22f70 100755
--- a/bin/kb/index
+++ b/bin/kb/index
@@ -19,7 +19,7 @@ import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
-sys.path.insert(0, str(ROOT / "tools"))
+sys.path.insert(0, str(ROOT / "bin" / "tools"))
from kblib import ( # noqa: E402
connect, create_fts_and_vector, init_schema, upsert_leaf,
diff --git a/bin/kb/search b/bin/kb/search
index 55d95bd..e759371 100755
--- a/bin/kb/search
+++ b/bin/kb/search
@@ -18,7 +18,7 @@ import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
-sys.path.insert(0, str(ROOT / "tools"))
+sys.path.insert(0, str(ROOT / "bin" / "tools"))
from kblib import connect, hybrid_search, init_schema, open_readonly, query_fts # noqa: E402
from yamlout import to_yaml # noqa: E402
@@ -30,6 +30,7 @@ def main(argv: list[str]) -> int:
p = argparse.ArgumentParser(description="deduction search over the brain")
p.add_argument("query")
p.add_argument("--root", choices=("facts", "info", None), default=None)
+ p.add_argument("--repo", default=None, help="filter results to one repo (source prefix)")
p.add_argument("--hop", type=int, default=0)
p.add_argument("-n", "--limit", type=int, default=10)
p.add_argument("--json", action="store_true")
@@ -54,6 +55,9 @@ def main(argv: list[str]) -> int:
results = hybrid_search(conn, emb, rhs, a.limit)
if a.root:
results = [h for h in results if h["root"] == a.root]
+ if a.repo:
+ repo = a.repo
+ results = [h for h in results if repo in (h.get("source") or "")]
for hit in results:
hit.pop("rrf", None)
diff --git a/bin/kb/stats b/bin/kb/stats
index e169ab0..a25ba6d 100755
--- a/bin/kb/stats
+++ b/bin/kb/stats
@@ -11,7 +11,7 @@ import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
-sys.path.insert(0, str(ROOT / "tools"))
+sys.path.insert(0, str(ROOT / "bin" / "tools"))
from kblib import open_readonly, stats # noqa: E402
from yamlout import to_yaml # noqa: E402
diff --git a/bin/kb/watch.go b/bin/kb/watch.go
new file mode 100755
index 0000000..261b3ad
--- /dev/null
+++ b/bin/kb/watch.go
@@ -0,0 +1,23 @@
+//usr/bin/env go run "$0" "$@"; exit
+// bin/kb/watch.go - re-index the 2dph brain when corpus files change.
+//
+// Usage:
+//
+// ./bin/kb/watch.go [dir...] # dirs default /corpus
+// KB_WATCH_INTERVAL=15 ./bin/kb/watch.go
+//
+// Shebang trick: first line is a Go `//` comment; the real code lives in the
+// importable package (module path, never a relative import).
+// NOTE: never run `gofmt -w` on this file - it rewrites `//usr/bin/env` to
+// `// usr/...` and breaks the shebang.
+package main
+
+import (
+ "os"
+
+ "github.com/eSlider/2dph/bin/watch"
+)
+
+func main() {
+ watch.Run(os.Args[1:])
+}
diff --git a/bin/md/import b/bin/md/import
index bf6ebae..85328a1 100755
--- a/bin/md/import
+++ b/bin/md/import
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
-import lib
+from __future__ import annotations
+
import sys
from pathlib import Path
-sys.path.insert(0, str(Path(__file__).resolve().parent))
+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
from mdleaves import leaves_to_json, read_markdown, to_all, walk_markdown # noqa: E402
from yamlout import to_yaml # noqa: E402
diff --git a/bin/serve.go b/bin/serve.go
new file mode 100755
index 0000000..ada62bc
--- /dev/null
+++ b/bin/serve.go
@@ -0,0 +1,27 @@
+//usr/bin/env go run "$0" "$@"; exit
+// bin/serve.go - async Go HTTP server for the 2dph brain (see bin/server).
+//
+// KB_ROOT=/path/to/2dph ./bin/serve.go # serve the brain
+// KB_SEARCH_CMD=... KB_WORKERS=4 KB_PORT=8630 ./bin/serve.go
+//
+// Shebang trick: the first line is a Go `//` comment; when executed, env runs
+// `go run "$0"` so this file doubles as an executable script. The real code
+// lives in the importable package (module path, never a relative import).
+// NOTE: never run `gofmt -w` on this file - it rewrites `//usr/bin/env` to
+// `// usr/...` and breaks the shebang.
+package main
+
+import (
+ "os"
+
+ "github.com/eSlider/2dph/bin/server"
+)
+
+func main() {
+ if env := os.Getenv("KB_ROOT"); env == "" {
+ if wd, err := os.Getwd(); err == nil {
+ os.Setenv("KB_ROOT", wd)
+ }
+ }
+ server.Run()
+}
diff --git a/serve/main.go b/bin/server/server.go
similarity index 86%
rename from serve/main.go
rename to bin/server/server.go
index 5656de8..9e23824 100644
--- a/serve/main.go
+++ b/bin/server/server.go
@@ -1,9 +1,16 @@
-// Package main serves the 2dph brain over HTTP.
+// Package server serves the 2dph brain over HTTP.
//
// Async by design: every request runs on its own goroutine, and CPU-heavy
// searches are serialized through a bounded worker pool (a counting
// semaphore) so N requests can't spawn N Python interpreters at once.
-package main
+//
+// Used by bin/serve.go which is a self-executing shebang script:
+//
+// ///usr/bin/env go run "$0" "$@"; exit
+// package main
+// import "github.com/eSlider/2dph/bin/server"
+// func main() { server.Run() }
+package server
import (
"context"
@@ -115,10 +122,14 @@ func (b *brainSearcher) Search(ctx context.Context, query string, limit int) ([]
return out, nil
}
-func main() {
+// Run starts the HTTP server. Reads env: KB_SEARCH_CMD (default bin/kb/search,
+// relative to the repo root given by KB_ROOT), KB_WORKERS (default 4), KB_PORT
+// (default 8630).
+func Run() {
+ root := os.Getenv("KB_ROOT")
searchPath := os.Getenv("KB_SEARCH_CMD")
if searchPath == "" {
- searchPath = filepath.Join("bin", "kb", "search")
+ searchPath = filepath.Join(root, "bin", "kb", "search")
}
workers := 4
if raw := os.Getenv("KB_WORKERS"); raw != "" {
@@ -140,4 +151,4 @@ func main() {
if err := http.ListenAndServe(addr, handler); err != nil {
log.Fatal(err)
}
-}
\ No newline at end of file
+}
diff --git a/serve/main_test.go b/bin/server/server_test.go
similarity index 92%
rename from serve/main_test.go
rename to bin/server/server_test.go
index 8c31ab7..da5641e 100644
--- a/serve/main_test.go
+++ b/bin/server/server_test.go
@@ -1,11 +1,8 @@
-package main
+package server
import (
- "bytes"
"context"
"encoding/json"
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"sync"
@@ -55,10 +52,6 @@ func (f *fakeSearcher) count() int {
return f.calls
}
-func newTestServer(s Searcher, workers int) http.Handler {
- return NewServer(s, workers)
-}
-
func get(t *testing.T, h http.Handler, path string) (int, []byte) {
t.Helper()
req := httptest.NewRequest(http.MethodGet, path, nil)
@@ -68,7 +61,7 @@ func get(t *testing.T, h http.Handler, path string) (int, []byte) {
}
func TestHealth(t *testing.T) {
- h := newTestServer(&fakeSearcher{}, 1)
+ h := NewServer(&fakeSearcher{}, 1)
code, body := get(t, h, "/health")
if code != http.StatusOK {
t.Fatalf("health code = %d, want 200", code)
@@ -83,7 +76,7 @@ func TestHealth(t *testing.T) {
}
func TestSearchMissingQuery(t *testing.T) {
- h := newTestServer(&fakeSearcher{}, 1)
+ h := NewServer(&fakeSearcher{}, 1)
if code, _ := get(t, h, "/search"); code != http.StatusBadRequest {
t.Fatalf("code = %d, want 400", code)
}
@@ -93,7 +86,7 @@ func TestSearchReturnsSearcherResult(t *testing.T) {
fs := &fakeSearcher{callback: func(q string, limit int) ([]byte, error) {
return []byte(`{"query":"` + q + `","count":1,"results":[{"id":"x"}]}`), nil
}}
- h := newTestServer(fs, 1)
+ h := NewServer(fs, 1)
code, body := get(t, h, "/search?q=matrix")
if code != http.StatusOK {
t.Fatalf("code = %d, want 200", code)
@@ -113,7 +106,7 @@ func TestSearchReturnsSearcherResult(t *testing.T) {
func TestSearchConcurrencyBounded(t *testing.T) {
// 8 parallel requests on a 3-worker pool: at most 3 concurrent searches.
fs := &fakeSearcher{delay: 20 * time.Millisecond}
- h := newTestServer(fs, 3)
+ h := NewServer(fs, 3)
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
@@ -142,7 +135,7 @@ func TestSearchConcurrencyBounded(t *testing.T) {
}
func TestSearchRejectsBadLimit(t *testing.T) {
- h := newTestServer(&fakeSearcher{}, 1)
+ h := NewServer(&fakeSearcher{}, 1)
if code, _ := get(t, h, "/search?q=x&n=hundred"); code != http.StatusBadRequest {
t.Fatalf("code = %d, want 400", code)
}
@@ -171,7 +164,4 @@ func TestSearchTimeout(t *testing.T) {
case <-time.After(500 * time.Millisecond):
t.Fatal("request hung after context cancellation")
}
- _ = io.Discard
- _ = bytes.MinRead
- _ = fmt.Sprintf
-}
\ No newline at end of file
+}
diff --git a/tools/__init__.py b/bin/tools/__init__.py
similarity index 100%
rename from tools/__init__.py
rename to bin/tools/__init__.py
diff --git a/tools/ci/test_semver_bump.py b/bin/tools/ci/test_semver_bump.py
similarity index 100%
rename from tools/ci/test_semver_bump.py
rename to bin/tools/ci/test_semver_bump.py
diff --git a/tools/crmfacts.py b/bin/tools/crmfacts.py
similarity index 100%
rename from tools/crmfacts.py
rename to bin/tools/crmfacts.py
diff --git a/bin/tools/gitimport.py b/bin/tools/gitimport.py
new file mode 100644
index 0000000..6c71780
--- /dev/null
+++ b/bin/tools/gitimport.py
@@ -0,0 +1,117 @@
+"""gitimport - parse `git log` output and turn commits into brain leafs.
+
+Pure, testable functions. Field grammar (see bin/git/import):
+
+ git log --no-merges --name-only \
+ --format='%x1e%H%x1f%an%x1f%ae%x1f%aI%x1f%s'
+
+ 0x1e = record separator, 0x1f = field separator.
+ Files: newline-separated lines following each record's subject.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+
+REC_SEP = "\x1e"
+FIELD_SEP = "\x1f"
+
+
+@dataclass
+class Commit:
+ sha: str
+ author: str
+ email: str
+ date: str
+ subject: str
+ files: list[str] = field(default_factory=list)
+
+ def leaf_text(self, repo: str) -> str:
+ head = f"commit {self.sha[:12]} in {repo} — {self.subject}"
+ body = [head, f"Author: {self.author} <{self.email}>", f"Date: {self.date}"]
+ if self.files:
+ body.append("Changing: " + ", ".join(self.files))
+ return "\n".join(body)
+
+
+def parse_log(text: str) -> list[Commit]:
+ """Parse `git log` output into Commit records.
+
+ Records are separated by 0x1e. A record is fields joined by 0x1f,
+ followed by optional newline-separated file paths inside the next
+ segment (git emits blank line + files after each record).
+ """
+ commits: list[Commit] = []
+ # field records and file lists alternate; simpler: split on REC_SEP,
+ # each chunk = header line, possibly followed by newline + files.
+ for chunk in text.split(REC_SEP):
+ chunk = chunk.strip("\n")
+ if not chunk:
+ continue
+ lines = chunk.split("\n", 1)
+ header = lines[0].split(FIELD_SEP)
+ if len(header) < 5:
+ continue
+ sha, author, email, date, subject = header[:5]
+ files = [ln.strip() for ln in lines[1].splitlines() if ln.strip()] if len(lines) > 1 else []
+ commits.append(Commit(sha=sha, author=author, email=email,
+ date=date, subject=subject, files=files))
+ return commits
+
+
+def commits_to_leafs(commits: list[Commit], repo: str) -> list[dict]:
+ """Map commits to the leaf shape bin/kb/index expects (source/repo/...)."""
+ out: list[dict] = []
+ for c in commits:
+ out.append({
+ "source": f"{repo}@{c.sha}",
+ "repo": repo,
+ "heading": f"commit {c.sha[:12]} — {c.subject}",
+ "text": c.leaf_text(repo),
+ "type": "commit",
+ "status": "current",
+ "related": ",".join(c.files),
+ })
+ return out
+
+
+GIT_SCHEMA = (
+ "CREATE NODE TABLE IF NOT EXISTS Commit (id STRING, repo STRING, subject STRING, "
+ "author STRING, email STRING, date STRING, PRIMARY KEY(id))",
+ "CREATE NODE TABLE IF NOT EXISTS Person (id STRING, name STRING, email STRING, PRIMARY KEY(id))",
+ "CREATE REL TABLE IF NOT EXISTS HAS_VERSION (FROM File TO Commit)",
+ "CREATE REL TABLE IF NOT EXISTS AUTHORED (FROM Commit TO Person)",
+)
+
+
+def ensure_git_schema(conn) -> None:
+ for stmt in GIT_SCHEMA:
+ conn.execute(stmt)
+
+
+def index_commits(conn, commits: list[Commit], repo: str) -> int:
+ """Write Commit/File/Person nodes + edges, one per commit (idempotent by sha)."""
+ ensure_git_schema(conn)
+ for c in commits:
+ conn.execute(
+ "MERGE (c:Commit {id:$sha}) SET c.repo=$repo, c.subject=$subject, "
+ "c.author=$author, c.email=$email, c.date=$date",
+ parameters={"sha": c.sha, "repo": repo, "subject": c.subject,
+ "author": c.author, "email": c.email, "date": c.date},
+ )
+ conn.execute(
+ "MERGE (p:Person {id:$email}) SET p.name=$name, p.email=$email",
+ parameters={"email": c.email, "name": c.author},
+ )
+ conn.execute("MATCH (c:Commit {id:$sha}), (p:Person {id:$email}) "
+ "MERGE (c)-[:AUTHORED]->(p)",
+ parameters={"sha": c.sha, "email": c.email})
+ for path in c.files:
+ conn.execute(
+ "MERGE (f:File {id:$fid}) SET f.path=$path, f.repo=$repo",
+ parameters={"fid": f"{repo}:{path}", "path": path, "repo": repo},
+ )
+ conn.execute("MATCH (f:File {id:$fid}), (c:Commit {id:$sha}) "
+ "MERGE (f)-[:HAS_VERSION]->(c)",
+ parameters={"fid": f"{repo}:{path}", "sha": c.sha})
+ return len(commits)
\ No newline at end of file
diff --git a/tools/kblib.py b/bin/tools/kblib.py
similarity index 81%
rename from tools/kblib.py
rename to bin/tools/kblib.py
index 33debf5..4d40dd4 100644
--- a/tools/kblib.py
+++ b/bin/tools/kblib.py
@@ -22,7 +22,17 @@
ROOT_INFO = "info"
CONF_CONFIRMED = "confirmed"
-VAR = Path(__file__).resolve().parents[1] / "var"
+def _repo_root() -> Path:
+ p = Path(__file__).resolve().parent
+ while True:
+ if (p / "var").is_dir() or (p / ".git").is_dir() or (p / "pyproject.toml").is_file():
+ return p
+ if p.parent == p:
+ return Path(__file__).resolve().parents[2]
+ p = p.parent
+
+
+VAR = _repo_root() / "var"
DB_PATH = VAR / "kb.lbug"
@@ -68,6 +78,19 @@ def init_schema(conn: ladybug.Connection) -> None:
conn.execute(
"CREATE REL TABLE IF NOT EXISTS RUNS_ON (FROM Leaf TO Host)"
)
+ conn.execute(
+ "CREATE NODE TABLE IF NOT EXISTS Commit (id STRING, repo STRING, subject STRING, "
+ "author STRING, email STRING, date STRING, PRIMARY KEY(id))"
+ )
+ conn.execute(
+ "CREATE NODE TABLE IF NOT EXISTS Person (id STRING, name STRING, email STRING, PRIMARY KEY(id))"
+ )
+ conn.execute(
+ "CREATE REL TABLE IF NOT EXISTS HAS_VERSION (FROM File TO Commit)"
+ )
+ conn.execute(
+ "CREATE REL TABLE IF NOT EXISTS AUTHORED (FROM Commit TO Person)"
+ )
def leaf_id(text: str, source: str) -> str:
@@ -109,6 +132,18 @@ def create_fts_and_vector(conn: ladybug.Connection, force: bool = False) -> None
pass
+def drop_indexes(conn: ladybug.Connection) -> None:
+ """Drop FTS + vector indexes so bulk MERGEs don't corrupt them.
+
+ Ladybug's FTS index goes inconsistent when rows are inserted while the
+ index exists ("document for node offset N is missing during delete").
+ Importers that add many leafs must drop indexes first, write, then
+ recreate via create_fts_and_vector().
+ """
+ conn.execute("DROP INDEX IF EXISTS Leaf.Leaf_fts")
+ conn.execute("DROP INDEX IF EXISTS Leaf.Leaf_vec")
+
+
def query_fts(conn: ladybug.Connection, text: str, limit: int = 10) -> list[dict]:
r = conn.execute(
"CALL QUERY_FTS_INDEX('Leaf', 'id', $q) "
diff --git a/tools/mdleaves.py b/bin/tools/mdleaves.py
similarity index 100%
rename from tools/mdleaves.py
rename to bin/tools/mdleaves.py
diff --git a/tools/semver.py b/bin/tools/semver.py
similarity index 100%
rename from tools/semver.py
rename to bin/tools/semver.py
diff --git a/tools/test_crm_facts.py b/bin/tools/test_crm_facts.py
similarity index 100%
rename from tools/test_crm_facts.py
rename to bin/tools/test_crm_facts.py
diff --git a/bin/tools/test_gitgraph.py b/bin/tools/test_gitgraph.py
new file mode 100644
index 0000000..a5a19eb
--- /dev/null
+++ b/bin/tools/test_gitgraph.py
@@ -0,0 +1,71 @@
+import os
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+import kblib # noqa: E402
+import gitimport # noqa: E402
+
+SAMPLE = (
+ "\x1e" + "a1b2c3d" + "\x1f" + "Ada Lovelace" + "\x1f" + "ada@example.com"
+ + "\x1f" + "2026-08-10T12:00:00+01:00" + "\x1f" + "feat: first commit"
+ + "\n\nREADME.md\nsrc/main.c\n"
+)
+
+COMMIT_PERSON_SCHEMA = (
+ "CREATE NODE TABLE IF NOT EXISTS Commit (id STRING, repo STRING, subject STRING, "
+ "author STRING, email STRING, date STRING, PRIMARY KEY(id))"
+)
+PERSON_SCHEMA = (
+ "CREATE NODE TABLE IF NOT EXISTS Person (id STRING, name STRING, email STRING, PRIMARY KEY(id))"
+)
+HAS_VERSION_SCHEMA = "CREATE REL TABLE IF NOT EXISTS HAS_VERSION (FROM File TO Commit)"
+AUTHORED_SCHEMA = "CREATE REL TABLE IF NOT EXISTS AUTHORED (FROM Commit TO Person)"
+
+
+class GitGraphTest(unittest.TestCase):
+ def setUp(self):
+ self.dir = tempfile.mkdtemp()
+ self.dbpath = os.path.join(self.dir, "kb.lbug")
+ self.db, self.conn = kblib.connect(self.dbpath, read_only=False)
+ kblib.init_schema(self.conn)
+ self.conn.execute(COMMIT_PERSON_SCHEMA)
+ self.conn.execute(PERSON_SCHEMA)
+ self.conn.execute(HAS_VERSION_SCHEMA)
+ self.conn.execute(AUTHORED_SCHEMA)
+
+ def tearDown(self):
+ self.conn.close()
+ self.db.close()
+
+ def test_index_commits_creates_nodes_and_edges(self):
+ cs = gitimport.parse_log(SAMPLE)
+ gitimport.index_commits(self.conn, cs, "sample-repo")
+ rp = self.conn.execute("MATCH (p:Person) RETURN p.name, p.email").get_all()
+ self.assertEqual([tuple(r) for r in rp], [("Ada Lovelace", "ada@example.com")])
+ rc = self.conn.execute("MATCH (c:Commit) RETURN c.id, c.repo").get_all()
+ self.assertEqual(len(rc), 1)
+ self.assertEqual(rc[0][1], "sample-repo")
+ # File -[:HAS_VERSION]-> Commit -[:AUTHORED]-> Person
+ rf = self.conn.execute(
+ "MATCH (f:File)-[:HAS_VERSION]->(c:Commit)-[:AUTHORED]->(p:Person) "
+ "RETURN f.path, c.id, p.email").get_all()
+ paths = sorted(r[0] for r in rf)
+ self.assertEqual(paths, ["README.md", "src/main.c"])
+ self.assertTrue(all(r[2] == "ada@example.com" for r in rf))
+
+ def test_index_commits_idempotent(self):
+ cs = gitimport.parse_log(SAMPLE)
+ gitimport.index_commits(self.conn, cs, "sample-repo")
+ gitimport.index_commits(self.conn, cs, "sample-repo")
+ n = self.conn.execute("MATCH (c:Commit) RETURN count(*)").get_all()[0][0]
+ self.assertEqual(n, 1)
+ p = self.conn.execute("MATCH (p:Person) RETURN count(*)").get_all()[0][0]
+ self.assertEqual(p, 1)
+
+
+if __name__ == "__main__":
+ unittest.main()
\ No newline at end of file
diff --git a/bin/tools/test_gitimport.py b/bin/tools/test_gitimport.py
new file mode 100644
index 0000000..14d8883
--- /dev/null
+++ b/bin/tools/test_gitimport.py
@@ -0,0 +1,57 @@
+import sys
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+import gitimport # noqa: E402
+
+SAMPLE = (
+ "\x1e" + "a1b2c3d" + "\x1f" + "Ada Lovelace" + "\x1f" + "ada@example.com"
+ + "\x1f" + "2026-08-10T12:00:00+01:00" + "\x1f" + "feat: first commit"
+ + "\n\nREADME.md\nsrc/main.c\n"
+ + "\x1e" + "e4f5a6b" + "\x1f" + "Bob Babbage" + "\x1f" + "bob@example.com"
+ + "\x1f" + "2026-08-11T09:30:00+01:00" + "\x1f" + "fix: typo"
+ + "\n\ndocs/notes.md"
+)
+
+
+class GitparseTest(unittest.TestCase):
+ def test_parses_records(self):
+ cs = gitimport.parse_log(SAMPLE)
+ self.assertEqual(len(cs), 2)
+
+ def test_parses_commit_fields(self):
+ cs = gitimport.parse_log(SAMPLE)
+ c = cs[0]
+ self.assertEqual(c.sha, "a1b2c3d")
+ self.assertEqual(c.author, "Ada Lovelace")
+ self.assertEqual(c.email, "ada@example.com")
+ self.assertEqual(c.date, "2026-08-10T12:00:00+01:00")
+ self.assertEqual(c.subject, "feat: first commit")
+
+ def test_parses_changed_files(self):
+ cs = gitimport.parse_log(SAMPLE)
+ self.assertEqual(cs[0].files, ["README.md", "src/main.c"])
+ self.assertEqual(cs[1].files, ["docs/notes.md"])
+
+ def test_ignores_empty(self):
+ self.assertEqual(gitimport.parse_log(""), [])
+
+ def test_skip_malformed_record(self):
+ self.assertEqual(gitimport.parse_log("\x1eweird\x1e"), [])
+
+ def test_commit_leaf_shape(self):
+ leafs = gitimport.commits_to_leafs(gitimport.parse_log(SAMPLE), "sample-repo")
+ self.assertEqual(len(leafs), 2)
+ lf = leafs[0]
+ self.assertEqual(lf["type"], "commit")
+ self.assertEqual(lf["repo"], "sample-repo")
+ self.assertEqual(lf["source"], "sample-repo@a1b2c3d")
+ self.assertIn("Ada Lovelace", lf["text"])
+ self.assertIn("README.md", lf["related"])
+ self.assertIn("feat: first commit", lf["heading"])
+
+
+if __name__ == "__main__":
+ unittest.main()
\ No newline at end of file
diff --git a/tools/test_kblib.py b/bin/tools/test_kblib.py
similarity index 100%
rename from tools/test_kblib.py
rename to bin/tools/test_kblib.py
diff --git a/tools/web-search/__init__.py b/bin/tools/web-search/__init__.py
similarity index 100%
rename from tools/web-search/__init__.py
rename to bin/tools/web-search/__init__.py
diff --git a/tools/web-search/fixtures/healthy.json b/bin/tools/web-search/fixtures/healthy.json
similarity index 100%
rename from tools/web-search/fixtures/healthy.json
rename to bin/tools/web-search/fixtures/healthy.json
diff --git a/tools/web-search/fixtures/throttled.json b/bin/tools/web-search/fixtures/throttled.json
similarity index 100%
rename from tools/web-search/fixtures/throttled.json
rename to bin/tools/web-search/fixtures/throttled.json
diff --git a/tools/web-search/test_websearch.py b/bin/tools/web-search/test_websearch.py
similarity index 100%
rename from tools/web-search/test_websearch.py
rename to bin/tools/web-search/test_websearch.py
diff --git a/tools/web-search/websearch.py b/bin/tools/web-search/websearch.py
similarity index 100%
rename from tools/web-search/websearch.py
rename to bin/tools/web-search/websearch.py
diff --git a/tools/yamlout.py b/bin/tools/yamlout.py
similarity index 100%
rename from tools/yamlout.py
rename to bin/tools/yamlout.py
diff --git a/bin/watch/watch.go b/bin/watch/watch.go
new file mode 100644
index 0000000..c7669c0
--- /dev/null
+++ b/bin/watch/watch.go
@@ -0,0 +1,105 @@
+// Package watch polls corpus directories for changes and re-runs bin/kb/index.
+//
+// Port of the former bin/kb-watch bash script to an importable, testable Go
+// package. Polls file mtimes (no inotify deps); cheap and reliable.
+package watch
+
+import (
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Options controls the polling loop. Zero value uses defaults.
+type Options struct {
+ Dirs []string
+ Interval time.Duration
+ // IndexCmd is the kb/index command template. %s is replaced by the repo
+ // root (from KB_ROOT). Defaults to `python3 /bin/kb/index`.
+ IndexCmd string
+}
+
+// Run blocks forever polling Dirs (defaults: KB_WATCH_DIRS or /corpus) every
+// Interval (default 30s) and re-indexing when files change. KB_ROOT names the
+// repo root used to locate bin/kb/index.
+func Run(args []string) {
+ opts := fromEnv(args)
+ root, _ := os.Getwd()
+ if r := os.Getenv("KB_ROOT"); r != "" {
+ root = r
+ }
+ log.Printf("watch: dirs=%v interval=%s root=%s", opts.Dirs, opts.Interval, root)
+ var last string
+ for {
+ if flag := Stamp(opts.Dirs); flag != "" && flag != last {
+ last = flag
+ reindex(opts.IndexCmd, root)
+ }
+ time.Sleep(opts.Interval)
+ }
+}
+
+func fromEnv(args []string) Options {
+ opts := Options{Interval: 30 * time.Second}
+ if raw := os.Getenv("KB_WATCH_INTERVAL"); raw != "" {
+ if n, err := strconv.Atoi(raw); err == nil && n > 0 {
+ opts.Interval = time.Duration(n) * time.Second
+ }
+ }
+ defDirs := "/corpus"
+ if raw := os.Getenv("KB_WATCH_DIRS"); raw != "" {
+ defDirs = raw
+ }
+ if len(args) > 0 {
+ opts.Dirs = args
+ } else {
+ for _, d := range strings.Split(defDirs, " ") {
+ if d != "" {
+ opts.Dirs = append(opts.Dirs, d)
+ }
+ }
+ }
+ pys := os.Getenv("KB_PY")
+ if pys == "" {
+ pys = "python3"
+ }
+ opts.IndexCmd = pys + " /bin/kb/index"
+ return opts
+}
+
+// Stamp returns a rolling fingerprint (newest mtime under dirs) that changes
+// whenever any corpus file is touched. Empty when no files found.
+func Stamp(dirs []string) string {
+ var newest time.Time
+ for _, dir := range dirs {
+ _ = filepath.WalkDir(dir, func(path string, _ os.DirEntry, err error) error {
+ if err != nil {
+ return nil
+ }
+ if info, e := os.Stat(path); e == nil && info.ModTime().After(newest) {
+ newest = info.ModTime()
+ }
+ return nil
+ })
+ }
+ if newest.IsZero() {
+ return ""
+ }
+ return strconv.FormatInt(newest.UnixNano(), 10)
+}
+
+func reindex(template, root string) {
+ cmd := strings.ReplaceAll(template, "", root)
+ parts := strings.Fields(cmd)
+ c := exec.Command(parts[0], parts[1:]...)
+ out, err := c.CombinedOutput()
+ if err != nil {
+ log.Printf("watch: index failed: %v\n%s", err, out)
+ } else {
+ log.Printf("watch: re-indexed")
+ }
+}
diff --git a/bin/watch/watch_test.go b/bin/watch/watch_test.go
new file mode 100644
index 0000000..2292def
--- /dev/null
+++ b/bin/watch/watch_test.go
@@ -0,0 +1,49 @@
+package watch
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+func TestStampChangesWhenFileTouched(t *testing.T) {
+ dir := t.TempDir()
+ a := filepath.Join(dir, "a.md")
+ if err := os.WriteFile(a, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ s1 := Stamp([]string{dir})
+ if s1 == "" {
+ t.Fatal("stamp empty for a dir with a file")
+ }
+ time.Sleep(10 * time.Millisecond)
+ if err := os.WriteFile(a, []byte("y"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if s2 := Stamp([]string{dir}); s2 == s1 {
+ t.Fatal("stamp did not change after the file was modified")
+ }
+}
+
+func TestStampEmptyForMissingDir(t *testing.T) {
+ if s := Stamp([]string{filepath.Join(t.TempDir(), "nope")}); s != "" {
+ t.Fatalf("stamp = %q, want empty for missing dir", s)
+ }
+}
+
+func TestFromEnvDefaults(t *testing.T) {
+ t.Setenv("KB_WATCH_INTERVAL", "")
+ t.Setenv("KB_WATCH_DIRS", "")
+ t.Setenv("KB_PY", "")
+ opts := fromEnv(nil)
+ if len(opts.Dirs) == 0 || opts.Dirs[0] != "/corpus" {
+ t.Fatalf("default dirs = %v, want [/corpus]", opts.Dirs)
+ }
+ if opts.Interval != 30*time.Second {
+ t.Fatalf("default interval = %s, want 30s", opts.Interval)
+ }
+ if opts.IndexCmd == "" {
+ t.Fatal("default index cmd is empty")
+ }
+}
diff --git a/bin/web/search b/bin/web/search
index 865ba15..87984b8 100755
--- a/bin/web/search
+++ b/bin/web/search
@@ -25,7 +25,7 @@ import urllib.parse
import urllib.request
from pathlib import Path
-TOOLS = Path(__file__).resolve().parents[1].parent / "tools"
+TOOLS = Path(__file__).resolve().parents[1] / "tools"
sys.path.insert(0, str(TOOLS))
sys.path.insert(0, str(TOOLS / "web-search"))
diff --git a/docs/crm-associations-proof.md b/docs/crm-associations-proof.md
index f1452d6..c80bc85 100644
--- a/docs/crm-associations-proof.md
+++ b/docs/crm-associations-proof.md
@@ -27,7 +27,7 @@ written to the brain under `root=facts` by `bin/facts/crm`.
## Gates after fix
-- `uv run python -m unittest discover -s tools -t .` → 26 tests OK
+- `uv run python -m unittest discover -s bin/tools -t .` → 26 tests OK
- `bin/facts/audit self` + `bin/facts/audit db` → ok
- `bin/kb/eval` → recall@5 = 1.0
-- `go test ./...` (serve/) → ok
\ No newline at end of file
+- `go test ./...` (bin/server + bin/watch) → ok
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..2aa0757
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module github.com/eSlider/2dph
+
+go 1.25
\ No newline at end of file
diff --git a/serve/go.mod b/serve/go.mod
deleted file mode 100644
index 808dbab..0000000
--- a/serve/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module github.com/eSlider/2dph/serve
-
-go 1.25
\ No newline at end of file
From 678a1d1dbaf51c54462f730cab030d9917e3c365 Mon Sep 17 00:00:00 2001
From: Andriy Oblivantsev
Date: Tue, 11 Aug 2026 21:57:38 +0100
Subject: [PATCH 03/19] feat(mail): full Gmail+OnlyOffice sync, import, and
brain indexing
- bin/mail/sync.go: async Go sync engine (8 workers, paginated Gmail via
API + OnlyOffice IMAP); Gmail attachments key off body.attachmentId, not
MIME partId; ICS sidecars Latin-1->UTF-8 normalized (TestICSToMarkdownNormalizesLatin1)
- bin/mail/import: message.json -> markdown; PDFs via pdftotext -layout
fast path with docling subprocess fallback for the ~5% textless files
- bin/mail/index_mail: fresh-rebuild indexer (repo corpus + mail) avoiding
ladybug WAL corruption on bulk-insert into indexed DBs; split from import
- bin/kb/index: keep FTS/VECTOR indexes across incremental runs (drop+recreate
leaves stale backing tables killing the vector index)
- docs: README/PLAN/AGENTS cover the mail pipeline
Result: 17,835 messages -> 28,918 info leafs, FTS+HNSW healthy.
---
AGENTS.md | 23 +-
PLAN.md | 18 +-
README.md | 9 +
bin/kb/index | 14 +-
bin/mail/import | 450 ++++++
bin/mail/index_mail | 136 ++
bin/mail/sync.go | 25 +
bin/mail/sync/cli.go | 154 ++
bin/mail/sync/gmail.go | 356 +++++
bin/mail/sync/ics.go | 258 ++++
bin/mail/sync/onlyoffice.go | 222 +++
bin/mail/sync/sync.go | 384 +++++
bin/mail/sync/sync_test.go | 240 ++++
bin/mail/sync/types.go | 46 +
bin/tools/mailconv.py | 148 ++
bin/tools/test_mailconv.py | 123 ++
go.mod | 7 +-
go.sum | 14 +
pyproject.toml | 2 +
uv.lock | 2631 +++++++++++++++++++++++++++++++++--
20 files changed, 5117 insertions(+), 143 deletions(-)
create mode 100755 bin/mail/import
create mode 100755 bin/mail/index_mail
create mode 100755 bin/mail/sync.go
create mode 100644 bin/mail/sync/cli.go
create mode 100644 bin/mail/sync/gmail.go
create mode 100644 bin/mail/sync/ics.go
create mode 100644 bin/mail/sync/onlyoffice.go
create mode 100644 bin/mail/sync/sync.go
create mode 100644 bin/mail/sync/sync_test.go
create mode 100644 bin/mail/sync/types.go
create mode 100644 bin/tools/mailconv.py
create mode 100644 bin/tools/test_mailconv.py
create mode 100644 go.sum
diff --git a/AGENTS.md b/AGENTS.md
index 0a6e2b4..415aa04 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -39,14 +39,35 @@ bin/ self-describing tools bin/{subject}/{method} (shebang)
bin/serve.go async Go HTTP server entry (self-executing go run shebang)
bin/watch/ corpus watcher Go package (mtimes, no inotify deps)
bin/server/ async Go HTTP server (goroutines, bounded worker pool)
+bin/mail/ mail pipeline: sync (Go), import (md), index_mail (rebuild)
bin/tools/ vendored python libs behind bin/* (kblib, yamlout, websearch)
bin/docker-entrypoint container entrypoint (brain index|search|serve|watch)
compose.yaml docker composition (root level, not docker/)
Dockerfile multi-stage: python deps + static Go binaries
-var/ kb.lbug, caches (gitignored)
+var/ kb.lbug, var/mail/*, caches (gitignored)
.venv/ ladybug + model2vec + mistune
```
+## Mail pipeline
+
+```bash
+bin/mail/sync.go --source onlyoffice,gmail --workers 8 --out var/mail # raw message.json + attachments
+bin/mail/import --from-raw var/mail # message.json → message.md (convert only)
+bin/mail/index_mail # rebuild brain incl. all mail (fresh DB)
+```
+
+- `sync` (Go) downloads messages + attachments; Gmail uses paginated list +
+ `body.attachmentId` (not partId) for attachments.
+- `import` converts body + attachments to markdown. PDFs use poppler
+ `pdftotext -layout` fast path (~15ms); textless/scanned PDFs fall back to
+ docling (isolated subprocess — its native onnx can segfault the parent).
+ Conversion never touches the brain DB (crash safety).
+- `index_mail` always rebuilds from scratch (repo corpus + mail). Ladybug
+ corrupts its WAL when brand-new leafs are bulk-inserted while FTS/vector
+ indexes exist; a fresh DB with indexes created last is the only safe path.
+ Keep conversion + indexing separate so a conversion crash can't leave the
+ DB mid-transaction.
+
## Tools
```bash
diff --git a/PLAN.md b/PLAN.md
index ec0771a..b6ad21b 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -93,8 +93,24 @@ Common props on every node/edge: `root`, `confidence`, `evidence[]`, `how`,
- OQ1: mutually-contradicting evidence — how to resolve (authority weighting,
temporal freshness, audit adjudication).
-- OQ2: OCR pipeline for pdfs/images/docs (late phase).
+- OQ2: OCR pipeline for pdfs/images/docs — mostly solved: poppler pdftotext
+ fast-path for born-digital PDFs, docling fallback for the ~5% textless ones.
- OQ3: optional duckdb-md layer for `SELECT … FORMAT MARKDOWN` export/write-back.
+- OQ4: YAML-first storage for leafs — deferred: JSON is ~10x faster to
+ serialize and unambiguous; YAML only where humans edit files.
+
+## Mail pipeline (done)
+
+1. `bin/mail/sync.go` (Go, 8 workers) — paginated Gmail/OnlyOffice download.
+ Gmail attachments key off `body.attachmentId`, not MIME `partId`.
+2. `bin/mail/import --from-raw` — message.json → message.md; PDFs via
+ `pdftotext -layout` (~15ms) with docling subprocess fallback; ICS sidecars
+ Latin-1→UTF-8 normalized.
+3. `bin/mail/index_mail` — fresh rebuild (repo corpus + mail) because ladybug
+ corrupts its WAL on bulk-insert into an already-indexed DB. Conversion and
+ indexing stay separate for crash safety.
+4. Result: 17,835 messages → 28,918 info leafs, FTS + HNSW healthy, searchable
+ via `bin/kb/search`.
## CI/CD pipeline (D15)
diff --git a/README.md b/README.md
index 2d4d8fc..ab9901e 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,15 @@ bin/kb/stats # index health
bin/kb/eval # recall@5 gate
```
+Mail is a first-class corpus (retrievable through the same search):
+
+```bash
+bin/mail/sync.go --source onlyoffice,gmail --workers 8 --out var/mail # raw sync (Go)
+bin/mail/import --from-raw var/mail # JSON → markdown
+bin/mail/index_mail # rebuild brain incl. mail
+bin/kb/search "Mietwagen Nürnberg invoice" # now answers from mail
+```
+
## Storage
- **LadybugDB** — single `var/kb.lbug`, Cypher property graph, HNSW + BM25
diff --git a/bin/kb/index b/bin/kb/index
index 4e22f70..6bf7cf5 100755
--- a/bin/kb/index
+++ b/bin/kb/index
@@ -116,11 +116,13 @@ def main(argv: list[str]) -> int:
db, conn = connect(DB_PATH, read_only=False)
init_schema(conn)
- if not (a.rebuild or _already_indexed(conn)):
- create_fts_and_vector(conn, force=True)
+ # Keep the FTS/VECTOR indexes in place across incremental runs: ladybug's
+ # DROP INDEX leaves the backing tables registered on migrated DBs, so a
+ # drop+recreate silently kills the vector index. Only create when missing.
embed = embedder()
done, total = index_leafs(conn, leafs, embed, a.limit)
- create_fts_and_vector(conn, force=(done > 0 or a.rebuild))
+ if a.rebuild or not _has_indexes(conn):
+ create_fts_and_vector(conn, force=True)
s = stats(conn)
conn.close()
db.close()
@@ -130,9 +132,11 @@ def main(argv: list[str]) -> int:
return 0
-def _already_indexed(conn) -> bool:
+def _has_indexes(conn) -> bool:
try:
- return conn.execute("MATCH (l:Leaf) RETURN count(*)").get_all()[0][0] > 0
+ rows = conn.execute("CALL SHOW_INDEXES() RETURN *").get_all()
+ names = {row[1] for row in rows if row[0] == "Leaf"}
+ return "id" in names and "Leaf_vec" in names
except Exception:
return False
diff --git a/bin/mail/import b/bin/mail/import
new file mode 100755
index 0000000..044834c
--- /dev/null
+++ b/bin/mail/import
@@ -0,0 +1,450 @@
+#!/usr/bin/env python3
+"""mail/import - pull OnlyOffice mails into var/mail/ as markdown.
+
+ bin/mail/import --from-raw var/mail convert Go-synced message.json to md
+ bin/mail/import import newest inbox messages
+ bin/mail/import --folder sent import sent folder
+ bin/mail/import --since 2026-01-01 only messages after a date
+ bin/mail/import --limit 50 cap messages per run
+ bin/mail/import --no-attachments body only, skip attachment conversion
+ bin/mail/import --ocr OCR scanned PDFs/images via docling
+ bin/mail/import --dry-run list messages without writing anything
+
+Writes one directory per message: var/mail/{folder}/{message_id}/
+ message.md frontmatter + markdown body
+ attachments/ raw attachment files (zips unpacked to _unpacked/)
+ attachments/*.md converted attachment content
+
+Indexing is a separate step (bin/mail/index_mail): conversion can crash in
+native docling and must not leave the brain DB mid-transaction.
+
+Requires ONLYOFFICE_URL/USER/PASS in .env (or env). Idempotent: a message
+already present (message.md exists) is skipped unless --force.
+"""
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import re
+import subprocess
+import sys
+import time
+import urllib.parse
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+sys.path.insert(0, str(ROOT / "bin" / "tools"))
+
+from mailconv import ( # noqa: E402
+ ARCHIVE_SUFFIXES,
+ IMAGE_SUFFIXES,
+ LEGACY_OFFICE_SUFFIXES,
+ TEXT_SUFFIXES,
+ html_to_markdown,
+ is_convertible,
+ normalize_markdown,
+ subject_to_filename,
+ zip_extract_safe,
+)
+
+import requests # noqa: E402
+
+FOLDER_IDS = {"inbox": 1, "sent": 2, "drafts": 3, "trash": 4, "spam": 5}
+DEFAULT_LIMIT = 25
+
+
+def load_env() -> dict:
+ env = {k: v for k, v in os.environ.items()}
+ envfile = ROOT / ".env"
+ if envfile.exists():
+ for line in envfile.read_text().splitlines():
+ line = line.strip()
+ if not line or line.startswith("#") or "=" not in line:
+ continue
+ k, _, v = line.partition("=")
+ env.setdefault(k.strip(), v.strip().strip("\"'"))
+ url = env.get("ONLYOFFICE_URL") or env.get("OO_URL")
+ user = env.get("ONLYOFFICE_USER") or env.get("OO_USER")
+ password = env.get("ONLYOFFICE_PASS") or env.get("OO_PASSWORD")
+ missing = [n for n, v in (("ONLYOFFICE_URL", url), ("ONLYOFFICE_USER", user),
+ ("ONLYOFFICE_PASS", password)) if not v]
+ if missing:
+ sys.exit(f"mail/import: missing {', '.join(missing)} (need .env or env)")
+ return {"url": url.rstrip("/"), "user": user, "password": password}
+
+
+class OOClient:
+ def __init__(self, conf: dict):
+ self.base = conf["url"]
+ self.session = requests.Session()
+ self.token = None
+ self._login(conf)
+
+ def _login(self, conf: dict) -> None:
+ r = self.session.post(f"{self.base}/api/2.0/authentication.json",
+ json={"userName": conf["user"], "password": conf["password"], "type": 0},
+ timeout=30)
+ r.raise_for_status()
+ body = r.json()
+ self.token = (body.get("response") or {}).get("token", "")
+ if not self.token:
+ sys.exit("mail/import: authentication failed (empty token)")
+
+ def _headers(self) -> dict:
+ return {"Authorization": f"Bearer {self.token}", "Accept": "application/json"}
+
+ def get(self, path: str, params: dict | None = None):
+ r = self.session.get(f"{self.base}{path}", params=params, headers=self._headers(), timeout=30)
+ r.raise_for_status()
+ return r.json()
+
+ def list_messages(self, folder: int, page: int = 1, count: int = DEFAULT_LIMIT) -> list[dict]:
+ data = self.get("/api/2.0/mail/messages",
+ params={"folder": folder, "page": page, "count": count})
+ return data.get("response", [])
+
+ def get_message(self, message_id: str) -> dict:
+ data = self.get(f"/api/2.0/mail/messages/{message_id}")
+ return data.get("response", {})
+
+ def download_attachment(self, attach_id, dest: Path) -> bool:
+ """Download one attachment via the portal session cookie (.ashx handler)."""
+ url = f"{self.base}/addons/mail/httphandlers/download.ashx?attachid={attach_id}"
+ r = self.session.get(url, timeout=60)
+ if r.status_code != 200:
+ return False
+ dest.parent.mkdir(parents=True, exist_ok=True)
+ dest.write_bytes(r.content)
+ return True
+
+
+def folder_id(name: str) -> int:
+ if name in FOLDER_IDS:
+ return FOLDER_IDS[name]
+ if name.isdigit():
+ return int(name)
+ sys.exit(f"mail/import: unknown folder '{name}' (use {', '.join(FOLDER_IDS)})")
+
+
+def safe_attachment_name(att: dict) -> str:
+ name = att.get("fileName") or att.get("storedName") or "attachment"
+ name = re.sub(r"[^\w.\- ]+", "_", name)
+ return name
+
+
+def convert_file_to_md(path: Path, ocr: bool) -> str | None:
+ """Convert one attachment file to markdown text; None when not convertible."""
+ suffix = path.suffix.lower()
+ if suffix in TEXT_SUFFIXES:
+ return normalize_markdown(path.read_text(encoding="utf-8", errors="replace"))
+ if suffix in (".docx", ".pptx", ".xlsx", ".html", ".htm", ".epub", ".eml", ".msg"):
+ try:
+ from markitdown import MarkItDown
+ md = MarkItDown()
+ result = md.convert(str(path))
+ return normalize_markdown(result.text_content)
+ except Exception as e:
+ return f"\n\n"
+ if suffix == ".pdf":
+ return _convert_pdf(path, ocr)
+ if suffix in IMAGE_SUFFIXES and ocr:
+ return _convert_pdf(path, ocr)
+ if suffix in LEGACY_OFFICE_SUFFIXES:
+ return _convert_legacy(path)
+ if suffix in ARCHIVE_SUFFIXES:
+ return None # handled by caller (unpack + recurse)
+ return None
+
+
+def _convert_pdf(path: Path, ocr: bool) -> str:
+ """Convert one PDF to markdown.
+
+ Fast path: poppler's pdftotext (-layout) extracts exact text from
+ born-digital PDFs in ~15ms vs docling's 1-3s. Only textless PDFs (scanned
+ pages, layout-heavy) fall back to docling, which runs isolated in a
+ subprocess because its native onnx/RT-DETR has segfaulted the main process.
+ """
+ text = _pdf_fast_text(path)
+ if ocr or text is None or not text.strip():
+ return _convert_pdf_docling(path, ocr)
+ return normalize_markdown(text)
+
+
+def _pdf_fast_text(path: Path) -> str | None:
+ """pdftotext -layout; None when poppler is unavailable (or the PDF has no text layer)."""
+ try:
+ proc = subprocess.run(
+ ["pdftotext", "-layout", str(path), "-"],
+ capture_output=True, timeout=60)
+ except (OSError, subprocess.TimeoutExpired):
+ return None
+ if proc.returncode != 0:
+ return None
+ return proc.stdout.decode("utf-8", errors="replace")
+
+
+def _convert_pdf_docling(path: Path, ocr: bool) -> str:
+ try:
+ proc = subprocess.run(
+ [sys.executable, os.path.abspath(__file__), "--pdf-worker", str(path),
+ "--ocr" if ocr else "--no-ocr"],
+ capture_output=True, text=True, timeout=600)
+ except subprocess.TimeoutExpired:
+ return "\n\n"
+ if proc.returncode != 0:
+ tail = proc.stderr.strip().splitlines()[-3:]
+ return f"\n\n"
+ return proc.stdout
+
+
+def _pdf_worker(path: Path, ocr: bool) -> None:
+ """docling worker entry: prints converted markdown on stdout, exits non-zero on error."""
+ try:
+ from docling.document_converter import DocumentConverter, PdfFormatOption
+ from docling.datamodel.pipeline_options import PdfPipelineOptions
+ opts = PdfPipelineOptions()
+ opts.do_ocr = bool(ocr)
+ opts.do_table_structure = True
+ conv = DocumentConverter(format_options={"pdf": PdfFormatOption(pipeline_options=opts)})
+ res = conv.convert(str(path))
+ sys.stdout.write(normalize_markdown(res.document.export_to_markdown()))
+ sys.exit(0)
+ except Exception as e:
+ # errors/stacktraces to stderr; the caller only reports a one-liner
+ print(f"pdf-worker: {e}", file=sys.stderr)
+ import traceback
+ traceback.print_exc(file=sys.stderr)
+ sys.exit(1)
+
+
+def _convert_legacy(path: Path) -> str:
+ """Legacy .doc/.xls/.ppt -> md via pandoc (installed) or a stub."""
+ try:
+ out = subprocess.run(["pandoc", str(path), "-t", "markdown"],
+ capture_output=True, text=True, timeout=120)
+ if out.returncode == 0 and out.stdout.strip():
+ return normalize_markdown(out.stdout)
+ except (FileNotFoundError, subprocess.TimeoutExpired):
+ pass
+ return f"\n\n"
+
+
+def write_message_md(msg: dict, folder: str, out_dir: Path, target_dir: Path | None = None) -> Path:
+ import yaml
+ body_html = msg.get("htmlBody") or ""
+ body_text = msg.get("textBody") or ""
+ body_md = ""
+ if body_html.strip():
+ body_md = html_to_markdown(body_html)
+ elif body_text.strip():
+ body_md = normalize_markdown(body_text)
+ # accept both OnlyOffice (receivedDate) and Go-sync (receivedAt) date keys
+ date = msg.get("receivedDate") or msg.get("receivedAt") or ""
+ if date and not isinstance(date, str):
+ date = str(date)
+ meta = {
+ "id": msg.get("id"),
+ "source": msg.get("source"),
+ "folder": folder,
+ "subject": msg.get("subject", ""),
+ "from": msg.get("from", ""),
+ "to": msg.get("to", ""),
+ "cc": msg.get("cc", ""),
+ "date": date,
+ "has_attachments": bool(msg.get("hasAttachments")),
+ "mime_message_id": msg.get("mimeMessageId", ""),
+ "calendar_uid": msg.get("calendarUid", ""),
+ "type": "mail",
+ }
+ meta = {k: v for k, v in meta.items() if v not in (None, "")}
+ frontmatter = "---\n" + yaml.safe_dump(meta, sort_keys=False, allow_unicode=True).strip() + "\n---\n"
+ content = f"{frontmatter}\n# {meta.get('subject','')}\n\n{body_md}".strip() + "\n"
+ if target_dir is not None:
+ msg_dir = target_dir
+ else:
+ msg_dir = out_dir / folder / str(meta.get("id"))
+ msg_dir.mkdir(parents=True, exist_ok=True)
+ md_path = msg_dir / "message.md"
+ md_path.write_text(content, encoding="utf-8")
+ return md_path
+
+
+def convert_attachments(msg: dict, msg_dir: Path, ocr: bool) -> list[dict]:
+ """Download + convert each attachment; returns [{name, md, raw}] summaries.
+
+ raw file keeps the API storedName (unique hash, avoids collisions); the
+ markdown is named after the friendly fileName when available.
+
+ In --from-raw mode attachments are already on disk (Go sync wrote them;
+ .ics already has a structured .md sidecar). Files with an existing .md
+ sidecar are left as-is, only unconverted raws are converted here.
+ """
+ out: list[dict] = []
+ atts = msg.get("attachments") or []
+ att_dir = msg_dir / "attachments"
+ for att in atts:
+ aid = att.get("fileId")
+ display = safe_attachment_name(att)
+ stored = att.get("storedName")
+ raw_name = safe_attachment_name({"storedName": stored}) if stored else display
+ raw = att_dir / raw_name
+ if aid and not raw.exists() and OOCLIENT is not None:
+ if not OOCLIENT.download_attachment(aid, raw):
+ out.append({"name": display, "md": "\n\n", "raw": str(raw)})
+ continue
+ if not raw.exists():
+ out.append({"name": display, "md": "\n\n", "raw": str(raw)})
+ continue
+ md_stem = Path(display).stem or raw.stem
+ md_path = att_dir / f"{md_stem}.md"
+ # Go sync pre-wrote structured .md for .ics; keep it.
+ if not md_path.exists():
+ md_text = _convert_att_recursive(raw, ocr)
+ md_path.write_text(f"# Attachment: {display}\n\n{md_text}\n", encoding="utf-8")
+ else:
+ md_text = md_path.read_text(encoding="utf-8", errors="replace")
+ out.append({"name": display, "md": md_text, "raw": str(raw), "md_file": str(md_path)})
+ return out
+
+
+def _convert_att_recursive(path: Path, ocr: bool) -> str:
+ if path.suffix.lower() in ARCHIVE_SUFFIXES:
+ parts: list[str] = []
+ unpack = path.parent / "_unpacked" / path.stem
+ files = zip_extract_safe(path, unpack)
+ for f in files:
+ sub = _convert_att_recursive(f, ocr)
+ if sub and sub.strip():
+ parts.append(f"## {f.name}\n\n{sub}")
+ return "\n\n".join(parts) if parts else "\n\n"
+ text = convert_file_to_md(path, ocr)
+ return text or "\n\n"
+
+
+# module-level client for attachment downloads in convert_attachments
+OOCLIENT: OOClient | None = None
+
+
+def convert_one(msg_dir: Path, full: dict, folder: str, out_root: Path,
+ ocr: bool, no_attachments: bool, target_dir: Path | None = None) -> dict:
+ """Write message.md + convert attachments for one message dict.
+
+ Works for both live API messages and the Go-sync message.json shape
+ (source field optional; attachments read from attachments/ dir).
+ target_dir overrides the derived path (used by --from-raw where the
+ directory layout is authoritative, not the message folder field).
+ """
+ mid = str(full.get("id"))
+ write_message_md(full, folder, out_root, target_dir=target_dir)
+ converted: list[dict] = []
+ if not no_attachments:
+ converted = convert_attachments(full, msg_dir, ocr)
+ return {"id": mid, "subject": full.get("subject", ""),
+ "date": full.get("receivedDate", "") or full.get("receivedAt", ""),
+ "attachments": len(converted)}
+
+
+def main(argv: list[str]) -> int:
+ global OOCLIENT
+ p = argparse.ArgumentParser(description="pull OnlyOffice mails to var/mail as markdown")
+ p.add_argument("--folder", default="inbox", help="inbox|sent|drafts|trash|spam or numeric id")
+ p.add_argument("--limit", type=int, default=DEFAULT_LIMIT, help="max messages per run")
+ p.add_argument("--offset", type=int, default=0, help="skip N messages")
+ p.add_argument("--since", default="", help="only messages received after YYYY-MM-DD")
+ p.add_argument("--id", action="append", default=[], help="import specific message id (repeatable)")
+ p.add_argument("--from-raw", default="",
+ help="convert Go-synced dirs (var/mail///message.json) to markdown")
+ p.add_argument("--no-attachments", action="store_true", help="skip attachment download+convert")
+ p.add_argument("--ocr", action="store_true", help="OCR scanned PDFs/images via docling")
+ p.add_argument("--force", action="store_true", help="re-import even if message.md exists")
+ p.add_argument("--dry-run", action="store_true", help="list messages, write nothing")
+ p.add_argument("--json", action="store_true")
+ p.add_argument("--pdf-worker", default="", help=argparse.SUPPRESS)
+ p.add_argument("--no-ocr", action="store_true", help=argparse.SUPPRESS)
+ a = p.parse_args(argv)
+
+ if a.pdf_worker:
+ _pdf_worker(Path(a.pdf_worker), ocr=not a.no_ocr)
+ return 0
+
+ conf = load_env()
+ fid = folder_id(a.folder)
+ out_root = ROOT / "var" / "mail"
+ summary: list[dict] = []
+ if a.from_raw:
+ OOCLIENT = None
+ raw_root = Path(a.from_raw)
+ for msg_dir in sorted(raw_root.rglob("message.json")):
+ mid = msg_dir.parent.name
+ entry = {"id": mid, "subject": "", "date": "",
+ "attachments": 0, "skipped": False}
+ md_path = msg_dir.parent / "message.md"
+ if md_path.exists() and not a.force:
+ entry["skipped"] = True
+ summary.append(entry)
+ continue
+ if a.dry_run:
+ entry["skipped"] = "dry-run"
+ summary.append(entry)
+ continue
+ full = json.loads(msg_dir.read_text(encoding="utf-8"))
+ entry.update(convert_one(msg_dir.parent, full, full.get("folder") or a.folder,
+ raw_root, a.ocr, a.no_attachments,
+ target_dir=msg_dir.parent))
+ summary.append(entry)
+ else:
+ OOCLIENT = OOClient(conf)
+ if a.id:
+ messages = [{"id": i} for i in a.id]
+ else:
+ page = 1
+ messages = []
+ want = a.offset + a.limit
+ while len(messages) < want:
+ count = min(DEFAULT_LIMIT, want - len(messages))
+ chunk = OOCLIENT.list_messages(fid, page=page, count=count)
+ if not chunk:
+ break
+ messages.extend(chunk)
+ if len(chunk) < count:
+ break
+ page += 1
+ messages = messages[a.offset:a.offset + a.limit]
+ if a.since:
+ messages = [m for m in messages
+ if (m.get("receivedDate") or "") >= a.since]
+
+ for m in messages:
+ mid = str(m.get("id"))
+ entry = {"id": mid, "subject": m.get("subject", ""),
+ "date": m.get("receivedDate", ""), "attachments": 0, "skipped": False}
+ msg_dir = out_root / a.folder / mid
+ md_path = msg_dir / "message.md"
+ if md_path.exists() and not a.force:
+ entry["skipped"] = True
+ summary.append(entry)
+ continue
+ if a.dry_run:
+ entry["skipped"] = "dry-run"
+ summary.append(entry)
+ continue
+ full = OOCLIENT.get_message(mid)
+ entry.update(convert_one(msg_dir, full, a.folder, out_root, a.ocr, a.no_attachments,
+ target_dir=msg_dir))
+ summary.append(entry)
+ if a.json:
+ print(json.dumps(summary, ensure_ascii=False, indent=2))
+ else:
+ imported = [e for e in summary if not e["skipped"]]
+ print(f"mail/import: folder={a.folder} checked={len(summary)} "
+ f"imported={len(imported)} (skipped={sum(e['skipped'] is True for e in summary)})")
+ for e in summary:
+ flag = "skip" if e["skipped"] is True else ("dry" if e["skipped"] == "dry-run" else "ok ")
+ print(f" [{flag}] {e['id']} {e['date'][:10]} {e['subject'][:60]}"
+ f" (atts={e['attachments']})")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))
diff --git a/bin/mail/index_mail b/bin/mail/index_mail
new file mode 100755
index 0000000..4d44eec
--- /dev/null
+++ b/bin/mail/index_mail
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+"""mail/index_mail - rebuild the brain with every markdown under var/mail.
+
+Ladybug corrupts its WAL when brand-new leafs are bulk-inserted while the
+FTS/VECTOR indexes already exist, so indexing ALWAYS runs as a fresh rebuild
+(repo corpus + var/mail), matching the proven-safe `kb/index --rebuild` path.
+Conversion and indexing stay separate: conversion can crash in native docling
+and must not leave the brain DB mid-transaction.
+
+ bin/mail/index_mail rebuild the index incl. all mail
+ bin/mail/index_mail --dry-run count without writing
+ bin/mail/index_mail --limit N cap messages included
+ bin/mail/index_mail --since D only messages dated >= D (YYYY-MM-DD)
+"""
+from __future__ import annotations
+
+import argparse
+import json
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+sys.path.insert(0, str(ROOT / "bin" / "tools"))
+
+from kblib import DB_PATH, VAR, connect, create_fts_and_vector, init_schema, stats, upsert_leaf # noqa: E402
+from mdleaves import read_markdown, to_all, walk_markdown # noqa: E402
+
+
+def msg_date(md: Path) -> str:
+ j = md.parent / "message.json"
+ try:
+ d = json.loads(j.read_text(encoding="utf-8"))
+ return (d.get("receivedDate") or d.get("receivedAt") or "")[:10]
+ except Exception:
+ return ""
+
+
+def mail_leafs(limit: int, since: str, repo: str = "ooMail") -> list[dict]:
+ root = ROOT / "var" / "mail"
+ mds = sorted(root.rglob("message.md"))
+ if since:
+ mds = [m for m in mds if msg_date(m) >= since]
+ if limit:
+ mds = mds[:limit]
+ leafs: list[dict] = []
+ for md in mds:
+ files = [md] + sorted((md.parent / "attachments").glob("*.md"))
+ for f in files:
+ if not f.exists():
+ continue
+ for lf in to_all(read_markdown(f), f, repo=repo):
+ lf["source"] = f"ooMail:{md.parent.name}:{f.name}"
+ lf["how"] = "mail/import"
+ leafs.append(lf)
+ return leafs
+
+
+def main(argv: list[str]) -> int:
+ p = argparse.ArgumentParser(description="rebuild the brain incl. all mail")
+ p.add_argument("--dry-run", action="store_true", help="count only, write nothing")
+ p.add_argument("--limit", type=int, default=0, help="cap messages included")
+ p.add_argument("--since", default="", help="only messages dated >= YYYY-MM-DD")
+ p.add_argument("--json", action="store_true")
+ a = p.parse_args(argv)
+
+ mail = mail_leafs(a.limit, a.since)
+ if a.dry_run:
+ print(f"mail/index_mail: {len(mail)} mail leafs would be indexed")
+ return 0
+
+ # Fresh rebuild: delete DB, index repo corpus + mail, create indexes once
+ # at the end. Never insert into an already-indexed DB (WAL corruption).
+ VAR.mkdir(exist_ok=True)
+ if DB_PATH.exists():
+ DB_PATH.unlink()
+
+ corpus = _load_corpus()
+ leafs = corpus + mail
+
+ db, conn = connect(DB_PATH, read_only=False)
+ init_schema(conn)
+ embed = _embedder()
+ done, total = _index_leafs(conn, leafs, embed)
+ create_fts_and_vector(conn, force=True)
+ s = stats(conn)
+ conn.close()
+ db.close()
+
+ result = {"indexed": done, "corpus_total": total, "mail_leafs": len(mail),
+ **{k: v for k, v in s.items() if k in ("total", "by_root")}}
+ print(json.dumps(result, indent=2) if a.json else
+ f"mail/index_mail: indexed {done}/{total} leafs (mail={len(mail)}); db total {s['total']}")
+ return 0
+
+
+CORPUS_DEFAULTS = ["README.md", "PLAN.md", "AGENTS.md", "docs", "skills"]
+
+
+def _load_corpus() -> list[dict]:
+ files: list[Path] = []
+ for entry in CORPUS_DEFAULTS:
+ p = ROOT / entry
+ if p.is_file():
+ files.append(p)
+ elif p.is_dir():
+ files.extend(walk_markdown(p))
+ leafs: list[dict] = []
+ for path in files:
+ try:
+ leafs.extend(to_all(read_markdown(path), path, repo="eSlider/2dph"))
+ except OSError as e:
+ print(f"mail/index_mail: skip {path}: {e}", file=sys.stderr)
+ return leafs
+
+
+def _index_leafs(conn, leafs: list[dict], embed_fn) -> tuple[int, int]:
+ count = 0
+ for lf in leafs:
+ query = f"{lf['heading']}\n\n{lf['text']}"
+ emb = embed_fn(lf["text"]) if lf["text"] else None
+ upsert_leaf(conn, text=query, root="info", confidence="confirmed",
+ source=lf["source"], source_rev="mail" if lf.get("how") == "mail/import" else "working-tree",
+ how=lf.get("how", "kb/index"), loc=lf["source"], type_=lf.get("type", "reference"),
+ embedding=emb)
+ count += 1
+ return count, len(leafs)
+
+
+def _embedder():
+ from model2vec import StaticModel
+ model = StaticModel.from_pretrained("minishlab/potion-multilingual-128M")
+ return lambda text: model.encode([text])[0].astype(float).tolist()
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))
diff --git a/bin/mail/sync.go b/bin/mail/sync.go
new file mode 100755
index 0000000..b6d7f05
--- /dev/null
+++ b/bin/mail/sync.go
@@ -0,0 +1,25 @@
+//usr/bin/env go run "$0" "$@"; exit
+// bin/mail/sync.go - async download of OnlyOffice and Gmail mail to var/mail/.
+//
+// ./bin/mail/sync.go --source onlyoffice,gmail --limit 50 --workers 8
+// ./bin/mail/sync.go --source gmail --force
+// ./bin/mail/sync.go --dry-run
+//
+// Writes raw message.json + attachments under var/mail///; run
+// bin/mail/import --from-raw afterwards to convert everything to markdown.
+//
+// Shebang trick: first line is a Go `//` comment; the real code lives in the
+// importable package (module path, never a relative import).
+// NOTE: never run `gofmt -w` on this file - it rewrites `//usr/bin/env` to
+// `// usr/...` and breaks the shebang.
+package main
+
+import (
+ "os"
+
+ "github.com/eSlider/2dph/bin/mail/sync"
+)
+
+func main() {
+ os.Exit(sync.Main(os.Args[1:]))
+}
diff --git a/bin/mail/sync/cli.go b/bin/mail/sync/cli.go
new file mode 100644
index 0000000..fab2022
--- /dev/null
+++ b/bin/mail/sync/cli.go
@@ -0,0 +1,154 @@
+// Package synccmd wires the sync library to a CLI: reads .env, parses flags,
+// picks sources, prints stats. Kept separate from the library so unit tests
+// don't depend on os.Args/env.
+package sync
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+// CLIConfig is a superset of SyncConfig plus flag parsing results.
+type CLIConfig struct {
+ Sync SyncConfig
+ Env string // .env path; default /.env
+ Sources string
+ Help bool
+}
+
+// ParseCLI reads os.Args into a CLIConfig. Exit codes: 0 ok, 2 usage.
+func ParseCLI(args []string) (CLIConfig, int, error) {
+ fs := flag.NewFlagSet("mail/sync", flag.ContinueOnError)
+ var (
+ env = fs.String("env", "", ".env file (default: /.env)")
+ out = fs.String("out", "", "var/mail root (default: /var/mail)")
+ workers = fs.Int("workers", 4, "concurrent downloads")
+ limit = fs.Int("limit", 0, "max messages per source (0 = all)")
+ offset = fs.Int("offset", 0, "skip first N messages per source")
+ force = fs.Bool("force", false, "overwrite existing message.json + attachments")
+ dryRun = fs.Bool("dry-run", false, "list message counts without writing")
+ srcs = fs.String("source", "onlyoffice", "comma list: onlyoffice,gmail (default onlyoffice)")
+ help = fs.Bool("help", false, "usage")
+ )
+ fs.SetOutput(os.Stderr)
+ if err := fs.Parse(args); err != nil {
+ return CLIConfig{}, 2, err
+ }
+ if *help || fs.NArg() > 0 {
+ return CLIConfig{Help: true}, 0, nil
+ }
+ wd, err := os.Getwd()
+ if err != nil {
+ return CLIConfig{}, 2, err
+ }
+ if *env == "" {
+ *env = filepath.Join(wd, ".env")
+ }
+ if *out == "" {
+ *out = filepath.Join(wd, "var", "mail")
+ }
+ envVars := readEnv(*env)
+ cfg := SyncConfig{
+ Out: *out,
+ Workers: *workers,
+ Limit: *limit,
+ Offset: *offset,
+ Force: *force,
+ DryRun: *dryRun,
+ Policy: RetryPolicy{},
+ }
+ cli := CLIConfig{Sync: cfg, Env: *env, Sources: *srcs}
+ for _, s := range strings.Split(*srcs, ",") {
+ switch strings.TrimSpace(s) {
+ case "onlyoffice":
+ u := pick(envVars["ONLYOFFICE_URL"], envVars["OO_URL"])
+ user := pick(envVars["ONLYOFFICE_USER"], envVars["OO_USER"])
+ pass := pick(envVars["ONLYOFFICE_PASS"], envVars["OO_PASSWORD"])
+ if u == "" || user == "" || pass == "" {
+ return CLIConfig{}, 2, fmt.Errorf("onlyoffice source needs ONLYOFFICE_URL/USER/PASS in %s", *env)
+ }
+ cfg.OO = &OOConfig{URL: u, User: user, Password: pass}
+ case "gmail":
+ home, _ := os.UserHomeDir()
+ cfg.Gmail = &GmailCredentials{
+ CredentialsPath: filepath.Join(home, ".gmail-mcp", "credentials.json"),
+ KeysPath: filepath.Join(home, ".gmail-mcp", "gcp-oauth.keys.json"),
+ }
+ default:
+ return CLIConfig{}, 2, fmt.Errorf("unknown source %q", s)
+ }
+ }
+ cli.Sync = cfg
+ return cli, 0, nil
+}
+
+// Main is the CLI entry: returns process exit code.
+func Main(args []string) int {
+ cli, code, err := ParseCLI(args)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "mail/sync:", err)
+ return code
+ }
+ if cli.Help {
+ fmt.Fprintln(os.Stderr, "usage: bin/mail/sync.go [--source onlyoffice,gmail] [--limit N] [--offset N] [--workers N] [--force] [--dry-run]")
+ return 0
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 6*time.Hour)
+ defer cancel()
+ start := time.Now()
+ stats, err := Run(ctx, cli.Sync)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "mail/sync:", err)
+ return 1
+ }
+ if cli.Sync.DryRun {
+ fmt.Printf("mail/sync: dry-run checked=%d (no writes)\n", stats.Checked)
+ return 0
+ }
+ fmt.Printf("mail/sync: checked=%d new=%d skipped=%d failed=%d in %s\n",
+ stats.Checked, stats.New, stats.Skipped, stats.Failed, time.Since(start).Round(time.Millisecond))
+ if stats.Failed > 0 {
+ return 1
+ }
+ return 0
+}
+
+// readEnv parses KEY=VALUE lines (ignoring comments) with KEY=PATH override.
+func readEnv(path string) map[string]string {
+ out := map[string]string{}
+ b, err := os.ReadFile(path)
+ if err != nil {
+ return out
+ }
+ for _, line := range strings.Split(string(b), "\n") {
+ line = strings.TrimSpace(line)
+ if line == "" || strings.HasPrefix(line, "#") || !strings.Contains(line, "=") {
+ continue
+ }
+ k, v, _ := strings.Cut(line, "=")
+ out[strings.TrimSpace(k)] = strings.Trim(strings.TrimSpace(v), "\"'")
+ }
+ // env overrides file
+ for _, kv := range os.Environ() {
+ k, v, ok := strings.Cut(kv, "=")
+ if !ok {
+ continue
+ }
+ if strings.HasPrefix(k, "ONLYOFFICE_") || strings.HasPrefix(k, "OO_") {
+ out[k] = v
+ }
+ }
+ return out
+}
+
+func pick(a, b string) string {
+ if a != "" {
+ return a
+ }
+ return b
+}
diff --git a/bin/mail/sync/gmail.go b/bin/mail/sync/gmail.go
new file mode 100644
index 0000000..40495a3
--- /dev/null
+++ b/bin/mail/sync/gmail.go
@@ -0,0 +1,356 @@
+package sync
+
+import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+// GmailCredentials holds the OAuth files produced by the gmail MCP
+// (@gongrzhe/server-gmail-autoauth-mcp) auto-auth flow.
+type GmailCredentials struct {
+ CredentialsPath string // ~/.gmail-mcp/credentials.json
+ KeysPath string // ~/.gmail-mcp/gcp-oauth.keys.json
+ User string // fixed: the authed account
+}
+
+// gmailToken is the JSON shape of credentials.json + refresh response.
+type gmailToken struct {
+ AccessToken string `json:"access_token"`
+ RefreshToken string `json:"refresh_token"`
+ Expiry int64 `json:"expiry_date"` // ms epoch
+}
+
+type gmailKeys struct {
+ Installed *gmailKeyBlock `json:"installed"`
+ Web *gmailKeyBlock `json:"web"`
+}
+type gmailKeyBlock struct {
+ ClientID string `json:"client_id"`
+ ClientSecret string `json:"client_secret"`
+}
+
+// GmailClient talks to the Gmail REST API using the OAuth refresh token from
+// ~/.gmail-mcp/. Token is refreshed lazily with a mutex-guarded cache.
+type GmailClient struct {
+ creds GmailCredentials
+ client *http.Client
+ mu chan struct{}
+ token *gmailToken
+ user string
+}
+
+func NewGmailClient(creds GmailCredentials) (*GmailClient, error) {
+ if creds.CredentialsPath == "" {
+ home, _ := os.UserHomeDir()
+ creds.CredentialsPath = filepath.Join(home, ".gmail-mcp", "credentials.json")
+ creds.KeysPath = filepath.Join(home, ".gmail-mcp", "gcp-oauth.keys.json")
+ }
+ g := &GmailClient{
+ creds: creds,
+ client: &http.Client{Timeout: 60 * time.Second},
+ mu: make(chan struct{}, 1),
+ }
+ g.mu <- struct{}{}
+ return g, nil
+}
+
+// accessToken returns a fresh bearer token, refreshing via the Google token
+// endpoint when the cached one is missing or about to expire.
+func (g *GmailClient) accessToken(ctx context.Context) (string, error) {
+ select {
+ case <-g.mu:
+ case <-ctx.Done():
+ return "", ctx.Err()
+ }
+ defer func() { g.mu <- struct{}{} }()
+ if g.token != nil && g.token.AccessToken != "" && g.token.Expiry > time.Now().UnixMilli()+300_000 {
+ return g.token.AccessToken, nil
+ }
+ return g.refreshLocked(ctx)
+}
+
+func (g *GmailClient) refreshLocked(ctx context.Context) (string, error) {
+ cred, err := os.ReadFile(g.creds.CredentialsPath)
+ if err != nil {
+ return "", fmt.Errorf("read gmail credentials %s: %w", g.creds.CredentialsPath, err)
+ }
+ var t gmailToken
+ if err := json.Unmarshal(cred, &t); err != nil {
+ return "", fmt.Errorf("parse gmail credentials: %w", err)
+ }
+ if t.RefreshToken == "" {
+ return "", errors.New("gmail credentials.json has no refresh_token (run the gmail MCP auth flow)")
+ }
+ keys, err := os.ReadFile(g.creds.KeysPath)
+ if err != nil {
+ return "", fmt.Errorf("read gmail keys %s: %w", g.creds.KeysPath, err)
+ }
+ var k gmailKeys
+ if err := json.Unmarshal(keys, &k); err != nil {
+ return "", fmt.Errorf("parse gmail keys: %w", err)
+ }
+ block := k.Installed
+ if block == nil {
+ block = k.Web
+ }
+ if block == nil {
+ return "", errors.New("gmail gcp-oauth.keys.json has no installed/web block")
+ }
+
+ form := url.Values{}
+ form.Set("client_id", block.ClientID)
+ form.Set("client_secret", block.ClientSecret)
+ form.Set("refresh_token", t.RefreshToken)
+ form.Set("grant_type", "refresh_token")
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://oauth2.googleapis.com/token",
+ strings.NewReader(form.Encode()))
+ if err != nil {
+ return "", err
+ }
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ resp, err := g.client.Do(req)
+ if err != nil {
+ return "", fmt.Errorf("gmail token refresh: %w", err)
+ }
+ defer resp.Body.Close()
+ body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
+ if resp.StatusCode != http.StatusOK {
+ var e struct {
+ Error string `json:"error"`
+ Desc string `json:"error_description"`
+ }
+ _ = json.Unmarshal(body, &e)
+ if e.Error == "invalid_grant" {
+ return "", fmt.Errorf("gmail OAuth token invalid/expired - re-auth via: npx -y @gongrzhe/server-gmail-autoauth-mcp auth (uses ~/.gmail-mcp)")
+ }
+ return "", fmt.Errorf("gmail token refresh status %d: %s", resp.StatusCode, truncate(string(body), 300))
+ }
+ var out struct {
+ AccessToken string `json:"access_token"`
+ ExpiresIn int64 `json:"expires_in"`
+ }
+ if err := json.Unmarshal(body, &out); err != nil {
+ return "", fmt.Errorf("gmail token refresh parse: %w", err)
+ }
+ g.token = &gmailToken{
+ AccessToken: out.AccessToken,
+ RefreshToken: t.RefreshToken,
+ Expiry: time.Now().UnixMilli() + out.ExpiresIn*1000,
+ }
+ return out.AccessToken, nil
+}
+
+// ListIDs returns message ids matching q, walking nextPageToken up to maxIDs
+// (0 = unlimited). Thread-level pagination via the messages.list endpoint.
+func (g *GmailClient) ListIDs(ctx context.Context, q string, maxIDs int, pageToken string) (ids []string, next string, err error) {
+ for {
+ params := url.Values{}
+ params.Set("q", q)
+ params.Set("maxResults", "100")
+ if pageToken != "" {
+ params.Set("pageToken", pageToken)
+ }
+ var out struct {
+ Messages []struct {
+ ID string `json:"id"`
+ } `json:"messages"`
+ NextPageToken string `json:"nextPageToken"`
+ }
+ if err := g.getJSON(ctx, "/gmail/v1/users/me/messages?"+params.Encode(), &out); err != nil {
+ return nil, "", err
+ }
+ for _, m := range out.Messages {
+ ids = append(ids, m.ID)
+ if maxIDs > 0 && len(ids) >= maxIDs {
+ return ids, out.NextPageToken, nil
+ }
+ }
+ if out.NextPageToken == "" {
+ break
+ }
+ pageToken = out.NextPageToken
+ }
+ return ids, "", nil
+}
+
+// GetMessage fetches a message in format=full and normalizes it.
+func (g *GmailClient) GetMessage(ctx context.Context, id string) (*Message, error) {
+ var raw struct {
+ ID string `json:"id"`
+ ThreadID string `json:"threadId"`
+ InternalDate string `json:"internalDate"` // ms epoch string
+ Payload gmailPart
+ }
+ path := "/gmail/v1/users/me/messages/" + url.PathEscape(id) + "?format=full"
+ if err := g.getJSON(ctx, path, &raw); err != nil {
+ return nil, err
+ }
+ m := &Message{
+ Source: "gmail",
+ ID: raw.ID,
+ Folder: "gmail",
+ }
+ for _, h := range raw.Payload.Headers {
+ switch strings.ToLower(h.Name) {
+ case "subject":
+ m.Subject = h.Value
+ case "from":
+ m.From = h.Value
+ case "to":
+ m.To = h.Value
+ case "cc":
+ m.CC = h.Value
+ case "bcc":
+ m.BCC = h.Value
+ case "message-id":
+ m.MimeMessageID = h.Value
+ case "date":
+ if t, err := time.Parse(time.RFC1123Z, h.Value); err == nil {
+ m.ReceivedAt = t
+ }
+ }
+ }
+ if ms, err := parseMS(raw.InternalDate); err == nil {
+ m.ReceivedAt = ms
+ }
+ m.TextBody, m.HTMLBody, m.Attachments = collectParts(raw.Payload, "root", m.ID, 0)
+ m.HasAttachments = len(m.Attachments) > 0
+ return m, nil
+}
+
+type gmailPart struct {
+ PartID string `json:"partId"`
+ MimeType string `json:"mimeType"`
+ Filename string `json:"filename"`
+ Body gmailBody `json:"body"`
+ Headers []gmailHeader `json:"headers"`
+ Parts []gmailPart `json:"parts"`
+}
+type gmailHeader struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+}
+type gmailBody struct {
+ Size int64 `json:"size"`
+ Data string `json:"data"`
+ AttachmentID string `json:"attachmentId"`
+}
+
+// collectParts walks the MIME tree: text bodies into plain/html, anything with
+// a filename into attachments (returned with base64 ids for later download).
+func collectParts(p gmailPart, mime string, msgID string, depth int) (text, html string, atts []Attachment) {
+ if depth > 16 {
+ return
+ }
+ mt := strings.ToLower(p.MimeType)
+ if p.Filename != "" && mt != "text/plain" && mt != "text/html" {
+ pid := p.PartID
+ if pid == "" {
+ pid = fmt.Sprintf("%d", depth)
+ }
+ // Gmail's attachments API keys off body.attachmentId, not partId.
+ attID := p.Body.AttachmentID
+ if attID == "" {
+ attID = pid
+ }
+ atts = append(atts, Attachment{
+ FileID: msgID + ":" + attID,
+ FileName: p.Filename,
+ StoredName: p.Filename,
+ Size: p.Body.Size,
+ ContentType: p.MimeType,
+ })
+ } else if data, err := base64.URLEncoding.DecodeString(p.Body.Data); err == nil && len(p.Body.Data) > 0 {
+ s := string(data)
+ if mt == "text/html" && html == "" {
+ html = s
+ } else if (mt == "text/plain" || mt == "") && text == "" {
+ text = s
+ }
+ }
+ for _, child := range p.Parts {
+ t, h, a := collectParts(child, mt, msgID, depth+1)
+ if text == "" {
+ text = t
+ }
+ if html == "" {
+ html = h
+ }
+ atts = append(atts, a...)
+ }
+ return
+}
+
+// DownloadAttachment fetches an attachment's bytes from the Gmail API.
+func (g *GmailClient) DownloadAttachment(ctx context.Context, msgID, attID string) ([]byte, error) {
+ // attID format is ":"; the API needs the bare attachment id.
+ partID := attID
+ if i := strings.Index(attID, ":"); i >= 0 {
+ partID = attID[i+1:]
+ }
+ var out struct {
+ Data string `json:"data"`
+ }
+ path := "/gmail/v1/users/me/messages/" + url.PathEscape(msgID) + "/attachments/" + url.PathEscape(partID)
+ if err := g.getJSON(ctx, path, &out); err != nil {
+ return nil, err
+ }
+ return base64.URLEncoding.DecodeString(out.Data)
+}
+
+func (g *GmailClient) getJSON(ctx context.Context, path string, out any) error {
+ tok, err := g.accessToken(ctx)
+ if err != nil {
+ return err
+ }
+ u := "https://gmail.googleapis.com" + path
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Authorization", "Bearer "+tok)
+ resp, err := g.client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ body, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("gmail %s: status %d: %s", path, resp.StatusCode, truncate(string(body), 300))
+ }
+ if out != nil {
+ return json.Unmarshal(body, out)
+ }
+ return nil
+}
+
+func parseMS(s string) (time.Time, error) {
+ if s == "" {
+ return time.Time{}, errors.New("empty")
+ }
+ var ms int64
+ if _, err := fmt.Sscanf(s, "%d", &ms); err != nil {
+ return time.Time{}, err
+ }
+ return time.UnixMilli(ms), nil
+}
+
+func truncate(s string, n int) string {
+ if len(s) <= n {
+ return s
+ }
+ return s[:n] + "…"
+}
+
+var _ = bytes.MinRead
diff --git a/bin/mail/sync/ics.go b/bin/mail/sync/ics.go
new file mode 100644
index 0000000..970c862
--- /dev/null
+++ b/bin/mail/sync/ics.go
@@ -0,0 +1,258 @@
+package sync
+
+import (
+ "fmt"
+ "strings"
+ "time"
+ "unicode/utf8"
+
+ ics "github.com/arran4/golang-ical"
+ "golang.org/x/text/encoding/charmap"
+)
+
+// ICSToMarkdown parses a VCALENDAR/VEVENT payload and renders a compact
+// structured markdown block: what / when / where / organizer / attendees.
+// Returns the raw text when the payload is not a calendar.
+func ICSToMarkdown(data []byte) string {
+ data = normalizeEncoding(data)
+ cal, err := ics.ParseCalendar(strings.NewReader(string(data)))
+ if err != nil {
+ return normalizeMarkdown(string(data))
+ }
+ method := ""
+ for _, p := range cal.CalendarProperties {
+ if p.IANAToken == string(ics.ComponentPropertyMethod) {
+ method = p.Value
+ break
+ }
+ }
+ method = strings.TrimSpace(method)
+ var out []string
+ for _, ev := range cal.Events() {
+ summary := strings.TrimSpace(propValue(ev, ics.ComponentPropertySummary))
+ if summary != "" {
+ out = append(out, "# "+summary)
+ }
+ if when := eventWhen(ev); when != "" {
+ out = append(out, "- **When:** "+when)
+ }
+ if loc := strings.TrimSpace(propValue(ev, ics.ComponentPropertyLocation)); loc != "" {
+ out = append(out, "- **Where:** "+loc)
+ }
+ if desc := strings.TrimSpace(stripHTML(propValue(ev, ics.ComponentPropertyDescription))); desc != "" {
+ out = append(out, "- **What:** "+desc)
+ }
+ if org := propValue(ev, ics.ComponentPropertyOrganizer); org != "" {
+ out = append(out, "- **Organizer:** "+attendeeFmt(org))
+ }
+ for _, a := range ev.Attendees() {
+ cn := strings.TrimSpace(firstParam(a.ICalParameters, "CN"))
+ partstat := string(a.ParticipationStatus())
+ name := cn
+ if name == "" {
+ name = a.Email()
+ }
+ line := name
+ if email := a.Email(); email != "" && email != name {
+ line = name + " <" + email + ">"
+ }
+ if partstat != "" && !strings.EqualFold(partstat, "NEEDS-ACTION") {
+ line += " (" + strings.Title(strings.ToLower(strings.ReplaceAll(partstat, "_", " "))) + ")"
+ }
+ out = append(out, "- **Attendee:** "+line)
+ }
+ }
+ if len(out) == 0 {
+ return normalizeMarkdown(string(data))
+ }
+ if method != "" {
+ out = append([]string{"*Calendar method: " + method + "*"}, out...)
+ }
+ return normalizeMarkdown(strings.Join(out, "\n\n"))
+}
+
+func eventWhen(ev *ics.VEvent) string {
+ start, errStart := ev.GetStartAt()
+ end, errEnd := ev.GetEndAt()
+ // All-day events: golang-ical has dedicated getters.
+ if errStart != nil {
+ if allDay, err := ev.GetAllDayStartAt(); err == nil {
+ start = allDay
+ errStart = nil
+ }
+ }
+ if errEnd != nil {
+ if allDay, err := ev.GetAllDayEndAt(); err == nil {
+ end = allDay
+ errEnd = nil
+ }
+ }
+ if errStart != nil {
+ // Non-IANA TZID (e.g. "W. Europe Standard Time"): parse the raw
+ // property text instead of failing.
+ return rawWhen(ev)
+ }
+ if errEnd != nil || end.Equal(start) {
+ return dtFmt(start)
+ }
+ return dtFmt(start) + " → " + dtFmt(end)
+}
+
+// rawWhen parses DTSTART/DTEND property values that golang-ical cannot resolve
+// because the TZID is not an IANA zone. Formats: 20260812T120000 or 20260812.
+func rawWhen(ev *ics.VEvent) string {
+ start := rawPropValue(ev, ics.ComponentPropertyDtStart)
+ end := rawPropValue(ev, ics.ComponentPropertyDtEnd)
+ if start == "" {
+ return ""
+ }
+ if end == "" || end == start {
+ return rawDTFmt(start)
+ }
+ return rawDTFmt(start) + " → " + rawDTFmt(end)
+}
+
+func rawPropValue(ev *ics.VEvent, prop ics.ComponentProperty) string {
+ p := ev.GetProperty(prop)
+ if p == nil {
+ return ""
+ }
+ return p.Value
+}
+
+// rawDTFmt turns 20260812T120000 into 2026-08-12 12:00; 20260812 into 2026-08-12.
+func rawDTFmt(s string) string {
+ s = strings.TrimSpace(s)
+ if len(s) >= 8 && isDigits(s[:8]) {
+ y, m, d := s[:4], s[4:6], s[6:8]
+ if len(s) > 8 && (s[8] == 'T' || s[8] == 't') && len(s) >= 15 && isDigits(s[9:15]) {
+ h, mi := s[9:11], s[11:13]
+ return fmt.Sprintf("%s-%s-%s %s:%s", y, m, d, h, mi)
+ }
+ return fmt.Sprintf("%s-%s-%s", y, m, d)
+ }
+ return s
+}
+
+func isDigits(s string) bool {
+ for _, c := range s {
+ if c < '0' || c > '9' {
+ return false
+ }
+ }
+ return s != ""
+}
+
+// dtFmt renders a time as local "2006-01-02 15:04" (tz label when meaningful).
+func dtFmt(t time.Time) string {
+ loc := t.Local()
+ label := ""
+ if loc.Location() != time.Local {
+ label = " " + loc.Location().String()
+ }
+ return loc.Format("2006-01-02 15:04") + label
+}
+
+// propertyGetter is satisfied by both *ics.Calendar and *ics.VEvent.
+type propertyGetter interface {
+ GetProperty(ics.ComponentProperty) *ics.IANAProperty
+}
+
+func propValue(ev propertyGetter, prop ics.ComponentProperty) string {
+ p := ev.GetProperty(prop)
+ if p == nil {
+ return ""
+ }
+ return p.Value
+}
+
+func firstParam(params map[string][]string, key string) string {
+ if vs, ok := params[key]; ok && len(vs) > 0 {
+ return vs[0]
+ }
+ return ""
+}
+
+func attendeeFmt(raw string) string {
+ raw = strings.TrimSpace(raw)
+ if i := strings.Index(raw, ":"); i >= 0 {
+ raw = raw[i+1:]
+ }
+ return raw
+}
+
+// stripHTML removes tags and decodes entities from an ics DESCRIPTION that may
+// carry HTML (Outlook/Exchange style), keeping text lines readable.
+func stripHTML(s string) string {
+ if !strings.Contains(s, "<") {
+ return s
+ }
+ lines := strings.Split(s, "\n")
+ for i, l := range lines {
+ var b strings.Builder
+ depth := 0
+ for j := 0; j < len(l); j++ {
+ c := l[j]
+ if c == '<' {
+ if j+1 < len(l) && l[j+1] == '/' {
+ depth--
+ } else {
+ depth++
+ }
+ for j < len(l) && l[j] != '>' {
+ j++
+ }
+ continue
+ }
+ if c == '>' {
+ continue
+ }
+ if depth == 0 {
+ b.WriteByte(c)
+ }
+ }
+ lines[i] = strings.TrimSpace(b.String())
+ }
+ return strings.Join(lines, "\n")
+}
+
+// normalizeMarkdown collapses blank-line runs and strips control chars.
+func normalizeMarkdown(s string) string {
+ s = strings.ReplaceAll(s, "\x00", "")
+ for _, ch := range []string{"\ufeff", "\u200b", "\u034f", "\u00ad", "\u2007", "\u2008", "\u200a", "\u2002"} {
+ s = strings.ReplaceAll(s, ch, "")
+ }
+ lines := strings.Split(s, "\n")
+ var out []string
+ blank := 0
+ for _, l := range lines {
+ if strings.TrimSpace(l) == "" {
+ blank++
+ if blank > 1 {
+ continue
+ }
+ } else {
+ blank = 0
+ }
+ out = append(out, l)
+ }
+ return strings.Join(out, "\n")
+}
+
+// normalizeEncoding re-encodes legacy single-byte text as UTF-8. ICS files
+// exported by some portals are Latin-1 (e.g. "N\xfcrnberg"); golang-ical
+// passes the bytes through, producing invalid UTF-8 in the markdown output.
+// Valid UTF-8 is returned untouched.
+func normalizeEncoding(data []byte) []byte {
+ if utf8.Valid(data) {
+ return data
+ }
+ dec := charmap.ISO8859_1.NewDecoder()
+ out, err := dec.Bytes(data)
+ if err != nil {
+ return data
+ }
+ return out
+}
+
+var _ = fmt.Sprintf // keep fmt import if helpers change
diff --git a/bin/mail/sync/onlyoffice.go b/bin/mail/sync/onlyoffice.go
new file mode 100644
index 0000000..08acc61
--- /dev/null
+++ b/bin/mail/sync/onlyoffice.go
@@ -0,0 +1,222 @@
+package sync
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/cookiejar"
+ "net/url"
+ "strings"
+ "time"
+)
+
+// OOConfig mirrors the .env / environment used by bin/mail/import.
+type OOConfig struct {
+ URL string
+ User string
+ Password string
+}
+
+// OOClient is a minimal OnlyOffice API client: authentication.json for the
+// bearer token plus the session cookie jar required by the .ashx download
+// handler. It mirrors the endpoint contract bin/mail/import already uses.
+type OOClient struct {
+ cfg OOConfig
+ client *http.Client
+ mu chan struct{}
+ token string
+ folderID int
+}
+
+func NewOOClient(cfg OOConfig, folderID int) (*OOClient, error) {
+ jar, err := cookiejar.New(nil)
+ if err != nil {
+ return nil, err
+ }
+ c := &OOClient{
+ cfg: cfg,
+ client: &http.Client{Jar: jar, Timeout: 60 * time.Second},
+ mu: make(chan struct{}, 1),
+ folderID: folderID,
+ }
+ c.mu <- struct{}{}
+ if err := c.authenticate(context.Background()); err != nil {
+ return nil, err
+ }
+ return c, nil
+}
+
+func (o *OOClient) authenticate(ctx context.Context) error {
+ select {
+ case <-o.mu:
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ defer func() { o.mu <- struct{}{} }()
+ body, _ := json.Marshal(map[string]any{
+ "userName": o.cfg.User, "password": o.cfg.Password, "type": 0,
+ })
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost,
+ strings.TrimRight(o.cfg.URL, "/")+"/api/2.0/authentication.json",
+ strings.NewReader(string(body)))
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ resp, err := o.client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ data, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
+ if resp.StatusCode < 200 || resp.StatusCode > 299 {
+ return fmt.Errorf("oo authenticate status %d: %s", resp.StatusCode, truncate(string(data), 200))
+ }
+ var out struct {
+ Response struct {
+ Token string `json:"token"`
+ } `json:"response"`
+ }
+ if err := json.Unmarshal(data, &out); err != nil {
+ return err
+ }
+ if out.Response.Token == "" {
+ return fmt.Errorf("oo authenticate: empty token")
+ }
+ o.token = out.Response.Token
+ return nil
+}
+
+// get performs an authenticated GET and decodes the JSON body into out.
+func (o *OOClient) get(ctx context.Context, path string, out any) error {
+ u := strings.TrimRight(o.cfg.URL, "/") + path
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Authorization", "Bearer "+o.token)
+ req.Header.Set("Accept", "application/json")
+ resp, err := o.client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ data, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
+ if resp.StatusCode < 200 || resp.StatusCode > 299 {
+ return fmt.Errorf("oo %s: status %d: %s", path, resp.StatusCode, truncate(string(data), 300))
+ }
+ if out != nil {
+ return json.Unmarshal(data, out)
+ }
+ return nil
+}
+
+// ooMessage mirrors the OnlyOffice mail message JSON (subset we need).
+type ooMessage struct {
+ ID int `json:"id"`
+ Subject string `json:"subject"`
+ From string `json:"from"`
+ To string `json:"to"`
+ CC string `json:"cc"`
+ BCC string `json:"bcc"`
+ ReceivedDate string `json:"receivedDate"`
+ HTMLBody string `json:"htmlBody"`
+ TextBody string `json:"textBody"`
+ HasAttachments bool `json:"hasAttachments"`
+ MimeMessageID string `json:"mimeMessageId"`
+ Attachments []struct {
+ FileID int `json:"fileId"`
+ FileName string `json:"fileName"`
+ StoredName string `json:"storedName"`
+ Size int64 `json:"size"`
+ ContentType string `json:"contentType"`
+ } `json:"attachments"`
+}
+
+// ListIDs returns message ids in the configured folder, paginating pages until
+// maxIDs is reached (0 = all).
+func (o *OOClient) ListIDs(ctx context.Context, maxIDs int, page int) (ids []int, next int, err error) {
+ var out struct {
+ Response []ooMessage `json:"response"`
+ }
+ count := 100
+ if maxIDs > 0 && maxIDs < count {
+ count = maxIDs
+ }
+ path := fmt.Sprintf("/api/2.0/mail/messages?folder=%d&page=%d&count=%d", o.folderID, page, count)
+ if err := o.get(ctx, path, &out); err != nil {
+ return nil, 0, err
+ }
+ for _, m := range out.Response {
+ ids = append(ids, m.ID)
+ if maxIDs > 0 && len(ids) >= maxIDs {
+ break
+ }
+ }
+ next = page + 1
+ return ids, next, nil
+}
+
+// GetMessage fetches the full message by id and normalizes into Message.
+func (o *OOClient) GetMessage(ctx context.Context, id int) (*Message, error) {
+ var out struct {
+ Response ooMessage `json:"response"`
+ }
+ path := fmt.Sprintf("/api/2.0/mail/messages/%d", id)
+ if err := o.get(ctx, path, &out); err != nil {
+ return nil, err
+ }
+ m := out.Response
+ msg := &Message{
+ Source: "onlyoffice",
+ ID: fmt.Sprintf("%d", m.ID),
+ Folder: "oo",
+ Subject: m.Subject,
+ From: m.From,
+ To: m.To,
+ CC: m.CC,
+ BCC: m.BCC,
+ HTMLBody: m.HTMLBody,
+ TextBody: m.TextBody,
+ HasAttachments: m.HasAttachments,
+ MimeMessageID: m.MimeMessageID,
+ }
+ if t, err := time.Parse(time.RFC3339Nano, m.ReceivedDate); err == nil {
+ msg.ReceivedAt = t
+ }
+ for _, a := range m.Attachments {
+ msg.Attachments = append(msg.Attachments, Attachment{
+ FileID: fmt.Sprintf("%d", a.FileID),
+ FileName: a.FileName,
+ StoredName: a.StoredName,
+ Size: a.Size,
+ ContentType: a.ContentType,
+ })
+ }
+ return msg, nil
+}
+
+// DownloadAttachment fetches attachment bytes via the .ashx handler, which
+// requires the session cookie (client.Jar) captured during authenticate().
+func (o *OOClient) DownloadAttachment(ctx context.Context, fileID string) ([]byte, error) {
+ u := strings.TrimRight(o.cfg.URL, "/") + "/addons/mail/httphandlers/download.ashx?attachid=" + url.QueryEscape(fileID)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := o.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ data, err := io.ReadAll(io.LimitReader(resp.Body, 64<<20))
+ if err != nil {
+ return nil, err
+ }
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("oo download attach %s: status %d", fileID, resp.StatusCode)
+ }
+ return data, nil
+}
diff --git a/bin/mail/sync/sync.go b/bin/mail/sync/sync.go
new file mode 100644
index 0000000..5c4b58e
--- /dev/null
+++ b/bin/mail/sync/sync.go
@@ -0,0 +1,384 @@
+package sync
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "math"
+ "math/rand"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+)
+
+// RetryPolicy is the exponential-backoff strategy applied to transient HTTP
+// failures (5xx, timeouts, network errors). Callers wrap transient errors with
+// retryWrap; everything else aborts immediately.
+type RetryPolicy struct {
+ MaxAttempts int // total attempts (>=1); 0 => 5
+ BaseDelay time.Duration // first backoff; 0 => 250ms
+ MaxDelay time.Duration // cap; 0 => 15s
+ Jitter float64 // 0..1 multiplier; 0 => 0.2
+}
+
+func (p RetryPolicy) withDefaults() RetryPolicy {
+ if p.MaxAttempts <= 0 {
+ p.MaxAttempts = 5
+ }
+ if p.BaseDelay <= 0 {
+ p.BaseDelay = 250 * time.Millisecond
+ }
+ if p.MaxDelay <= 0 {
+ p.MaxDelay = 15 * time.Second
+ }
+ if p.Jitter <= 0 {
+ p.Jitter = 0.2
+ }
+ return p
+}
+
+// delay returns the wait before attempt n (1-based): base * 2^(n-2) + jitter,
+// capped at MaxDelay. Attempt 1 waits 0, attempt 2 waits base, then doubles.
+func (p RetryPolicy) delay(attempt int) time.Duration {
+ if attempt <= 1 {
+ return 0
+ }
+ exp := math.Min(float64(attempt-2), 10)
+ d := float64(p.BaseDelay) * math.Pow(2, exp)
+ if p.Jitter > 0 {
+ d *= 1 - p.Jitter + 2*p.Jitter*rand.Float64()
+ }
+ if d > float64(p.MaxDelay) {
+ d = float64(p.MaxDelay)
+ }
+ return time.Duration(d)
+}
+
+type errRetry struct{ err error }
+
+func (e *errRetry) Error() string { return e.err.Error() }
+func (e *errRetry) Unwrap() error { return e.err }
+
+func isRetriable(err error) bool {
+ var r *errRetry
+ return errors.As(err, &r)
+}
+
+func retryWrap(err error) error {
+ if err == nil {
+ return nil
+ }
+ if isRetriable(err) {
+ return err
+ }
+ return &errRetry{err: err}
+}
+
+// Retry runs fn up to MaxAttempts times with exponential backoff between
+// attempts. Non-retriable errors abort immediately. Returns the last error.
+func Retry(ctx context.Context, policy RetryPolicy, fn func() error) error {
+ policy = policy.withDefaults()
+ var err error
+ for attempt := 1; attempt <= policy.MaxAttempts; attempt++ {
+ if err = fn(); err == nil {
+ return nil
+ }
+ if !isRetriable(err) {
+ return err
+ }
+ if attempt == policy.MaxAttempts {
+ return fmt.Errorf("after %d attempts: %w", policy.MaxAttempts, err)
+ }
+ select {
+ case <-time.After(policy.delay(attempt)):
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+ return err
+}
+
+// SyncConfig wires up a sync run.
+type SyncConfig struct {
+ OO *OOConfig // OnlyOffice source (optional)
+ Gmail *GmailCredentials // Gmail source (optional)
+ Out string // var/mail root; default /var/mail
+ Workers int // concurrency; default 4
+ Limit int // max messages per source (0 = all)
+ Offset int // skip first N messages per source
+ Force bool // overwrite existing message.json + attachments
+ DryRun bool // list without writing
+ Policy RetryPolicy
+}
+
+// SyncStats is returned by Run.
+type SyncStats struct {
+ Checked int
+ New int32
+ Failed int32
+ Skipped int32
+}
+
+// Source abstracts the two backends for the worker pool.
+type Source interface {
+ // ListIDs yields ids (string form) to fetch. cursor resumes pagination.
+ ListIDs(ctx context.Context, limit int, cursor string) (ids []string, next string, err error)
+ Get(ctx context.Context, id string) (*Message, error)
+ DownloadAttachment(ctx context.Context, msg *Message, att Attachment) ([]byte, error)
+ Folder() string
+}
+
+type ooSource struct {
+ c *OOClient
+ page int
+}
+type gmailSource struct {
+ c *GmailClient
+ cur string
+}
+
+func (s *ooSource) Folder() string { return "inbox" }
+func (s *gmailSource) Folder() string { return "gmail" }
+
+func (s *ooSource) ListIDs(ctx context.Context, limit int, cursor string) ([]string, string, error) {
+ page := s.page
+ if page == 0 {
+ page = 1
+ }
+ ids, next, err := s.c.ListIDs(ctx, limit, page)
+ s.page = next
+ strs := make([]string, len(ids))
+ for i, id := range ids {
+ strs[i] = fmt.Sprintf("%d", id)
+ }
+ return strs, "", err
+}
+
+func (s *ooSource) Get(ctx context.Context, id string) (*Message, error) {
+ var mid int
+ if _, err := fmt.Sscanf(id, "%d", &mid); err != nil {
+ return nil, fmt.Errorf("oo id %q: %w", id, err)
+ }
+ return s.c.GetMessage(ctx, mid)
+}
+
+func (s *ooSource) DownloadAttachment(ctx context.Context, msg *Message, att Attachment) ([]byte, error) {
+ return s.c.DownloadAttachment(ctx, att.FileID)
+}
+
+func (s *gmailSource) ListIDs(ctx context.Context, limit int, cursor string) ([]string, string, error) {
+ ids, next, err := s.c.ListIDs(ctx, "in:inbox", limit, cursor)
+ return ids, next, err
+}
+
+func (s *gmailSource) Get(ctx context.Context, id string) (*Message, error) {
+ return s.c.GetMessage(ctx, id)
+}
+
+func (s *gmailSource) DownloadAttachment(ctx context.Context, msg *Message, att Attachment) ([]byte, error) {
+ return s.c.DownloadAttachment(ctx, msg.ID, att.FileID)
+}
+
+// Run executes the sync across the configured sources with a worker pool.
+func Run(ctx context.Context, cfg SyncConfig) (*SyncStats, error) {
+ if cfg.Out == "" {
+ cfg.Out = "var/mail"
+ }
+ if cfg.Workers <= 0 {
+ cfg.Workers = 4
+ }
+ if err := os.MkdirAll(cfg.Out, 0o755); err != nil {
+ return nil, err
+ }
+ var sources []Source
+ if cfg.OO != nil {
+ oo, err := NewOOClient(*cfg.OO, 1) // folder inbox
+ if err != nil {
+ return nil, fmt.Errorf("onlyoffice auth: %w", err)
+ }
+ sources = append(sources, &ooSource{c: oo})
+ }
+ if cfg.Gmail != nil {
+ gm, err := NewGmailClient(*cfg.Gmail)
+ if err != nil {
+ return nil, fmt.Errorf("gmail init: %w", err)
+ }
+ sources = append(sources, &gmailSource{c: gm})
+ }
+ if len(sources) == 0 {
+ return nil, errors.New("sync: no source configured (need OO, Gmail, or both)")
+ }
+
+ stats := &SyncStats{}
+ var jobs []struct {
+ src Source
+ id string
+ }
+ for _, src := range sources {
+ ids, _, err := src.ListIDs(ctx, cfg.Offset+cfg.Limit, "")
+ if err != nil {
+ return nil, fmt.Errorf("list %s: %w", src.Folder(), err)
+ }
+ if cfg.Offset > 0 {
+ if cfg.Offset >= len(ids) {
+ ids = nil
+ } else {
+ ids = ids[cfg.Offset:]
+ }
+ }
+ if cfg.Limit > 0 && len(ids) > cfg.Limit {
+ ids = ids[:cfg.Limit]
+ }
+ stats.Checked += len(ids)
+ for _, id := range ids {
+ jobs = append(jobs, struct {
+ src Source
+ id string
+ }{src: src, id: id})
+ }
+ }
+
+ var (
+ wg sync.WaitGroup
+ mu sync.Mutex
+ failures []string
+ )
+ jobsCh := make(chan struct {
+ src Source
+ id string
+ })
+ for i := 0; i < cfg.Workers; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for j := range jobsCh {
+ status, err := processOne(ctx, j.src, j.id, cfg)
+ switch status {
+ case statusFailed:
+ mu.Lock()
+ failures = append(failures, j.src.Folder()+"/"+j.id+": "+err.Error())
+ mu.Unlock()
+ atomic.AddInt32(&stats.Failed, 1)
+ case statusNew:
+ atomic.AddInt32(&stats.New, 1)
+ case statusSkipped:
+ atomic.AddInt32(&stats.Skipped, 1)
+ }
+ }
+ }()
+ }
+ for _, j := range jobs {
+ select {
+ case jobsCh <- j:
+ case <-ctx.Done():
+ close(jobsCh)
+ wg.Wait()
+ return stats, ctx.Err()
+ }
+ }
+ close(jobsCh)
+ wg.Wait()
+
+ if len(failures) > 0 {
+ fmt.Fprintf(os.Stderr, "sync: %d failures:\n %s\n", len(failures), strings.Join(failures, "\n "))
+ }
+ return stats, nil
+}
+
+type status int
+
+const (
+ statusNew status = iota
+ statusSkipped
+ statusFailed
+)
+
+func processOne(ctx context.Context, src Source, id string, cfg SyncConfig) (status, error) {
+ if cfg.DryRun {
+ return statusNew, nil
+ }
+ dir := filepath.Join(cfg.Out, src.Folder(), id)
+ jsonPath := filepath.Join(dir, "message.json")
+ if !cfg.Force {
+ if _, err := os.Stat(jsonPath); err == nil {
+ return statusSkipped, nil
+ }
+ }
+ var msg *Message
+ err := Retry(ctx, cfg.Policy, func() error {
+ m, err := src.Get(ctx, id)
+ if err != nil {
+ return retryWrap(err)
+ }
+ m.Folder = src.Folder() // directory layout is authoritative
+ if err := writeMessage(jsonPath, m); err != nil {
+ return err
+ }
+ msg = m
+ return nil
+ })
+ if err != nil {
+ return statusFailed, err
+ }
+ for _, att := range msg.Attachments {
+ attDir := filepath.Join(dir, "attachments")
+ if err := os.MkdirAll(attDir, 0o755); err != nil {
+ return statusFailed, err
+ }
+ attPath := filepath.Join(attDir, sanitize(att.StoredName))
+ if _, err := os.Stat(attPath); err == nil && !cfg.Force {
+ continue
+ }
+ var data []byte
+ err := Retry(ctx, cfg.Policy, func() error {
+ b, err := src.DownloadAttachment(ctx, msg, att)
+ if err != nil {
+ return retryWrap(err)
+ }
+ data = b
+ return os.WriteFile(attPath, b, 0o644)
+ })
+ if err != nil {
+ return statusFailed, fmt.Errorf("attachment %s: %w", att.FileName, err)
+ }
+ // ICS attachments get structured markdown immediately (same name the
+ // Python converter would use: .md).
+ if isICS(att.FileName) {
+ stem := att.FileName
+ if i := strings.LastIndex(stem, "."); i >= 0 {
+ stem = stem[:i]
+ }
+ mdPath := filepath.Join(attDir, sanitize(stem)+".md")
+ if err := os.WriteFile(mdPath, []byte(ICSToMarkdown(data)), 0o644); err != nil {
+ return statusFailed, err
+ }
+ }
+ }
+ return statusNew, nil
+}
+
+func writeMessage(path string, m *Message) error {
+ b, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ return err
+ }
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ return err
+ }
+ return os.WriteFile(path, b, 0o644)
+}
+
+func sanitize(name string) string {
+ r := strings.NewReplacer("/", "_", "\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_",
+ "<", "_", ">", "_", "|", "_", " ", "_")
+ return r.Replace(name)
+}
+
+func isICS(name string) bool {
+ n := strings.ToLower(name)
+ return strings.HasSuffix(n, ".ics") || strings.HasSuffix(n, ".ical")
+}
diff --git a/bin/mail/sync/sync_test.go b/bin/mail/sync/sync_test.go
new file mode 100644
index 0000000..3eb789d
--- /dev/null
+++ b/bin/mail/sync/sync_test.go
@@ -0,0 +1,240 @@
+package sync
+
+import (
+ "context"
+ "encoding/base64"
+ "errors"
+ "testing"
+ "time"
+ "unicode/utf8"
+)
+
+func TestRetrySucceedsOnSecondTry(t *testing.T) {
+ attempts := 0
+ err := Retry(context.Background(), RetryPolicy{BaseDelay: time.Millisecond, MaxDelay: 5 * time.Millisecond}, func() error {
+ attempts++
+ if attempts == 1 {
+ return retryWrap(errors.New("boom"))
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if attempts != 2 {
+ t.Fatalf("expected 2 attempts, got %d", attempts)
+ }
+}
+
+func TestRetryExhaustsAttempts(t *testing.T) {
+ attempts := 0
+ err := Retry(context.Background(), RetryPolicy{MaxAttempts: 3, BaseDelay: time.Millisecond, MaxDelay: 5 * time.Millisecond}, func() error {
+ attempts++
+ return retryWrap(errors.New("nope"))
+ })
+ if err == nil {
+ t.Fatal("expected error after exhaustion")
+ }
+ if attempts != 3 {
+ t.Fatalf("expected 3 attempts, got %d", attempts)
+ }
+}
+
+func TestRetryNonRetriableAbortsImmediately(t *testing.T) {
+ attempts := 0
+ err := Retry(context.Background(), RetryPolicy{MaxAttempts: 5, BaseDelay: time.Millisecond}, func() error {
+ attempts++
+ return errors.New("permanent")
+ })
+ if err == nil {
+ t.Fatal("expected error")
+ }
+ if attempts != 1 {
+ t.Fatalf("expected 1 attempt for non-retriable, got %d", attempts)
+ }
+}
+
+func TestRetryRespectsContext(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel()
+ err := Retry(ctx, RetryPolicy{MaxAttempts: 5, BaseDelay: time.Millisecond}, func() error {
+ return retryWrap(errors.New("x"))
+ })
+ if !errors.Is(err, context.Canceled) {
+ t.Fatalf("expected context.Canceled, got %v", err)
+ }
+}
+
+func TestDelayGrows(t *testing.T) {
+ p := RetryPolicy{BaseDelay: time.Second, MaxDelay: 30 * time.Second, Jitter: 0}
+ d1 := p.delay(1) // attempt 1 => 0
+ d2 := p.delay(2)
+ d3 := p.delay(3)
+ if d1 != 0 {
+ t.Fatalf("attempt 1 delay should be 0, got %v", d1)
+ }
+ if d2 != time.Second {
+ t.Fatalf("attempt 2 delay should be 1s, got %v", d2)
+ }
+ if d3 != 2*time.Second {
+ t.Fatalf("attempt 3 delay should be 2s, got %v", d3)
+ }
+}
+
+func TestSanitize(t *testing.T) {
+ cases := map[string]string{
+ "a/b\\c:d*e": "a_b_c_d_e",
+ "normal.txt": "normal.txt",
+ "../evil": ".._evil",
+ "a b c.pdf": "a_b_c.pdf",
+ }
+ for in, want := range cases {
+ if got := sanitize(in); got != want {
+ t.Errorf("sanitize(%q) = %q, want %q", in, got, want)
+ }
+ }
+}
+
+func TestIsICS(t *testing.T) {
+ if !isICS("reply.ics") || !isICS("x.ICAL") {
+ t.Fatal("ics extensions not detected")
+ }
+ if isICS("invoice.pdf") {
+ t.Fatal("pdf misdetected as ics")
+ }
+}
+
+const fixtureReplyICS = `BEGIN:VCALENDAR
+METHOD:REPLY
+PRODID:Microsoft Exchange Server 2010
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:W. Europe Standard Time
+BEGIN:STANDARD
+DTSTART:16010101T030000
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:16010101T020000
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=ACCEPTED;CN="Baker, Ben":mailto:bbaker1@teksystems.com
+UID:bvlnr1i35ug30kn6rvu9dop00g@google.com
+SUMMARY;LANGUAGE=en-US:Accepted: Appointment (Ben Baker)
+DTSTART;TZID=W. Europe Standard Time:20260812T120000
+DTEND;TZID=W. Europe Standard Time:20260812T123000
+CLASS:PUBLIC
+STATUS:CONFIRMED
+LOCATION;LANGUAGE=en-US:https://meet.google.com/sxh-ubud-jrd
+END:VEVENT
+END:VCALENDAR`
+
+func TestICSToMarkdown(t *testing.T) {
+ out := ICSToMarkdown([]byte(fixtureReplyICS))
+ for _, want := range []string{
+ "Accepted: Appointment",
+ "When:",
+ "Where:",
+ "meet.google.com",
+ "Attendee:",
+ "Baker, Ben",
+ "Accepted",
+ "Calendar method: REPLY",
+ } {
+ if !contains(out, want) {
+ t.Errorf("output missing %q:\n%s", want, out)
+ }
+ }
+ if contains(out, "BEGIN:VCALENDAR") {
+ t.Errorf("raw ICS leaked into markdown:\n%s", out)
+ }
+}
+
+func TestICSToMarkdownFallback(t *testing.T) {
+ out := ICSToMarkdown([]byte("not a calendar"))
+ if !contains(out, "not a calendar") {
+ t.Fatalf("expected raw fallback, got %q", out)
+ }
+}
+
+func TestICSToMarkdownNormalizesLatin1(t *testing.T) {
+ // Real-world ICS from a rental portal: summary in UTF-8, location Latin-1
+ // ("N\xfcrnberg"). The markdown output must be valid UTF-8 everywhere.
+ raw := "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n" +
+ "SUMMARY:Mietwagen-Buchung: N\xc3\xbcrnberg\r\n" +
+ "LOCATION:N\xfcrnberg\r\nDTSTART:20200101T090000Z\r\nDTEND:20200101T180000Z\r\n" +
+ "END:VEVENT\r\nEND:VCALENDAR\r\n"
+ out := ICSToMarkdown([]byte(raw))
+ if !utf8.ValidString(out) {
+ t.Fatalf("output is not valid UTF-8:\n%q", out)
+ }
+ if !contains(out, "Nürnberg") {
+ t.Errorf("expected Nürnberg in output:\n%s", out)
+ }
+ if contains(out, "N\xfcrnberg") {
+ t.Errorf("Latin-1 bytes leaked into output:\n%q", out)
+ }
+}
+
+func TestICSToMarkdownAllDay(t *testing.T) {
+ ics := `BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:y@google.com
+SUMMARY:All day thing
+DTSTART;VALUE=DATE:20260815
+DTEND;VALUE=DATE:20260816
+END:VEVENT
+END:VCALENDAR`
+ out := ICSToMarkdown([]byte(ics))
+ if !contains(out, "All day thing") || !contains(out, "2026-08-15") {
+ t.Errorf("all-day event not parsed:\n%s", out)
+ }
+}
+
+func TestCollectParts(t *testing.T) {
+ p := gmailPart{
+ MimeType: "multipart/mixed",
+ Parts: []gmailPart{
+ {MimeType: "multipart/alternative", Parts: []gmailPart{
+ {MimeType: "text/plain", Body: gmailBody{Data: b64("plain text")}},
+ {MimeType: "text/html", Body: gmailBody{Data: b64("html
")}},
+ }},
+ {PartID: "2", MimeType: "application/pdf", Filename: "invoice.pdf", Body: gmailBody{Size: 100}},
+ },
+ }
+ text, html, atts := collectParts(p, "root", "abc123", 0)
+ if text != "plain text" {
+ t.Errorf("text = %q", text)
+ }
+ if html != "html
" {
+ t.Errorf("html = %q", html)
+ }
+ if len(atts) != 1 || atts[0].FileName != "invoice.pdf" || atts[0].FileID != "abc123:2" {
+ t.Errorf("atts = %+v", atts)
+ }
+}
+
+func b64(s string) string {
+ return base64.URLEncoding.EncodeToString([]byte(s))
+}
+
+func contains(s, sub string) bool {
+ return len(s) >= len(sub) && (s == sub || len(sub) == 0 ||
+ indexOf(s, sub) >= 0)
+}
+
+func indexOf(s, sub string) int {
+ for i := 0; i+len(sub) <= len(s); i++ {
+ if s[i:i+len(sub)] == sub {
+ return i
+ }
+ }
+ return -1
+}
diff --git a/bin/mail/sync/types.go b/bin/mail/sync/types.go
new file mode 100644
index 0000000..ebf6112
--- /dev/null
+++ b/bin/mail/sync/types.go
@@ -0,0 +1,46 @@
+// Package sync downloads OnlyOffice and Gmail messages to var/mail/ as raw
+// JSON + attachment files, then hands off to bin/mail/import --from-raw for
+// markdown conversion.
+//
+// On-disk schema (per message):
+//
+// var/mail///message.json # Message (this package)
+// var/mail///attachments/ # raw attachment bytes (storedName)
+//
+// The Message JSON is the contract shared with the Python converter. Fields
+// deliberately mirror what bin/mail/import already reads from the OnlyOffice
+// API, so conversion is source-agnostic.
+package sync
+
+import (
+ "time"
+)
+
+// Attachment describes one attachment of a Message. FileID/FileName/StoredName
+// mirror OnlyOffice; Gmail fills them from its own ids. StoredName is always
+// unique (hash/attachment id) so raw files never collide.
+type Attachment struct {
+ FileID string `json:"fileId,omitempty"`
+ FileName string `json:"fileName"`
+ StoredName string `json:"storedName"`
+ Size int64 `json:"size,omitempty"`
+ ContentType string `json:"contentType,omitempty"`
+}
+
+// Message is the normalized record written to var/mail///message.json.
+type Message struct {
+ Source string `json:"source"` // "onlyoffice" | "gmail"
+ ID string `json:"id"`
+ Folder string `json:"folder"`
+ Subject string `json:"subject,omitempty"`
+ From string `json:"from,omitempty"`
+ To string `json:"to,omitempty"`
+ CC string `json:"cc,omitempty"`
+ BCC string `json:"bcc,omitempty"`
+ ReceivedAt time.Time `json:"receivedAt,omitempty"`
+ HTMLBody string `json:"htmlBody,omitempty"`
+ TextBody string `json:"textBody,omitempty"`
+ HasAttachments bool `json:"hasAttachments,omitempty"`
+ Attachments []Attachment `json:"attachments,omitempty"`
+ MimeMessageID string `json:"mimeMessageId,omitempty"`
+}
diff --git a/bin/tools/mailconv.py b/bin/tools/mailconv.py
new file mode 100644
index 0000000..6756d16
--- /dev/null
+++ b/bin/tools/mailconv.py
@@ -0,0 +1,148 @@
+"""mailconv - pure helpers for bin/mail/import (mail -> markdown + attachments).
+
+Shared with unit tests in bin/tools/test_mailconv.py. No network, no OnlyOffice
+dependencies here: everything is `str -> str` or `Path -> str` so the tests run
+offline against fixtures.
+"""
+from __future__ import annotations
+
+import html
+import re
+import zipfile
+from pathlib import Path
+
+# Body part / attachment file suffixes we know how to turn into markdown text.
+TEXT_SUFFIXES = {".md", ".markdown", ".txt", ".csv", ".json", ".xml", ".yaml", ".yml", ".log", ".tsv",
+ ".ics", ".ical", ".vcf", ".eml"}
+OFFICE_SUFFIXES = {".docx", ".pptx", ".xlsx", ".html", ".htm", ".epub", ".eml", ".msg"}
+PDF_SUFFIXES = {".pdf"}
+IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".tif", ".webp"}
+ARCHIVE_SUFFIXES = {".zip"}
+# Legacy binary Office (doc/xls/ppt) — markitdown/docling skip them; we try
+# pandoc first, else leave a stub.
+LEGACY_OFFICE_SUFFIXES = {".doc", ".xls", ".ppt"}
+
+CONVERTIBLE_SUFFIXES = (
+ TEXT_SUFFIXES | OFFICE_SUFFIXES | PDF_SUFFIXES | IMAGE_SUFFIXES | ARCHIVE_SUFFIXES | LEGACY_OFFICE_SUFFIXES
+)
+
+
+def clean_email_address(raw: str) -> str:
+ """Extract the bare email from '"Name" ' and strip control chars."""
+ m = re.search(r"<([^<>@\s]+@[^<>@\s]+)>", raw)
+ return (m.group(1) if m else raw).strip()
+
+
+def subject_to_filename(subject: str, max_len: int = 80) -> str:
+ """Turn a mail subject into a filesystem-safe slug (keep first token readable)."""
+ s = re.sub(r"[^\w\-. ]+", "", subject).strip()
+ s = re.sub(r"\s+", "_", s)
+ s = s.strip("._")
+ if not s:
+ s = "untitled"
+ return s[:max_len] or "untitled"
+
+
+def strip_html(html_text: str) -> str:
+ """Naive HTML -> plain text fallback (used only if markitdown is missing)."""
+ import re as _re
+ text = _re.sub(r"(?is)<(script|style)[^>]*>.*?\1>", "", html_text)
+ text = _re.sub(r"(?s)
", "\n", text)
+ text = _re.sub(r"(?s)
", "\n\n", text)
+ text = _re.sub(r"(?s)<[^>]+>", "", text)
+ return html.unescape(text).strip()
+
+
+def _unwrap_tables(html_text: str) -> str:
+ """Unwrap mail HTML tables into pipe-joined text lines.
+
+ Outlook/Stripe-style emails wrap content in nested spacer/frame tables that
+ markitdown renders as hundreds of `--- |` cells and duplicated blocks.
+ Every becomes plain "cell1 | cell2" lines (key-value pairs survive),
+ so only headings/paragraphs/links reach markitdown and no table noise is left.
+ """
+ try:
+ from bs4 import BeautifulSoup
+ except Exception:
+ return html_text
+ soup = BeautifulSoup(html_text, "html.parser")
+ for table in reversed(soup.find_all("table")):
+ lines: list[str] = []
+ for row in table.find_all("tr"):
+ cells = [c.get_text(" ", strip=True) for c in row.find_all(["td", "th"])]
+ line = " | ".join(x for x in cells if x)
+ if line:
+ lines.append(line)
+ if lines:
+ table.replace_with(BeautifulSoup("\n".join(lines), "html.parser"))
+ else:
+ table.decompose()
+ return str(soup)
+
+
+def html_to_markdown(html_text: str) -> str:
+ """Convert a mail HTML body to markdown using markitdown when available."""
+ html_text = _unwrap_tables(html_text)
+ try:
+ from markitdown import MarkItDown
+ import io
+ md = MarkItDown()
+ result = md.convert_stream(io.BytesIO(html_text.encode("utf-8", errors="replace")),
+ file_extension=".html")
+ text = result.text_content.strip()
+ if text:
+ return normalize_markdown(text)
+ except Exception:
+ pass
+ return normalize_markdown(strip_html(html_text))
+
+
+def normalize_markdown(text: str) -> str:
+ """Collapse the pdfminer/markitdown NUL artifacts and stray control chars."""
+ # NUL bytes that pdfminer inserts between digits/letters.
+ text = text.replace("\x00", "")
+ # Email spacer noise: zero-width chars, soft hyphens, figure spaces,
+ # combining grapheme joiner, BOM.
+ for ch in ("\ufeff", "\u200b", "\u034f", "\u00ad", "\u2007", "\u2008", "\u200a", "\u2002"):
+ text = text.replace(ch, "")
+ text = re.sub(r"[ \t]{2,}", " ", text)
+ # Trim trailing whitespace per line so space-only spacer rows collapse.
+ text = "\n".join(l.rstrip() for l in text.split("\n"))
+ # Collapse 3+ blank lines to two.
+ text = re.sub(r"\n{3,}", "\n\n", text)
+ # Remove weird trailing control chars.
+ text = "".join(ch for ch in text if ch >= " " or ch in "\n\t")
+ return text.strip()
+
+
+def split_zip_members(zip_path: Path) -> list[str]:
+ """Return safe member names of a zip archive (skips dir entries)."""
+ try:
+ with zipfile.ZipFile(zip_path) as zf:
+ return [m for m in zf.namelist() if not m.endswith("/")]
+ except zipfile.BadZipFile:
+ return []
+
+
+def zip_extract_safe(zip_path: Path, dest: Path) -> list[Path]:
+ """Extract a zip into dest guarding against path traversal; returns files."""
+ out: list[Path] = []
+ try:
+ with zipfile.ZipFile(zip_path) as zf:
+ for member in zf.infolist():
+ if member.is_dir():
+ continue
+ target = (dest / member.filename).resolve()
+ if not target.is_relative_to(dest.resolve()):
+ continue
+ target.parent.mkdir(parents=True, exist_ok=True)
+ with zf.open(member) as src, open(target, "wb") as dst:
+ dst.write(src.read())
+ out.append(target)
+ except zipfile.BadZipFile:
+ return []
+ return out
+
+
+def is_convertible(suffix: str) -> bool:
+ return suffix.lower() in CONVERTIBLE_SUFFIXES
diff --git a/bin/tools/test_mailconv.py b/bin/tools/test_mailconv.py
new file mode 100644
index 0000000..6b945f5
--- /dev/null
+++ b/bin/tools/test_mailconv.py
@@ -0,0 +1,123 @@
+import io
+import os
+import sys
+import unittest
+import zipfile
+from pathlib import Path
+
+sys.path.insert(0, os.path.dirname(__file__))
+
+from mailconv import ( # noqa: E402
+ clean_email_address,
+ html_to_markdown,
+ is_convertible,
+ normalize_markdown,
+ split_zip_members,
+ subject_to_filename,
+ zip_extract_safe,
+)
+from mailconv import _unwrap_tables # noqa: E402
+
+
+class TestMailConv(unittest.TestCase):
+ def test_clean_email_address(self):
+ self.assertEqual(clean_email_address('"Ben Baker" '), "bb@teks.com")
+ self.assertEqual(clean_email_address("eslider@gmail.com"), "eslider@gmail.com")
+ self.assertEqual(clean_email_address(""), "a@b.c")
+
+ def test_subject_to_filename(self):
+ self.assertEqual(subject_to_filename("Your receipt #2422"), "Your_receipt_2422")
+ self.assertEqual(subject_to_filename("a/b\\c:d*e"), "abcde")
+ self.assertEqual(subject_to_filename(" "), "untitled")
+
+ def test_html_to_markdown(self):
+ out = html_to_markdown("Hi
Some bold text.
")
+ self.assertIn("Hi", out)
+ self.assertIn("**bold**", out)
+
+ def test_html_strip_fallback(self):
+ from mailconv import strip_html
+ self.assertEqual(strip_html("a
b
"), "a\n\nb")
+
+ def test_flatten_layout_tables(self):
+ html = (""
+ + "".join(f"| spacer{i} | " for i in range(12))
+ + "
"
+ + "real
"
+ + "")
+ out = _unwrap_tables(html)
+ # tables unwrapped into pipe text; no left; content preserved
+ self.assertNotIn(" | spacer0 | ", out)
+ self.assertIn("spacer0 | spacer1", out)
+ self.assertIn("a | b", out)
+ self.assertIn("real", out)
+
+ def test_html_to_markdown_layout_clean(self):
+ html = "" + "".join(f"| x{i} | " for i in range(12)) + "
Hi
"
+ out = html_to_markdown(html)
+ self.assertIn("Hi", out)
+ self.assertNotIn("| ---", out)
+
+ def test_normalize_markdown_removes_nul(self):
+ self.assertEqual(normalize_markdown("Z0\x00A\x00Y\x00B"), "Z0AYB")
+ self.assertEqual(normalize_markdown("a\n\n\n\nb"), "a\n\nb")
+
+ def test_normalize_strips_email_noise(self):
+ noisy = "\ufeffa\u200b\u034f\u00ad\u2007\u2002 b\u200a c\u2008"
+ out = normalize_markdown(noisy)
+ self.assertNotIn("\u200b", out)
+ self.assertNotIn("\ufeff", out)
+ self.assertNotIn("\u034f", out)
+ self.assertIn("a b c", out)
+
+ def test_split_zip_members(self):
+ p = Path(self._mk_zip(["a.txt", "sub/b.txt"]))
+ self.assertEqual(split_zip_members(p), ["a.txt", "sub/b.txt"])
+
+ def test_zip_extract_safe(self):
+ zip_path = self._mk_zip(["a.txt", "dir/b.txt"])
+ dest = Path(self._tmp("x"))
+ files = zip_extract_safe(zip_path, dest)
+ self.assertEqual(len(files), 2)
+ self.assertTrue((dest / "a.txt").exists())
+ self.assertTrue((dest / "dir" / "b.txt").exists())
+
+ def test_zip_extract_safe_blocks_traversal(self):
+ # member "../evil.txt" must not escape dest
+ zip_path = Path(self._tmp("evil.zip"))
+ with zipfile.ZipFile(zip_path, "w") as zf:
+ zf.writestr("../evil.txt", "boom")
+ dest = Path(self._tmp("out"))
+ files = zip_extract_safe(zip_path, dest)
+ self.assertEqual(files, [])
+ self.assertFalse((dest.parent / "evil.txt").exists())
+
+ def test_is_convertible(self):
+ self.assertTrue(is_convertible(".pdf"))
+ self.assertTrue(is_convertible(".zip"))
+ self.assertTrue(is_convertible(".docx"))
+ self.assertTrue(is_convertible(".TXT"))
+ self.assertFalse(is_convertible(".exe"))
+ self.assertFalse(is_convertible(".unknown"))
+
+ def _mk_zip(self, members):
+ zpath = Path(self._tmp("arc.zip"))
+ with zipfile.ZipFile(zpath, "w") as zf:
+ for m in members:
+ zf.writestr(m, "content")
+ return str(zpath)
+
+ def _tmp(self, name):
+ d = self.__class__._td
+ p = Path(d) / name
+ p.parent.mkdir(parents=True, exist_ok=True)
+ return str(p)
+
+ @classmethod
+ def setUpClass(cls):
+ import tempfile
+ cls._td = tempfile.mkdtemp(prefix="mailconv_test_")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/go.mod b/go.mod
index 2aa0757..2d704f6 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,8 @@
module github.com/eSlider/2dph
-go 1.25
\ No newline at end of file
+go 1.25.0
+
+require (
+ github.com/arran4/golang-ical v0.3.5
+ golang.org/x/text v0.40.0
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..5962f4f
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,14 @@
+github.com/arran4/golang-ical v0.3.5 h1:bbz6ld4dC+MmCKiFfOd6SkmIGnhNMBACZ485ULh7p9A=
+github.com/arran4/golang-ical v0.3.5/go.mod h1:OnguFgjN0Hmx8jzpmWcC+AkHio94ujmLHKoaef7xQh8=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
+golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/pyproject.toml b/pyproject.toml
index d8ec539..5de7d10 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,9 @@ readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
dependencies = [
+ "docling>=2.119.0",
"ladybug==0.19.1",
+ "markitdown[docx,epub,html,image-exif,pdf,pptx,xlsx,zip]>=0.1.7",
"mistune==3.3.4",
"model2vec==0.8.2",
"numpy>=2.5.2",
diff --git a/uv.lock b/uv.lock
index 2e4cc51..a6bbced 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,13 +1,26 @@
version = 1
revision = 3
requires-python = ">=3.12"
+resolution-markers = [
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'darwin'",
+ "python_full_version == '3.13.*' and sys_platform == 'darwin'",
+ "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'win32'",
+ "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'win32'",
+ "python_full_version < '3.13' and sys_platform == 'win32'",
+ "python_full_version < '3.13' and sys_platform == 'darwin'",
+ "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'win32'",
+]
[[package]]
name = "2dph"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
+ { name = "docling" },
{ name = "ladybug" },
+ { name = "markitdown", extra = ["docx", "pdf", "pptx", "xlsx"] },
{ name = "mistune" },
{ name = "model2vec" },
{ name = "numpy" },
@@ -17,7 +30,9 @@ dependencies = [
[package.metadata]
requires-dist = [
+ { name = "docling", specifier = ">=2.119.0" },
{ name = "ladybug", specifier = "==0.19.1" },
+ { name = "markitdown", extras = ["docx", "epub", "html", "image-exif", "pdf", "pptx", "xlsx", "zip"], specifier = ">=0.1.7" },
{ name = "mistune", specifier = "==3.3.4" },
{ name = "model2vec", specifier = "==0.8.2" },
{ name = "numpy", specifier = ">=2.5.2" },
@@ -25,6 +40,48 @@ requires-dist = [
{ name = "requests", specifier = ">=2.34.2" },
]
+[[package]]
+name = "accelerate"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huggingface-hub" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "psutil" },
+ { name = "pyyaml" },
+ { name = "safetensors" },
+ { name = "torch" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8d/75/94cd5d389649578aca399e5aa822637eec18319a1dadc400ffe2f9a7493f/accelerate-1.14.0.tar.gz", hash = "sha256:41b9c4377a54e0b460a959b0defa1b736e4ca0a2373252d9a539964c2afe3c8d", size = 412167, upload-time = "2026-06-11T13:45:52.326Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a8/db/253133d7e7cb40d3af384bb2f5c0b4a2b7fdcffbc95c688cc67a20a3c103/accelerate-1.14.0-py3-none-any.whl", hash = "sha256:e94390c2863b873be18f623f9df48a0d8fe5eff13ea7f1a00092b0a7904888c6", size = 389246, upload-time = "2026-06-11T13:45:50.477Z" },
+]
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
+]
+
+[[package]]
+name = "antlr4-python3-runtime"
+version = "4.9.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" }
+
[[package]]
name = "anyio"
version = "4.14.2"
@@ -38,6 +95,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
]
+[[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 = "beautifulsoup4"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "soupsieve" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" },
+]
+
[[package]]
name = "certifi"
version = "2026.7.22"
@@ -47,6 +126,91 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
]
+[[package]]
+name = "cffi"
+version = "2.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" },
+ { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" },
+ { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" },
+ { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" },
+ { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" },
+ { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" },
+ { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" },
+ { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" },
+ { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" },
+ { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" },
+ { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" },
+ { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" },
+ { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" },
+ { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" },
+ { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" },
+ { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" },
+ { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" },
+ { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" },
+ { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" },
+ { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" },
+ { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" },
+ { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" },
+ { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" },
+ { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" },
+ { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" },
+ { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" },
+ { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" },
+]
+
[[package]]
name = "charset-normalizer"
version = "3.4.9"
@@ -120,6 +284,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
]
+[[package]]
+name = "cobble"
+version = "0.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/7a/a507c709be2c96e1bb6102eb7b7f4026c5e5e223ef7d745a17d239e9d844/cobble-0.1.4.tar.gz", hash = "sha256:de38be1539992c8a06e569630717c485a5f91be2192c461ea2b220607dfa78aa", size = 3805, upload-time = "2024-06-01T18:11:09.528Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d5/e1/3714a2f371985215c219c2a70953d38e3eed81ef165aed061d21de0e998b/cobble-0.1.4-py3-none-any.whl", hash = "sha256:36c91b1655e599fd428e2b95fdd5f0da1ca2e9f1abb0bc871dec21a0e78a2b44", size = 3984, upload-time = "2024-06-01T18:11:07.911Z" },
+]
+
[[package]]
name = "colorama"
version = "0.4.6"
@@ -129,6 +302,360 @@ 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 = "coloredlogs"
+version = "15.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "humanfriendly" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" },
+]
+
+[[package]]
+name = "colorlog"
+version = "6.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8c/55/ba79756cb90c8d69d599d57785398ac87bba7b19c80e87f4e8a562197c93/colorlog-6.12.0.tar.gz", hash = "sha256:2a7924c1dadf18b22a0eb8b06d1c7b01d5341707ec1641eb6fcc4fde0c3e8e5f", size = 18151, upload-time = "2026-07-23T13:40:40.71Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d4/19/0b6647bf5e331521e55d2b63bfbdc210bd9cd605189273f03614a05f702d/colorlog-6.12.0-py3-none-any.whl", hash = "sha256:30d392604e9110045a2c2aeefc27d7a017abbab63f3a8aee594eac0801df784e", size = 12239, upload-time = "2026-07-23T13:40:39.562Z" },
+]
+
+[[package]]
+name = "cryptography"
+version = "50.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
+ { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
+ { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
+ { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
+ { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
+ { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
+ { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
+ { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
+ { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" },
+ { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" },
+ { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" },
+ { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" },
+ { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" },
+ { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" },
+ { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
+ { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
+ { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
+ { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
+ { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
+ { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
+]
+
+[[package]]
+name = "cuda-bindings"
+version = "13.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cuda-pathfinder" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" },
+ { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" },
+ { url = "https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2732904099e0a4d4db774a5fc6d91ee95fae065b4d2ecabb4968c5fe2406c9d7", size = 6476660, upload-time = "2026-05-29T23:11:59.188Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/81/bff68ce829999c1e4209c761bbf903b1c06ec570416ddb25020864ad5907/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ab2f74ed65bfef4163ba07a8db16f1085e0729291db12a2423aff84ee8278b8", size = 6013639, upload-time = "2026-05-29T23:12:03.509Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/e0/c8a1f0c8f9ffdea4f5fe6dbab89b326cef4d85caf489dad39e209da89416/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd4c814d311ec08c981f6dded1dbe7d4b371067ee4f6c14cccec4bde9590f80", size = 6534419, upload-time = "2026-05-29T23:12:05.633Z" },
+ { url = "https://files.pythonhosted.org/packages/52/b8/83b1f563925b290f2d11a01a77a84013ba56052fe3653a5bef3ccfbb43d6/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3c772dfff49681541d59630c90f858e173ac926b9c593a2b7123f2a1043cc76", size = 5809771, upload-time = "2026-05-29T23:12:10.422Z" },
+ { url = "https://files.pythonhosted.org/packages/12/20/e79b4bfe98f075195afb6343d41c498f9dbd2d161d7021d4d28bceb83581/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36febb7c1079d68a981dbbd8d5a67235b399802b82075c9388624719607e52b9", size = 6358584, upload-time = "2026-05-29T23:12:12.767Z" },
+]
+
+[[package]]
+name = "cuda-pathfinder"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/b4/d088047afe39827556df21118cac9ffd20cc3f968c99a7681494d1eb333c/cuda_pathfinder-1.6.0-py3-none-any.whl", hash = "sha256:1503af579d8379c24bdd65528379bc57039b0455be9f5f9686cf8e473a1fce51", size = 54591, upload-time = "2026-07-21T15:03:56.224Z" },
+]
+
+[[package]]
+name = "cuda-toolkit"
+version = "13.0.3.0"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/c7/a79086a62c98befcdb8349656c6f114e2db3b8b2422f6e25c97a7f2a9a3c/cuda_toolkit-13.0.3.0-py2.py3-none-any.whl", hash = "sha256:d693caaa261214ddd7dbb60d68e71cbed884e68c2be7509778f3051da0b91c3f", size = 2512, upload-time = "2026-04-14T00:50:08.173Z" },
+]
+
+[package.optional-dependencies]
+cublas = [
+ { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+cudart = [
+ { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+cufft = [
+ { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+cufile = [
+ { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+cupti = [
+ { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+curand = [
+ { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+cusolver = [
+ { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+cusparse = [
+ { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+ { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+nvjitlink = [
+ { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+nvrtc = [
+ { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+nvtx = [
+ { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" },
+]
+
+[[package]]
+name = "defusedxml"
+version = "0.7.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
+]
+
+[[package]]
+name = "dill"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" },
+]
+
+[[package]]
+name = "doclang"
+version = "0.7.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "lxml" },
+ { name = "typer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/3a/005e4856ad8e9b9879414a4df4dbc56dc3663b96f9d8c920ef210e8931cf/doclang-0.7.3.tar.gz", hash = "sha256:ca50615357e46ebf9597bb9065b9112367103ec24bd539f8ae12649224cf50b0", size = 31569, upload-time = "2026-07-15T08:11:02.917Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a5/81/334ccc0f0cd7c3d75996b6b596e7f4c62c4c46a0ca042003315c28170159/doclang-0.7.3-py3-none-any.whl", hash = "sha256:9440c4ca9f7e061a7b8d33bdf15b1029be69a4c13cd8952dd6ce541884e4c685", size = 32267, upload-time = "2026-07-15T08:11:01.977Z" },
+]
+
+[[package]]
+name = "docling"
+version = "2.119.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "docling-slim", extra = ["standard"] },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/30/c2/f9fe1cc7780a41956c73e003773f002f9721671e9f76a2d19333e38f2e4c/docling-2.119.0.tar.gz", hash = "sha256:04b9aef29b9b94fc6e56fcd440824337d132a84074c7ba20b9cf055f220e72fb", size = 9016, upload-time = "2026-08-10T10:28:54.837Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/ea/37626a6ee6d4b26c14701b678eae81f53615c01afaa68b6b2367c4bbb09c/docling-2.119.0-py3-none-any.whl", hash = "sha256:3ff6f1dbe9f53ac034aca03c39fae374b381dc1eda4ad85d27a458fc13fbc917", size = 5180, upload-time = "2026-08-10T10:28:53.773Z" },
+]
+
+[[package]]
+name = "docling-core"
+version = "2.91.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "defusedxml" },
+ { name = "doclang" },
+ { name = "jsonref" },
+ { name = "jsonschema" },
+ { name = "latex2mathml" },
+ { name = "pandas" },
+ { name = "pillow" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "pyyaml" },
+ { name = "tabulate" },
+ { name = "typer" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/5aaf6f1221242a4dd598b515786ffca796352a8d960a9c6a0deb614301f4/docling_core-2.91.0.tar.gz", hash = "sha256:dc40fe76524a2700f869265015a9ef86027888e73b5652f324b3b5c52a2df240", size = 344852, upload-time = "2026-08-06T14:23:07.919Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/68/de7c0ba404d035afe78ae4e602290bb6c330b34cc53fd5f92c9950be4ac0/docling_core-2.91.0-py3-none-any.whl", hash = "sha256:4949a5dd77ae1daf4153c095897d3bdde1c870f2bbe401bf94d8834bef867998", size = 286832, upload-time = "2026-08-06T14:23:05.908Z" },
+]
+
+[package.optional-dependencies]
+chunking = [
+ { name = "semchunk" },
+ { name = "transformers", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" },
+ { name = "transformers", version = "5.15.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" },
+ { name = "tree-sitter" },
+ { name = "tree-sitter-c" },
+ { name = "tree-sitter-javascript" },
+ { name = "tree-sitter-python" },
+ { name = "tree-sitter-typescript" },
+]
+
+[[package]]
+name = "docling-ibm-models"
+version = "3.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "accelerate" },
+ { name = "docling-core" },
+ { name = "huggingface-hub" },
+ { name = "jsonlines" },
+ { name = "numpy" },
+ { name = "pillow" },
+ { name = "pydantic" },
+ { name = "rtree" },
+ { name = "safetensors", extra = ["torch"] },
+ { name = "torch" },
+ { name = "torchvision" },
+ { name = "tqdm" },
+ { name = "transformers", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" },
+ { name = "transformers", version = "5.15.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bd/b5/f95bd7df8acc3b792fd11c6d00c8a9ea3966b10edd734fb194d07be29606/docling_ibm_models-3.14.0.tar.gz", hash = "sha256:def964e3d524f66c7321ef9d48d4021278f14319f01d3f78058cd2324f641e22", size = 100765, upload-time = "2026-08-11T06:58:25.953Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4e/96/c168e6d31397203eeed572c3b2cd1bb1efff7f9b0753cb53855fd505894a/docling_ibm_models-3.14.0-py3-none-any.whl", hash = "sha256:795d39cd0f7b1e14a702e681b0ef0f9bd31deaedddb4e2686ad577296ecb8fc9", size = 94362, upload-time = "2026-08-11T06:58:24.513Z" },
+]
+
+[[package]]
+name = "docling-parse"
+version = "7.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "docling-core" },
+ { name = "pillow" },
+ { name = "pydantic" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/bf/e2e75880a275d02016536a4969edf6827f12f6a8f9e3733e162647330021/docling_parse-7.12.0.tar.gz", hash = "sha256:62e07e64d0f1ec7e8dd69a0fe9c5dc1d3c6566e4215db3200254bb91c0950fb0", size = 6826844, upload-time = "2026-08-11T09:37:26.692Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/23/93/d816fa83c06a12d636f41101a97d317c670386be46b3c98c6fad96165771/docling_parse-7.12.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:aaeda6aeef6d18de3931c118c8c2738d4d96d0bc905a1d35c08f5efca7f7bf32", size = 9656222, upload-time = "2026-08-11T09:36:56.661Z" },
+ { url = "https://files.pythonhosted.org/packages/53/1f/118c2449d9010fdd1f9d3ece848bd654261207127e4eca1c058ae756ff28/docling_parse-7.12.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a46b8d7b24d8739398c15ebc99020f8d1aba39bfe29b0228277718bbe720433", size = 10177571, upload-time = "2026-08-11T09:36:58.53Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/10/da10719d994196050342ac4677f586baf77a3f8f8525016c2b94a43a238a/docling_parse-7.12.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1782df91ffa439518c56d745ba9f6bcf6b899c3fd9942331c7e0f4a99aefd2e9", size = 10562851, upload-time = "2026-08-11T09:37:00.786Z" },
+ { url = "https://files.pythonhosted.org/packages/97/62/33d690f2af829233ab8e7ed8509bd003116a1f6debf13bc7bfed64fff748/docling_parse-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:871210fe8554a0f8b55a04de563d61183fa8b40e4e809c19b5f969c7b4f019de", size = 11623243, upload-time = "2026-08-11T09:37:02.891Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/77/ec04cc06b5d6e026dcd7ca05293ef2687839351f566dc040c865e0da7e92/docling_parse-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:ad24c38f46dc99d96864bdb88a48769bb71697cad96ea20e4d2f48cad42d4977", size = 8950994, upload-time = "2026-08-11T09:37:04.912Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/c4/d544a97dc93db0932dd52a8fe9941ca2b59bef1333c8f398cb3f2232bae8/docling_parse-7.12.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dcef3d25258750c02326bfb7899b753366bda1543fd58e6a8329bc2d7344b3f7", size = 9656250, upload-time = "2026-08-11T09:37:06.602Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ff/2f4607d9e3b93a9321f6ad2de2379b829a975bd9153ed4009dc83467db99/docling_parse-7.12.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5cb24e4806191ff063bb17b08931f8043c8905c707d19004b1f684734b99392", size = 10177684, upload-time = "2026-08-11T09:37:08.634Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/50/d43a896ade40afd97b2fbcc17169bdc4abcbc442310114df66eaa2555c02/docling_parse-7.12.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60fd9d4fe416335c1f241ea5fa697a6d361022fe4c285f3535c594dcbc2005ad", size = 10562643, upload-time = "2026-08-11T09:37:10.746Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/28/ca952f6c5aff3e7650498e1ceedac070ab56d44a0799256b7dc2be316185/docling_parse-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:5944d21942d0dd22438fd3edc97343665ad44b92d5208b7683bd6af0bd7ed20f", size = 11622973, upload-time = "2026-08-11T09:37:12.709Z" },
+ { url = "https://files.pythonhosted.org/packages/09/43/e84d09f86a40d288296fe9413429c4bcda64991da52f16283a868493e247/docling_parse-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:58e4e1830af45933ff58762c8a22c47d7448ccc3a43eb9d4d567822a613403a6", size = 8950908, upload-time = "2026-08-11T09:37:14.815Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/75/976c9a5788d17024e27bc9ebd7547b6286b0411b1f0422e0eb2728eea48a/docling_parse-7.12.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:00dec572aec9b2f64aee7e724f236f05a83065cbe464145667e64d1d68d1cc2f", size = 9656572, upload-time = "2026-08-11T09:37:16.664Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ef/9914e74ebbd538a20a178fd29d7d7eaef3ea0c981975844a854aaa79d732/docling_parse-7.12.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd42aba4ac361bf4e8b9e8bdf8be8516fc8030510c4360435790045af0ec337", size = 10178399, upload-time = "2026-08-11T09:37:18.384Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9b/642de8e568ce1aec65d53bb438d0a98583052d915d8bbedd0a840b76a9f5/docling_parse-7.12.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd62f8ac2a5403f427ac0d49aaf8f4f369219f24d6f0005175510ec5e9f40b33", size = 10563406, upload-time = "2026-08-11T09:37:20.359Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/c8/4a741e56c9851d7283771b9206a517e598ffdcfff6366d1f1f5e43f6924d/docling_parse-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:2dcb6f4c231066e87aafd7d90c492d634db27851e470042a57c22764bf6ca8aa", size = 12055028, upload-time = "2026-08-11T09:37:22.483Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/00/d2ff3db1f562c8a4acff7e766da3726285b9a2767e0b6c356e94f0563fd5/docling_parse-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:882d4904f6c7889a8256213e882fe41a5b1b3fb3d1d044c162cfffa019a2e42e", size = 9316100, upload-time = "2026-08-11T09:37:24.354Z" },
+]
+
+[[package]]
+name = "docling-slim"
+version = "2.119.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "docling-core" },
+ { name = "filetype" },
+ { name = "pluggy" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "requests" },
+ { name = "tqdm" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/08/8b/7eb40b83cb91ecd899faeaf421c81df20054053e3cc64cafbd6ce0c530cd/docling_slim-2.119.0.tar.gz", hash = "sha256:7b4ee3891e536403f07b6ab702bc757d6c3e4fe146475656d97cf911ac96db8f", size = 582133, upload-time = "2026-08-10T10:27:35.891Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/09/74f745d6178dbec12eb503261f87b0bb947fac5b122cea2e6d3c433ce7ae/docling_slim-2.119.0-py3-none-any.whl", hash = "sha256:ca385bc6b0ca99a4f0bba07feaa4c0fc4d5fa685a22184baa79de4bd87598323", size = 723087, upload-time = "2026-08-10T10:27:34.074Z" },
+]
+
+[package.optional-dependencies]
+standard = [
+ { name = "accelerate" },
+ { name = "beautifulsoup4" },
+ { name = "defusedxml" },
+ { name = "docling-core", extra = ["chunking"] },
+ { name = "docling-ibm-models" },
+ { name = "docling-parse" },
+ { name = "httpx" },
+ { name = "huggingface-hub" },
+ { name = "mail-parser" },
+ { name = "marko" },
+ { name = "numpy" },
+ { name = "openpyxl" },
+ { name = "pillow" },
+ { name = "polyfactory" },
+ { name = "pylatexenc" },
+ { name = "pypdfium2" },
+ { name = "python-docx" },
+ { name = "python-dotenv" },
+ { name = "python-oxmsg" },
+ { name = "python-pptx" },
+ { name = "rapidocr" },
+ { name = "rich" },
+ { name = "rtree" },
+ { name = "scipy" },
+ { name = "torch" },
+ { name = "torchvision" },
+ { name = "typer" },
+ { name = "websockets" },
+]
+
+[[package]]
+name = "et-xmlfile"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
+]
+
+[[package]]
+name = "faker"
+version = "40.36.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/d2/026af1e002bbc6df534d1f8262b18ec79a974f928e9290bfbfdfe7c7b2af/faker-40.36.0.tar.gz", hash = "sha256:754048c76c03afa7de83eee8f4bcee3cf668cbb7d995f54a4e9678db7f110308", size = 2025903, upload-time = "2026-07-24T21:11:33.088Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/50/9a/b947ed175ce9a0dcb070ccf3607f0ce8720cfb5ed1a36166a150b2acd5af/faker-40.36.0-py3-none-any.whl", hash = "sha256:82b9497d9cfe017048075bcf969298a74b1b6e39f5e4dad1211085d1133f7b62", size = 2062829, upload-time = "2026-07-24T21:11:31.37Z" },
+]
+
[[package]]
name = "filelock"
version = "3.32.2"
@@ -138,6 +665,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" },
]
+[[package]]
+name = "filetype"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" },
+]
+
+[[package]]
+name = "flatbuffers"
+version = "25.12.19"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" },
+]
+
[[package]]
name = "fsspec"
version = "2026.7.0"
@@ -228,6 +772,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/de/d8/95b735e183957c1f26d94c52977f09d466d55119cbbc1558ea4975e4c216/huggingface_hub-1.27.0-py3-none-any.whl", hash = "sha256:7df6827c2f956c60fbaa64646e979e566db76f619dd0a9729dfb8c5a3eb4f68d", size = 784926, upload-time = "2026-08-07T12:48:02.905Z" },
]
+[[package]]
+name = "humanfriendly"
+version = "10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyreadline3", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" },
+]
+
[[package]]
name = "idna"
version = "3.18"
@@ -258,6 +814,54 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
]
+[[package]]
+name = "jsonlines"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/35/87/bcda8e46c88d0e34cad2f09ee2d0c7f5957bccdb9791b0b934ec84d84be4/jsonlines-4.0.0.tar.gz", hash = "sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74", size = 11359, upload-time = "2023-09-01T12:34:44.187Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/62/d9ba6323b9202dd2fe166beab8a86d29465c41a0288cbe229fac60c1ab8d/jsonlines-4.0.0-py3-none-any.whl", hash = "sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55", size = 8701, upload-time = "2023-09-01T12:34:42.563Z" },
+]
+
+[[package]]
+name = "jsonref"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" },
+]
+
+[[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 = "ladybug"
version = "0.19.1"
@@ -291,38 +895,234 @@ wheels = [
]
[[package]]
-name = "markupsafe"
-version = "3.0.3"
+name = "latex2mathml"
+version = "3.81.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/3b/62/35bb816c5c19d4d0cde5bdfb82ebb996306243d5f94e03f201658c629960/latex2mathml-3.81.0.tar.gz", hash = "sha256:4b959cdc3cac8686bc0e3e5aece8127dfb1b81ca1241bed8e00ef31b82bb4022", size = 77584, upload-time = "2026-04-15T00:55:27.977Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
- { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
- { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
- { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
- { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
- { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
- { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
- { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
- { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
- { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
- { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
- { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
- { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
- { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
- { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
- { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
- { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
- { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
- { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
- { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
- { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
- { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
- { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
- { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
- { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
- { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
- { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/b1/c488b530994c4f68e46efa99a4d6ca6741aaf158e35779fe6c4d8a9a427d/latex2mathml-3.81.0-py3-none-any.whl", hash = "sha256:d317710393fe20579aea39cfe8928fa2ad9b8780896e585326c75e89c1d1d1a4", size = 79185, upload-time = "2026-04-15T00:55:29.301Z" },
+]
+
+[[package]]
+name = "lxml"
+version = "6.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821, upload-time = "2026-05-18T19:17:42.068Z" },
+ { url = "https://files.pythonhosted.org/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252, upload-time = "2026-05-18T19:17:47.897Z" },
+ { url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" },
+ { url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" },
+ { url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" },
+ { url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" },
+ { url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171, upload-time = "2026-05-18T19:18:52.779Z" },
+ { url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" },
+ { url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" },
+ { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" },
+ { url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" },
+ { url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610, upload-time = "2026-05-19T19:22:50.843Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" },
+ { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" },
+ { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" },
+ { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" },
+ { url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" },
+ { url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" },
+ { url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" },
+ { url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" },
+ { url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" },
+ { url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" },
+ { url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" },
+ { url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" },
+ { url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" },
+ { url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" },
+ { url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" },
+ { url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" },
+ { url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" },
+]
+
+[[package]]
+name = "magika"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "numpy" },
+ { name = "onnxruntime" },
+ { name = "python-dotenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/f3/3d1dcdd7b9c41d589f5cff252d32ed91cdf86ba84391cfc81d9d8773571d/magika-0.6.3.tar.gz", hash = "sha256:7cc52aa7359af861957043e2bf7265ed4741067251c104532765cd668c0c0cb1", size = 3042784, upload-time = "2025-10-30T15:22:34.499Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/e4/35c323beb3280482c94299d61626116856ac2d4ec16ecef50afc4fdd4291/magika-0.6.3-py3-none-any.whl", hash = "sha256:eda443d08006ee495e02083b32e51b98cb3696ab595a7d13900d8e2ef506ec9d", size = 2969474, upload-time = "2025-10-30T15:22:25.298Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8f/132b0d7cd51c02c39fd52658a5896276c30c8cc2fd453270b19db8c40f7e/magika-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:86901e64b05dde5faff408c9b8245495b2e1fd4c226e3393d3d2a3fee65c504b", size = 13358841, upload-time = "2025-10-30T15:22:27.413Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/03/5ed859be502903a68b7b393b17ae0283bf34195cfcca79ce2dc25b9290e7/magika-0.6.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3d9661eedbdf445ac9567e97e7ceefb93545d77a6a32858139ea966b5806fb64", size = 15367335, upload-time = "2025-10-30T15:22:29.907Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/9e/f8ee7d644affa3b80efdd623a3d75865c8f058f3950cb87fb0c48e3559bc/magika-0.6.3-py3-none-win_amd64.whl", hash = "sha256:e57f75674447b20cab4db928ae58ab264d7d8582b55183a0b876711c2b2787f3", size = 12692831, upload-time = "2025-10-30T15:22:32.063Z" },
+]
+
+[[package]]
+name = "mail-parser"
+version = "4.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/62/54e04e45102517d564142e1cc3e0fa4a9a9db32f1c5dd2aff82b02f47220/mail_parser-4.6.0.tar.gz", hash = "sha256:d4251ff1eef58bf2ed2d39007e83762e8edd6be2df01334ff24aad9c1a75605d", size = 2859373, upload-time = "2026-08-09T08:44:29.811Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b1/7c/d30573d54c9ae44945feca9085125c307748f9a7727e053bac9ffab708fc/mail_parser-4.6.0-py3-none-any.whl", hash = "sha256:18f02d80d2e561c992514d1cc602b98a6a2c3fa5911c415bb291492cc43826e7", size = 36979, upload-time = "2026-08-09T08:44:28.463Z" },
+]
+
+[[package]]
+name = "mammoth"
+version = "1.11.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cobble" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ed/3c/a58418d2af00f2da60d4a51e18cd0311307b72d48d2fffec36a97b4a5e44/mammoth-1.11.0.tar.gz", hash = "sha256:a0f59e442f34d5b6447f4b0999306cbf3e67aaabfa8cb516f878fb1456744637", size = 53142, upload-time = "2025-09-19T10:35:20.373Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ca/54/2e39566a131b13f6d8d193f974cb6a34e81bb7cc2fa6f7e03de067b36588/mammoth-1.11.0-py2.py3-none-any.whl", hash = "sha256:c077ab0d450bd7c0c6ecd529a23bf7e0fa8190c929e28998308ff4eada3f063b", size = 54752, upload-time = "2025-09-19T10:35:18.699Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
+]
+
+[[package]]
+name = "markdownify"
+version = "1.2.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "beautifulsoup4" },
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/92/ab/d1297139c0e2ceb151ae564c8c4f57ac0155d8f1f8b4cbd5d6523c82ea36/markdownify-1.2.3.tar.gz", hash = "sha256:1a176f05522c8a2cb1dd3ab9d307dcdadbed5c26ae717855bfc42b3b6d38d937", size = 18852, upload-time = "2026-06-30T20:27:39.06Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/10/fa543d484e8b1199243fe20eedd02cc5af050edebce98a7293a5773df592/markdownify-1.2.3-py3-none-any.whl", hash = "sha256:a189a0bedfd14009030fde5f85bb6f77c56897cb839b5c25315dd7d4e3e290ba", size = 15732, upload-time = "2026-06-30T20:27:38.094Z" },
+]
+
+[[package]]
+name = "markitdown"
+version = "0.1.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "beautifulsoup4" },
+ { name = "charset-normalizer" },
+ { name = "defusedxml" },
+ { name = "magika" },
+ { name = "markdownify" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/59/93/e8a4af0c47551beb6383e226e840cbc811a577b8096eb385251b3fcc8f62/markitdown-0.1.7.tar.gz", hash = "sha256:4d1f3c69cd43b82288fdc3653686d759dcf355ee7c681aa6a855aed98a1e4f44", size = 51767, upload-time = "2026-07-29T18:20:31.496Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/16/51d269a754d690ec31d3faa0686c8c14ac955dbc0580c358f256ba3391ec/markitdown-0.1.7-py3-none-any.whl", hash = "sha256:4eca912c87c6aa6897284a7f4bf6769a23bccf8544530f5d8b175fbe3797c916", size = 71093, upload-time = "2026-07-29T18:20:30.226Z" },
+]
+
+[package.optional-dependencies]
+docx = [
+ { name = "lxml" },
+ { name = "mammoth" },
+]
+pdf = [
+ { name = "pdfminer-six" },
+ { name = "pdfplumber" },
+]
+pptx = [
+ { name = "python-pptx" },
+]
+xlsx = [
+ { name = "openpyxl" },
+ { name = "pandas" },
+]
+
+[[package]]
+name = "marko"
+version = "2.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/cc/01b80dc58e4d44fe039403ef1ac0008bcb9375364ccd246a4b8bfec29b46/marko-2.2.3.tar.gz", hash = "sha256:e31ec2875383bc62f9093d16babed5a2c2cde601c00d834ea935a2222120ec19", size = 144531, upload-time = "2026-05-28T02:07:39.479Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/97/50/0a8fab45fa374820c27cc4c3178c4914c60902ba9d6404a692a979e20dbc/marko-2.2.3-py3-none-any.whl", hash = "sha256:8e1d7a0387281e59dfbc52a381b58c570156970e36b2bbe047f8a3a2f368cacc", size = 42951, upload-time = "2026-05-28T02:07:38.373Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
+ { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
+ { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
+ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
+ { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
+ { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
+ { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
@@ -353,6 +1153,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
+[[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 = "mistune"
version = "3.3.4"
@@ -379,6 +1188,60 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/a1/38101b223fb6cea0f2e401fbacdf4f6121628d45dc9329b255d7ac843234/model2vec-0.8.2-py3-none-any.whl", hash = "sha256:f0ecfe994316e401dca583fbf6dd22079d308c05717dd36d40bff60f265431cf", size = 54749, upload-time = "2026-05-29T12:01:19.411Z" },
]
+[[package]]
+name = "mpire"
+version = "2.10.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pygments" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "tqdm" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3a/93/80ac75c20ce54c785648b4ed363c88f148bf22637e10c9863db4fbe73e74/mpire-2.10.2.tar.gz", hash = "sha256:f66a321e93fadff34585a4bfa05e95bd946cf714b442f51c529038eb45773d97", size = 271270, upload-time = "2024-05-07T14:00:31.815Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/14/1db1729ad6db4999c3a16c47937d601fcb909aaa4224f5eca5a2f145a605/mpire-2.10.2-py3-none-any.whl", hash = "sha256:d627707f7a8d02aa4c7f7d59de399dec5290945ddf7fbd36cbb1d6ebb37a51fb", size = 272756, upload-time = "2024-05-07T14:00:29.633Z" },
+]
+
+[package.optional-dependencies]
+dill = [
+ { name = "multiprocess" },
+]
+
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
+]
+
+[[package]]
+name = "multiprocess"
+version = "0.70.19"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "dill" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a2/f2/e783ac7f2aeeed14e9e12801f22529cc7e6b7ab80928d6dcce4e9f00922d/multiprocess-0.70.19.tar.gz", hash = "sha256:952021e0e6c55a4a9fe4cd787895b86e239a40e76802a789d6305398d3975897", size = 2079989, upload-time = "2026-01-19T06:47:39.744Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e3/45/8004d1e6b9185c1a444d6b55ac5682acf9d98035e54386d967366035a03a/multiprocess-0.70.19-py310-none-any.whl", hash = "sha256:97404393419dcb2a8385910864eedf47a3cadf82c66345b44f036420eb0b5d87", size = 134948, upload-time = "2026-01-19T06:47:32.325Z" },
+ { url = "https://files.pythonhosted.org/packages/86/c2/dec9722dc3474c164a0b6bcd9a7ed7da542c98af8cabce05374abab35edd/multiprocess-0.70.19-py311-none-any.whl", hash = "sha256:928851ae7973aea4ce0eaf330bbdafb2e01398a91518d5c8818802845564f45c", size = 144457, upload-time = "2026-01-19T06:47:33.711Z" },
+ { url = "https://files.pythonhosted.org/packages/71/70/38998b950a97ea279e6bd657575d22d1a2047256caf707d9a10fbce4f065/multiprocess-0.70.19-py312-none-any.whl", hash = "sha256:3a56c0e85dd5025161bac5ce138dcac1e49174c7d8e74596537e729fd5c53c28", size = 150281, upload-time = "2026-01-19T06:47:35.037Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl", hash = "sha256:8d5eb4ec5017ba2fab4e34a747c6d2c2b6fecfe9e7236e77988db91580ada952", size = 156414, upload-time = "2026-01-19T06:47:35.915Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/61/af9115673a5870fd885247e2f1b68c4f1197737da315b520a91c757a861a/multiprocess-0.70.19-py314-none-any.whl", hash = "sha256:e8cc7fbdff15c0613f0a1f1f8744bef961b0a164c0ca29bdff53e9d2d93c5e5f", size = 160318, upload-time = "2026-01-19T06:47:37.497Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" },
+]
+
+[[package]]
+name = "networkx"
+version = "3.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
+]
+
[[package]]
name = "numpy"
version = "2.5.2"
@@ -453,152 +1316,1650 @@ wheels = [
]
[[package]]
-name = "packaging"
-version = "26.3"
+name = "nvidia-cublas"
+version = "13.1.1.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
+dependencies = [
+ { name = "nvidia-cuda-nvrtc" },
+]
wheels = [
- { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" },
]
[[package]]
-name = "pyyaml"
-version = "6.0.3"
+name = "nvidia-cuda-cupti"
+version = "13.0.85"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
- { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
- { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
- { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
- { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
- { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
- { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
- { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
- { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
- { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
- { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
- { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
- { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
- { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
- { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
- { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
- { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
- { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
- { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
- { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
- { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
- { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
- { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
- { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
- { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
- { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
- { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
- { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
- { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
- { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
- { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
- { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
- { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
- { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
- { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
- { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
- { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
- { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" },
+ { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" },
]
[[package]]
-name = "requests"
-version = "2.34.2"
+name = "nvidia-cuda-nvrtc"
+version = "13.0.88"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-runtime"
+version = "13.0.96"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" },
+]
+
+[[package]]
+name = "nvidia-cudnn-cu13"
+version = "9.20.0.48"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "certifi" },
- { name = "charset-normalizer" },
- { name = "idna" },
- { name = "urllib3" },
+ { name = "nvidia-cublas" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
+ { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" },
]
[[package]]
-name = "safetensors"
-version = "0.8.0"
+name = "nvidia-cufft"
+version = "12.0.0.61"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" }
+dependencies = [
+ { name = "nvidia-nvjitlink" },
+]
wheels = [
- { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" },
- { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" },
- { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" },
- { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" },
- { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" },
- { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" },
- { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" },
- { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" },
- { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" },
- { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" },
- { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" },
- { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" },
- { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" },
- { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" },
- { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" },
- { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" },
]
[[package]]
-name = "tokenizers"
-version = "0.23.1"
+name = "nvidia-cufile"
+version = "1.15.1.6"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" },
+]
+
+[[package]]
+name = "nvidia-curand"
+version = "10.4.0.35"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" },
+]
+
+[[package]]
+name = "nvidia-cusolver"
+version = "12.0.4.66"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "huggingface-hub" },
+ { name = "nvidia-cublas" },
+ { name = "nvidia-cusparse" },
+ { name = "nvidia-nvjitlink" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" },
- { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" },
- { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" },
- { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" },
- { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" },
- { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" },
- { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" },
- { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" },
- { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" },
- { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" },
- { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" },
- { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" },
- { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" },
- { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" },
- { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" },
- { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" },
]
[[package]]
-name = "tqdm"
-version = "4.70.0"
+name = "nvidia-cusparse"
+version = "12.6.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "nvidia-nvjitlink" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" },
]
[[package]]
-name = "typing-extensions"
-version = "4.16.0"
+name = "nvidia-cusparselt-cu13"
+version = "0.8.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
+ { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" },
+ { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" },
]
[[package]]
-name = "urllib3"
-version = "2.7.0"
+name = "nvidia-nccl-cu13"
+version = "2.29.7"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
+ { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" },
+]
+
+[[package]]
+name = "nvidia-nvjitlink"
+version = "13.3.33"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/ee/580ca6f29dcab0221db8706badca1bbbb084f1975c4d4e83329c3a7e31f0/nvidia_nvjitlink-13.3.33-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:26a6de7fb4c8fdaa7703d3dad720d6d427ddfea5c48a528fd97c11733ad830e5", size = 40742423, upload-time = "2026-05-26T16:54:51.613Z" },
+ { url = "https://files.pythonhosted.org/packages/69/30/45414e35ff2eee7db3da037e5707037ccf9d2b5218ffbdb055ea4d5aa98a/nvidia_nvjitlink-13.3.33-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce48b37dfeb3cb1eae4cf85adacb47d7a6539ea2272870c9a3628ce275c2037e", size = 39168635, upload-time = "2026-05-26T16:54:13.906Z" },
+]
+
+[[package]]
+name = "nvidia-nvshmem-cu13"
+version = "3.4.5"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" },
+]
+
+[[package]]
+name = "nvidia-nvtx"
+version = "13.0.85"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" },
+]
+
+[[package]]
+name = "olefile"
+version = "0.47"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240, upload-time = "2023-12-01T16:22:53.025Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload-time = "2023-12-01T16:22:51.518Z" },
+]
+
+[[package]]
+name = "omegaconf"
+version = "2.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "antlr4-python3-runtime" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ce/3d/e4b57b8d9008c6ebe0d5eff901f91d5700cf7bdb8c8863df817463a7fd5e/omegaconf-2.3.1.tar.gz", hash = "sha256:e5e7de64aeebeddaf8e6d3f7a783b32ac2a01c0fbd9c878012caecb891a1f42a", size = 3298472, upload-time = "2026-06-11T05:05:12.885Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/0e/152509871bf30df6fc38569f52a2db9b55dd41aae957adae50a053ac7778/omegaconf-2.3.1-py3-none-any.whl", hash = "sha256:3d701d14e9a8828f1edd28bb70b725908b34277cdd72cf7d6a83f94dadc6b6a0", size = 79502, upload-time = "2026-06-11T05:05:09.954Z" },
+]
+
+[[package]]
+name = "onnxruntime"
+version = "1.20.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coloredlogs" },
+ { name = "flatbuffers" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "protobuf" },
+ { name = "sympy" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/39/9335e0874f68f7d27103cbffc0e235e32e26759202df6085716375c078bb/onnxruntime-1.20.1-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:22b0655e2bf4f2161d52706e31f517a0e54939dc393e92577df51808a7edc8c9", size = 31007580, upload-time = "2024-11-21T00:49:07.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/9d/a42a84e10f1744dd27c6f2f9280cc3fb98f869dd19b7cd042e391ee2ab61/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f56e898815963d6dc4ee1c35fc6c36506466eff6d16f3cb9848cea4e8c8172", size = 11952833, upload-time = "2024-11-21T00:49:10.563Z" },
+ { url = "https://files.pythonhosted.org/packages/47/42/2f71f5680834688a9c81becbe5c5bb996fd33eaed5c66ae0606c3b1d6a02/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb71a814f66517a65628c9e4a2bb530a6edd2cd5d87ffa0af0f6f773a027d99e", size = 13333903, upload-time = "2024-11-21T00:49:12.984Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/f1/aabfdf91d013320aa2fc46cf43c88ca0182860ff15df872b4552254a9680/onnxruntime-1.20.1-cp312-cp312-win32.whl", hash = "sha256:bd386cc9ee5f686ee8a75ba74037750aca55183085bf1941da8efcfe12d5b120", size = 9814562, upload-time = "2024-11-21T00:49:15.453Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/80/76979e0b744307d488c79e41051117634b956612cc731f1028eb17ee7294/onnxruntime-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:19c2d843eb074f385e8bbb753a40df780511061a63f9def1b216bf53860223fb", size = 11331482, upload-time = "2024-11-21T00:49:19.412Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/71/c5d980ac4189589267a06f758bd6c5667d07e55656bed6c6c0580733ad07/onnxruntime-1.20.1-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:cc01437a32d0042b606f462245c8bbae269e5442797f6213e36ce61d5abdd8cc", size = 31007574, upload-time = "2024-11-21T00:49:23.225Z" },
+ { url = "https://files.pythonhosted.org/packages/81/0d/13bbd9489be2a6944f4a940084bfe388f1100472f38c07080a46fbd4ab96/onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb44b08e017a648924dbe91b82d89b0c105b1adcfe31e90d1dc06b8677ad37be", size = 11951459, upload-time = "2024-11-21T00:49:26.269Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/ea/4454ae122874fd52bbb8a961262de81c5f932edeb1b72217f594c700d6ef/onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda6aebdf7917c1d811f21d41633df00c58aff2bef2f598f69289c1f1dabc4b3", size = 13331620, upload-time = "2024-11-21T00:49:28.875Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/e0/50db43188ca1c945decaa8fc2a024c33446d31afed40149897d4f9de505f/onnxruntime-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:d30367df7e70f1d9fc5a6a68106f5961686d39b54d3221f760085524e8d38e16", size = 11331758, upload-time = "2024-11-21T00:49:31.417Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/55/3821c5fd60b52a6c82a00bba18531793c93c4addfe64fbf061e235c5617a/onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9158465745423b2b5d97ed25aa7740c7d38d2993ee2e5c3bfacb0c4145c49d8", size = 11950342, upload-time = "2024-11-21T00:49:34.164Z" },
+ { url = "https://files.pythonhosted.org/packages/14/56/fd990ca222cef4f9f4a9400567b9a15b220dee2eafffb16b2adbc55c8281/onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0df6f2df83d61f46e842dbcde610ede27218947c33e994545a22333491e72a3b", size = 13337040, upload-time = "2024-11-21T00:49:37.271Z" },
+]
+
+[[package]]
+name = "opencv-python"
+version = "5.0.0.93"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/79/4c/a438d23e09ce2033c09f7b784ad2fbdb0adf529e434101ed28f142226f98/opencv_python-5.0.0.93.tar.gz", hash = "sha256:66aac3e5b5faa48d4025816592f3af19e4bfc2c68dec067bae2dbb4ca10aa9e2", size = 81802749, upload-time = "2026-07-02T06:59:53.815Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9c/75/76f6ade78f6102c61034f828e2a22616708df2c9504bc8d6af9dd8f73dc5/opencv_python-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:198a75138241810206a17c829dbcc40a7cb1841cda538ca86cbbfc6c7d95f898", size = 48322443, upload-time = "2026-07-02T05:50:25.466Z" },
+ { url = "https://files.pythonhosted.org/packages/15/8c/bc1bda6aae69a32e9d84fc34153ba104cd25226861eb4aea33b2cea4860d/opencv_python-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:6bbc32f59e1b1a7db7b39c81f63d00625f041d333037fd8702f6da52cc39108b", size = 34782755, upload-time = "2026-07-02T05:51:30.556Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/8a/b04776ec45d2dea08a1b176f1829201db3515d4ed16c35f8fcc9fa7beb16/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2b4272e736836f66c2d176e43ab8101f3a00d45654916399f52e150c58981ac", size = 50614064, upload-time = "2026-07-02T06:53:22.604Z" },
+ { url = "https://files.pythonhosted.org/packages/95/54/eb47866b94f2b5b42dde17644b78055ef1ee05aae59962c7290e55270803/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8b6d0a212253dd26ad338c812f1f23ca118fdf05a9c8c6b9444f161aa8c5881", size = 71064711, upload-time = "2026-07-02T06:54:13.148Z" },
+ { url = "https://files.pythonhosted.org/packages/93/da/962579f1e703cbf8c5422fd1f576467dcb3b5b0b0b81c1471c979764353a/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:08d5d91d967b58d6db86073b2ad3eaef88ca4ebdfd45c9059bf59f5ded0c7ad2", size = 49798576, upload-time = "2026-07-02T06:54:33.781Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c8de2dec111122a02e8beb28e16c31904992dfd6186560b142a92c71403c1039", size = 73783032, upload-time = "2026-07-02T06:55:03.415Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/4b/edaf83b996ca5a1a3d8ccad485706b9c6d4742b13b9c4586bf1c1e7d9423/opencv_python-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:4b4b1a34c79bf8d3738e3cfe9a9e67b51a79663f6b692cbdad8c31f570da4157", size = 35564734, upload-time = "2026-07-02T05:49:57.704Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f0/9fa6e85cb10c8eb36a0222d27e50fe381b86ce49a55446bf39f491727564/opencv_python-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:f90ba04b8f73bc5c3814037699739f0156f597338a98f05956c684e7c3ca10d2", size = 44000345, upload-time = "2026-07-02T05:49:54.971Z" },
+]
+
+[[package]]
+name = "openpyxl"
+version = "3.1.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "et-xmlfile" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
+]
+
+[[package]]
+name = "pandas"
+version = "3.0.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "python-dateutil" },
+ { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" },
+ { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" },
+ { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" },
+ { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" },
+ { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" },
+ { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" },
+ { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" },
+ { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" },
+ { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" },
+ { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" },
+ { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" },
+ { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" },
+ { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" },
+ { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" },
+ { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" },
+]
+
+[[package]]
+name = "pdfminer-six"
+version = "20260107"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "charset-normalizer" },
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/34/a4/5cec1112009f0439a5ca6afa8ace321f0ab2f48da3255b7a1c8953014670/pdfminer_six-20260107.tar.gz", hash = "sha256:96bfd431e3577a55a0efd25676968ca4ce8fd5b53f14565f85716ff363889602", size = 8512094, upload-time = "2026-01-07T13:29:12.937Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/8b/28c4eaec9d6b036a52cb44720408f26b1a143ca9bce76cc19e8f5de00ab4/pdfminer_six-20260107-py3-none-any.whl", hash = "sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9", size = 6592252, upload-time = "2026-01-07T13:29:10.742Z" },
+]
+
+[[package]]
+name = "pdfplumber"
+version = "0.11.10"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pdfminer-six" },
+ { name = "pillow" },
+ { name = "pypdfium2" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/05/56/6f450312ba05a27d7713b73857c1a25100dbda04fbc1331b13fb227a607d/pdfplumber-0.11.10.tar.gz", hash = "sha256:b95b2d28c66efb0a794a83b88c6c6aea5987532a445d20a1cbcfa657022e6e57", size = 102892, upload-time = "2026-06-15T03:31:31.035Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/9a/07d658e1e7fad860f1c541ab941348125dbdab773be3a0afaf32361866c7/pdfplumber-0.11.10-py3-none-any.whl", hash = "sha256:7741ea81bf165b474b153e6789d10d18e06b6ddcf3ec84289c3ef2fed6802580", size = 60047, upload-time = "2026-06-15T03:31:29.702Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "12.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
+ { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
+ { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
+ { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
+ { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
+ { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
+ { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
+ { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
+ { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
+ { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
+ { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
+ { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
+ { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
+ { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
+ { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
+ { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
+ { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
+ { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
+ { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
+ { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
+ { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
+ { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
+ { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
+ { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
+ { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
+ { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
+]
+
+[[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 = "polyfactory"
+version = "3.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "faker" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/85/68/7717bd9e63ed254617a7d3dc9260904fb736d6ea203e58ffddcb186c64e4/polyfactory-3.3.0.tar.gz", hash = "sha256:237258b6ff43edf362ffd1f68086bb796466f786adfa002b0ac256dbf2246e9a", size = 348668, upload-time = "2026-02-22T09:46:28.01Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dd/34/b6f19941adcdaf415b5e8a8d577499f5b6a76b59cbae37f9b125a9ffe9f2/polyfactory-3.3.0-py3-none-any.whl", hash = "sha256:686abcaa761930d3df87b91e95b26b8d8cb9fdbbbe0b03d5f918acff5c72606e", size = 62707, upload-time = "2026-02-22T09:46:25.985Z" },
+]
+
+[[package]]
+name = "protobuf"
+version = "7.35.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" },
+ { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" },
+ { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" },
+]
+
+[[package]]
+name = "psutil"
+version = "7.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" },
+ { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" },
+ { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" },
+ { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" },
+ { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" },
+ { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" },
+ { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" },
+ { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
+]
+
+[[package]]
+name = "pyclipper"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/21/3c06205bb407e1f79b73b7b4dfb3950bd9537c4f625a68ab5cc41177f5bc/pyclipper-1.4.0.tar.gz", hash = "sha256:9882bd889f27da78add4dd6f881d25697efc740bf840274e749988d25496c8e1", size = 54489, upload-time = "2025-12-01T13:15:35.015Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/90/1b/7a07b68e0842324d46c03e512d8eefa9cb92ba2a792b3b4ebf939dafcac3/pyclipper-1.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:222ac96c8b8281b53d695b9c4fedc674f56d6d4320ad23f1bdbd168f4e316140", size = 265676, upload-time = "2025-12-01T13:15:04.15Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/dd/8bd622521c05d04963420ae6664093f154343ed044c53ea260a310c8bb4d/pyclipper-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f3672dbafbb458f1b96e1ee3e610d174acb5ace5bd2ed5d1252603bb797f2fc6", size = 140458, upload-time = "2025-12-01T13:15:05.76Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/06/6e3e241882bf7d6ab23d9c69ba4e85f1ec47397cbbeee948a16cf75e21ed/pyclipper-1.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1f807e2b4760a8e5c6d6b4e8c1d71ef52b7fe1946ff088f4fa41e16a881a5ca", size = 978235, upload-time = "2025-12-01T13:15:06.993Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/f4/3418c1cd5eea640a9fa2501d4bc0b3655fa8d40145d1a4f484b987990a75/pyclipper-1.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce1f83c9a4e10ea3de1959f0ae79e9a5bd41346dff648fee6228ba9eaf8b3872", size = 961388, upload-time = "2025-12-01T13:15:08.467Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/94/c85401d24be634af529c962dd5d781f3cb62a67cd769534df2cb3feee97a/pyclipper-1.4.0-cp312-cp312-win32.whl", hash = "sha256:3ef44b64666ebf1cb521a08a60c3e639d21b8c50bfbe846ba7c52a0415e936f4", size = 95169, upload-time = "2025-12-01T13:15:10.098Z" },
+ { url = "https://files.pythonhosted.org/packages/97/77/dfea08e3b230b82ee22543c30c35d33d42f846a77f96caf7c504dd54fab1/pyclipper-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:d1e5498d883b706a4ce636247f0d830c6eb34a25b843a1b78e2c969754ca9037", size = 104619, upload-time = "2025-12-01T13:15:11.592Z" },
+ { url = "https://files.pythonhosted.org/packages/67/d0/cbce7d47de1e6458f66a4d999b091640134deb8f2c7351eab993b70d2e10/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d49df13cbb2627ccb13a1046f3ea6ebf7177b5504ec61bdef87d6a704046fd6e", size = 264342, upload-time = "2025-12-01T13:15:12.697Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/cc/742b9d69d96c58ac156947e1b56d0f81cbacbccf869e2ac7229f2f86dc4e/pyclipper-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37bfec361e174110cdddffd5ecd070a8064015c99383d95eb692c253951eee8a", size = 139839, upload-time = "2025-12-01T13:15:13.911Z" },
+ { url = "https://files.pythonhosted.org/packages/db/48/dd301d62c1529efdd721b47b9e5fb52120fcdac5f4d3405cfc0d2f391414/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:14c8bdb5a72004b721c4e6f448d2c2262d74a7f0c9e3076aeff41e564a92389f", size = 972142, upload-time = "2025-12-01T13:15:15.477Z" },
+ { url = "https://files.pythonhosted.org/packages/07/bf/d493fd1b33bb090fa64e28c1009374d5d72fa705f9331cd56517c35e381e/pyclipper-1.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2a50c22c3a78cb4e48347ecf06930f61ce98cf9252f2e292aa025471e9d75b1", size = 952789, upload-time = "2025-12-01T13:15:17.042Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/88/b95ea8ea21ddca34aa14b123226a81526dd2faaa993f9aabd3ed21231604/pyclipper-1.4.0-cp313-cp313-win32.whl", hash = "sha256:c9a3faa416ff536cee93417a72bfb690d9dea136dc39a39dbbe1e5dadf108c9c", size = 94817, upload-time = "2025-12-01T13:15:18.724Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/42/0a1920d276a0e1ca21dc0d13ee9e3ba10a9a8aa3abac76cd5e5a9f503306/pyclipper-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d4b2d7c41086f1927d14947c563dfc7beed2f6c0d9af13c42fe3dcdc20d35832", size = 104007, upload-time = "2025-12-01T13:15:19.763Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/20/04d58c70f3ccd404f179f8dd81d16722a05a3bf1ab61445ee64e8218c1f8/pyclipper-1.4.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7c87480fc91a5af4c1ba310bdb7de2f089a3eeef5fe351a3cedc37da1fcced1c", size = 265167, upload-time = "2025-12-01T13:15:20.844Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/2e/a570c1abe69b7260ca0caab4236ce6ea3661193ebf8d1bd7f78ccce537a5/pyclipper-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81d8bb2d1fb9d66dc7ea4373b176bb4b02443a7e328b3b603a73faec088b952e", size = 139966, upload-time = "2025-12-01T13:15:22.036Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/3b/e0859e54adabdde8a24a29d3f525ebb31c71ddf2e8d93edce83a3c212ffc/pyclipper-1.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:773c0e06b683214dcfc6711be230c83b03cddebe8a57eae053d4603dd63582f9", size = 968216, upload-time = "2025-12-01T13:15:23.18Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/6b/e3c4febf0a35ae643ee579b09988dd931602b5bf311020535fd9e5b7e715/pyclipper-1.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bc45f2463d997848450dbed91c950ca37c6cf27f84a49a5cad4affc0b469e39", size = 954198, upload-time = "2025-12-01T13:15:24.522Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/74/728efcee02e12acb486ce9d56fa037120c9bf5b77c54bbdbaa441c14a9d9/pyclipper-1.4.0-cp314-cp314-win32.whl", hash = "sha256:0b8c2105b3b3c44dbe1a266f64309407fe30bf372cf39a94dc8aaa97df00da5b", size = 96951, upload-time = "2025-12-01T13:15:25.79Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/d7/7f4354e69f10a917e5c7d5d72a499ef2e10945312f5e72c414a0a08d2ae4/pyclipper-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:6c317e182590c88ec0194149995e3d71a979cfef3b246383f4e035f9d4a11826", size = 106782, upload-time = "2025-12-01T13:15:26.945Z" },
+ { url = "https://files.pythonhosted.org/packages/63/60/fc32c7a3d7f61a970511ec2857ecd09693d8ac80d560ee7b8e67a6d268c9/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f160a2c6ba036f7eaf09f1f10f4fbfa734234af9112fb5187877efed78df9303", size = 269880, upload-time = "2025-12-01T13:15:28.117Z" },
+ { url = "https://files.pythonhosted.org/packages/49/df/c4a72d3f62f0ba03ec440c4fff56cd2d674a4334d23c5064cbf41c9583f6/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9f11ad133257c52c40d50de7a0ca3370a0cdd8e3d11eec0604ad3c34ba549e9", size = 141706, upload-time = "2025-12-01T13:15:30.134Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/0b/cf55df03e2175e1e2da9db585241401e0bc98f76bee3791bed39d0313449/pyclipper-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:bbc827b77442c99deaeee26e0e7f172355ddb097a5e126aea206d447d3b26286", size = 105308, upload-time = "2025-12-01T13:15:31.225Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/dc/53df8b6931d47080b4fe4ee8450d42e660ee1c5c1556c7ab73359182b769/pyclipper-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04", size = 117608, upload-time = "2025-12-01T13:15:32.69Z" },
+]
+
+[[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.13.4"
+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/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.46.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
+ { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
+ { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
+ { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
+ { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
+ { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
+ { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
+ { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
+ { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
+ { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
+ { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
+ { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
+ { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
+ { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
+ { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
+ { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
+ { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
+ { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
+ { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
+ { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
+ { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
+ { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
+ { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
+ { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
+ { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.15.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" },
+]
+
+[[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 = "pylatexenc"
+version = "2.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/45/ddb0fb04acf95fe9cf9c369814dbdd08651bd2c9ee455f142651e06f4488/pylatexenc-2.11.tar.gz", hash = "sha256:305a072a99ce736246049c9da05841b9d718c0f7ea8888f5f596cf15cb621053", size = 165743, upload-time = "2026-07-25T17:26:31.534Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/06/3d67bd912ef337aa4856b466121d03f304aa1bb4d804f9298b6227cd227e/pylatexenc-2.11-py2.py3-none-any.whl", hash = "sha256:e78e7391d6c104f1ed150e21cfaa58016cdb50aa54406a2eecb793649ffdfdd0", size = 137533, upload-time = "2026-07-25T17:26:30.141Z" },
+]
+
+[[package]]
+name = "pypdfium2"
+version = "5.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/db/42/0b51bdf50ccf13f3deb3209ca996179a49761dc191748469cf0de55b0055/pypdfium2-5.12.1.tar.gz", hash = "sha256:d0e0648fb2e28f50efcd1ec0a5a18ced9f4d66b2c227fae9b603f0a883b2d13f", size = 274428, upload-time = "2026-07-17T10:01:22.713Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/09/7e/bd8df53b1131c582f6646372047b49162032bd01628d49b9a60cd94d2181/pypdfium2-5.12.1-py3-none-android_23_arm64_v8a.whl", hash = "sha256:05bab9b1ba2de7fc299ae2af25cb9c8a0543bc8bb893e879fe8c9ba8310e9ce4", size = 3392276, upload-time = "2026-07-17T10:00:47.376Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/13/a2b71e17b0439d2af78c817a381e3557371c6c56098581da8485aef65ea6/pypdfium2-5.12.1-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:d4ee061e566a6422b660cdddaaa799a2d1cbf2f016921bcaf24d61426d01d942", size = 2848776, upload-time = "2026-07-17T10:00:49.09Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/a8/d7a61700db3022792b28bce5264be90a7d2e7104998362af6b93766f50b2/pypdfium2-5.12.1-py3-none-macosx_13_0_arm64.whl", hash = "sha256:66a9ed40d70a5d728cd42148fecb9d7a0917c6161d6bb67c844093a4ed1df089", size = 3480243, upload-time = "2026-07-17T10:00:50.674Z" },
+ { url = "https://files.pythonhosted.org/packages/01/2c/d7a38fad74b6da0947cf8763aee0f8e6c9d3c12fc8e137aa615f7f8ae76c/pypdfium2-5.12.1-py3-none-macosx_13_0_x86_64.whl", hash = "sha256:847378a5ab41332998b2621b21bab2e96dc8c3eff36a08bce26695b964163983", size = 3643490, upload-time = "2026-07-17T10:00:52.236Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/29/aca739676323558595fcf8cdc8d7939d2b25aaaa6f538e829f1fee938cdd/pypdfium2-5.12.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6eabf028ad8e7bc7811c9acf3a72718c180569b624b844d2c6cc974609784275", size = 3649734, upload-time = "2026-07-17T10:00:53.776Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/e0/e4ecc05f4f1a11d11c8d684a24b2fc8be8207f341be985dac301caa4f6aa/pypdfium2-5.12.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7857cfa6642ec5a09db12ff8f5cf6b6494585b5e3a605399fddc4fb862837b63", size = 3380828, upload-time = "2026-07-17T10:00:55.377Z" },
+ { url = "https://files.pythonhosted.org/packages/17/1b/c94c9d486791276e736350917a11fe2cf3acba2c6c7f03f9ac0d51f8952a/pypdfium2-5.12.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05bfa20a08a96584253bbe38b60e13f81a037eac31c5579e607ec1480ad25dbf", size = 3777202, upload-time = "2026-07-17T10:00:57.212Z" },
+ { url = "https://files.pythonhosted.org/packages/14/aa/7f81f0c035fc32850dfab9daf78530814f215be226dad7491e3caa0a3e8c/pypdfium2-5.12.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f059f7bdbdf4352eb83691071096940d769d6ae5930b8734237fdb1bd78fbc2", size = 4186083, upload-time = "2026-07-17T10:00:59.022Z" },
+ { url = "https://files.pythonhosted.org/packages/23/16/21420a6f2bc5f981299c336817dd5d72709dad5fda30ac38cbd5f0f7b372/pypdfium2-5.12.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e10cbf41b21233ec5e20adfc170cf60edd77abead86a97dc708fff55a8a886c7", size = 3701734, upload-time = "2026-07-17T10:01:00.952Z" },
+ { url = "https://files.pythonhosted.org/packages/36/a1/bb89f49e2b3ea3e945b67859d2ec6e73e722a83c006099c675692641e51d/pypdfium2-5.12.1-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07eeebb2784f4cd38d386b924235df43217a397442796673296bb6efbdaad1d0", size = 4030403, upload-time = "2026-07-17T10:01:02.568Z" },
+ { url = "https://files.pythonhosted.org/packages/80/a7/cea5eb0c39e9c6fdf9853a3008bf021d08f962228073af90354b60c5ccdc/pypdfium2-5.12.1-py3-none-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c3e6cbe43581af79526184643920ab03a9401a0c79f2226bea9d4d1e3d34008", size = 3994411, upload-time = "2026-07-17T10:01:04.25Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/43/5e470213b27c13b0d94d03d97fc3740507edadea1d1e2ce2049bfbec4aa0/pypdfium2-5.12.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4648f0905441bcb141687ca2263bbf38a1aa056b943eef06019f91cff3e1da4a", size = 4993687, upload-time = "2026-07-17T10:01:05.811Z" },
+ { url = "https://files.pythonhosted.org/packages/db/da/af7972ca72f24ed720db6501333fd67bc66aa6aa5a5ec698551bd30ae62e/pypdfium2-5.12.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bdff622181fab64f32328591c9c8287cdc745c9a1f2afc26ca3feba39e3e6645", size = 4534560, upload-time = "2026-07-17T10:01:07.291Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/63/06a7f2cd691f7e336cbc53fd65453fee516042c8ddb016bc50d1cd2bed45/pypdfium2-5.12.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:236dbdc88aa54f14b27937ccb2ebe3dcf08c10dbb8652f432ea982dc9af39732", size = 5237681, upload-time = "2026-07-17T10:01:08.997Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/f5/937b080671758ab0b3d3d69b2657006682e4c6a0134b36774be4ed1afcfb/pypdfium2-5.12.1-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:9c8856ce7dd77a7827476c7d75afe1197d6cd505f5cb4167b6aacf661f3f8ea5", size = 5143027, upload-time = "2026-07-17T10:01:10.69Z" },
+ { url = "https://files.pythonhosted.org/packages/32/02/094632700c24728fa443dc89d9d1c6e4fc05bb00778b22e8482fdb133da0/pypdfium2-5.12.1-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:5f257bb40fa44ce9ba18d2c919777dbd3f16bf22548b1d68fd56c7c92f1de530", size = 4647048, upload-time = "2026-07-17T10:01:12.559Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/d0/12d84bf55a4fcf2c0ed242afc94168933194f672067e1d162aa60a8e4426/pypdfium2-5.12.1-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:974082344172da76a5c3c0782eaedfe6069dbe88db77d8c671ef36b61e9b14e2", size = 5088747, upload-time = "2026-07-17T10:01:14.42Z" },
+ { url = "https://files.pythonhosted.org/packages/92/9c/92a460bac1f6cfd6f96251802b3098a804fb251dfe0b5eb004ede958ae0e/pypdfium2-5.12.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:715ae16b34ea1d64884d58800155179ba700e9ea65a2f583b020666acd2bfb12", size = 5049695, upload-time = "2026-07-17T10:01:15.961Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/67/53c61d366222550220b42a9212131407f49d3dbcf050178c620bf80fa899/pypdfium2-5.12.1-py3-none-win32.whl", hash = "sha256:e5358d2ce4ebc5c899aab1df9ca5d215357244e9168aa443225d3c1e649c7eac", size = 3725466, upload-time = "2026-07-17T10:01:17.773Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/c3/08b62718faf2f6b6aa49207626e3113ff3ec1b3cd076c0ef8fd852f0e57c/pypdfium2-5.12.1-py3-none-win_amd64.whl", hash = "sha256:9609be73a6701a68f29dffe0335f7a2e4b3ba581542ed65d35d49f761a4600ca", size = 3859845, upload-time = "2026-07-17T10:01:19.417Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/d5/ad551e55790134c8bc87ea16e0866a3721def0620fbfa19112c8ad4a25a6/pypdfium2-5.12.1-py3-none-win_arm64.whl", hash = "sha256:afc0b7e0c975a429abc75875209ce17b66d749f6ac5cbe8ba72470e83901e304", size = 3674605, upload-time = "2026-07-17T10:01:21.008Z" },
+]
+
+[[package]]
+name = "pyreadline3"
+version = "3.5.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/6d/f94028646d7bbe6d9d873c47ee7c246f2d29129d253f0d96cb6fcab70733/pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf", size = 100368, upload-time = "2026-05-14T17:55:04.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d", size = 85243, upload-time = "2026-05-14T17:55:03.262Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "python-docx"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "lxml" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" },
+]
+
+[[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-oxmsg"
+version = "0.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "olefile" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a2/4e/869f34faedbc968796d2c7e9837dede079c9cb9750917356b1f1eda926e9/python_oxmsg-0.0.2.tar.gz", hash = "sha256:a6aff4deb1b5975d44d49dab1d9384089ffeec819e19c6940bc7ffbc84775fad", size = 34713, upload-time = "2025-02-03T17:13:47.415Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/53/67/f56c69a98c7eb244025845506387d0f961681657c9fcd8b2d2edd148f9d2/python_oxmsg-0.0.2-py3-none-any.whl", hash = "sha256:22be29b14c46016bcd05e34abddfd8e05ee82082f53b82753d115da3fc7d0355", size = 31455, upload-time = "2025-02-03T17:13:46.061Z" },
+]
+
+[[package]]
+name = "python-pptx"
+version = "1.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "lxml" },
+ { name = "pillow" },
+ { name = "typing-extensions" },
+ { name = "xlsxwriter" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297, upload-time = "2024-08-07T17:33:37.772Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" },
+]
+
+[[package]]
+name = "pywin32"
+version = "312"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" },
+ { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" },
+ { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" },
+ { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "rapidocr"
+version = "3.9.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorlog" },
+ { name = "numpy" },
+ { name = "omegaconf" },
+ { name = "opencv-python" },
+ { name = "pillow" },
+ { name = "pyclipper" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "shapely" },
+ { name = "six" },
+ { name = "tqdm" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/ed/0ee9b9281986974be9d2406ae0134c8d7c91d2fc613f16ffda9701eeda6f/rapidocr-3.9.2-py3-none-any.whl", hash = "sha256:04d6b8d151f823d930bd91910555f57bea897c0c44fa6794267b94cf9c1ef9a0", size = 27275208, upload-time = "2026-07-21T10:59:01.599Z" },
+]
+
+[[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 = "regex"
+version = "2026.7.19"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5", size = 416440, upload-time = "2026-07-19T00:19:48.923Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/b9/d11d7e501ac8fd7d617684423ebb9561e0b998481c1e4cbc0cb212c5d74a/regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d", size = 496778, upload-time = "2026-07-19T00:17:05.677Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/a9/a5ab6f312f24318019170dc485d5421fe4f89e43a98640da50d95a8a7041/regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd", size = 297122, upload-time = "2026-07-19T00:17:07.59Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/63/4cab4d7f2d384a144d420b763d97674cb70619c878ea6fcd7640d0e62143/regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6", size = 292009, upload-time = "2026-07-19T00:17:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/22/85/102a81b218298957d4ea7d2f084fae537a71add9d6ff93c8e67284c5f45e/regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797", size = 796708, upload-time = "2026-07-19T00:17:11.542Z" },
+ { url = "https://files.pythonhosted.org/packages/78/b5/dc136af5629938a037cd2b304c12240e132ec92f38be8ff9cc89af2a1f2d/regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18", size = 865651, upload-time = "2026-07-19T00:17:13.312Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/75/67402ae3cd9c8c988a4c805d15ee3eef015e7ca4cb112cf3e640fc1f4153/regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511", size = 911756, upload-time = "2026-07-19T00:17:15.015Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/8e/096d00c7c480ef2ff4265349b14e2261d4ab787ba1f74e2e80d1c58079c3/regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68", size = 801798, upload-time = "2026-07-19T00:17:17.208Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/41/e7ecac6edb5722417f85cc67eaf386322fbe8acf6918ec2fdc37c20dd9d0/regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11", size = 776933, upload-time = "2026-07-19T00:17:19.347Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/69/03c9b3f058d66403e0ca2c938696e81d51cd4c6d47ec5265f02f96948d9a/regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986", size = 784338, upload-time = "2026-07-19T00:17:21.057Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/f7/b38ab3d43f284afbb618fcd15d0e77eb786ae461ce1f6bc7494619ddc0f2/regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b", size = 860452, upload-time = "2026-07-19T00:17:23.119Z" },
+ { url = "https://files.pythonhosted.org/packages/15/5c/ff60ef0571121714f3cf9920bc183071e384a10b556d042e0fdb06cc07a5/regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb", size = 765958, upload-time = "2026-07-19T00:17:24.81Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/0f/bd34021162c0ab47f9a315bd56cd5642e920c8e5668a75ef6c6a6fca590d/regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035", size = 851765, upload-time = "2026-07-19T00:17:26.993Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/20/a2ca43edade0595cccfdc98636739f536d9e26898e7dbddc2b9e98898953/regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a", size = 789714, upload-time = "2026-07-19T00:17:28.699Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/47/e02db4015d424fc83c00ea0ac8c5e5ec14397943de9abf909d5ce3a25931/regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5", size = 267157, upload-time = "2026-07-19T00:17:31.051Z" },
+ { url = "https://files.pythonhosted.org/packages/08/8e/c780c131f79b42ed22d1bd7da4096c2c35f813e835acd02ef0f018bd892c/regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312", size = 277777, upload-time = "2026-07-19T00:17:32.848Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/4c/e4d7e086449bdf379d89774bf1f89dc4a41943f3c5a6125a03905b34b5fb/regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d", size = 277136, upload-time = "2026-07-19T00:17:34.803Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/3d/84165e4299ff76f3a40fe1f2abf939e976f693383a08d2beea6af62bd2c1/regex-2026.7.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40", size = 496552, upload-time = "2026-07-19T00:17:36.808Z" },
+ { url = "https://files.pythonhosted.org/packages/02/a2/a65293e6e4cf28eb7ee1be5335a5386c40d6742e9f47fafc8fec785e16c7/regex-2026.7.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38", size = 296983, upload-time = "2026-07-19T00:17:38.816Z" },
+ { url = "https://files.pythonhosted.org/packages/95/47/2d0564e93d87bc48618360ddca232a2ca612bbdf53ce8465d45ca5ce14ee/regex-2026.7.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11", size = 291832, upload-time = "2026-07-19T00:17:40.726Z" },
+ { url = "https://files.pythonhosted.org/packages/07/cd/42dfbabff3dfc9603c501c0e2e2c5adbb09d127b267bf5348de0af338c15/regex-2026.7.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13", size = 796775, upload-time = "2026-07-19T00:17:42.382Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5d/f6a4839f2b934e3eed5973fd07f5929ee97d4c98939fb275ea23c274ee16/regex-2026.7.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae", size = 865687, upload-time = "2026-07-19T00:17:44.185Z" },
+ { url = "https://files.pythonhosted.org/packages/14/b0/b47d6c36049bc59806a50bd4c86ced70bbe058d787f80281b1d7a9b0e024/regex-2026.7.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da", size = 911962, upload-time = "2026-07-19T00:17:46.442Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/be/ff61f28f9273658cfe23acbbac5217221f6519960ed401e61dfdab12bc35/regex-2026.7.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15", size = 801817, upload-time = "2026-07-19T00:17:48.25Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/bb/8b4f7f26b333f9f79e1b453613c39bb4776f51d38ae66dd0ba31d6b354ca/regex-2026.7.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f", size = 776908, upload-time = "2026-07-19T00:17:50.183Z" },
+ { url = "https://files.pythonhosted.org/packages/09/13/610110fc5921d380516d03c26b652555f08aa0d23ea78a771231873c3638/regex-2026.7.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939", size = 784426, upload-time = "2026-07-19T00:17:52.454Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/f5/1ef9e2a83a5947c57ebff0b377cb5727c3d5ec1992317a320d035cd0dbb6/regex-2026.7.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96", size = 860600, upload-time = "2026-07-19T00:17:54.229Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/02/073af33a3ec149241d11c80acea91e722aa0adbf05addd50f251c4fe89c3/regex-2026.7.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220", size = 765950, upload-time = "2026-07-19T00:17:56.041Z" },
+ { url = "https://files.pythonhosted.org/packages/81/a9/d1e9f819dc394a568ef370cd56cf25394e957a2235f8370f23b576e5a475/regex-2026.7.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc", size = 851794, upload-time = "2026-07-19T00:17:57.897Z" },
+ { url = "https://files.pythonhosted.org/packages/03/3a/8ae83eda7579feacdf984e71fb9e70635fb6f832eeddca58427ec4fca926/regex-2026.7.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2", size = 789845, upload-time = "2026-07-19T00:17:59.97Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/23/c195cbfe5a75fdec64d8f6554fd15237b837919d2c61bdc141d7c807b08b/regex-2026.7.19-cp313-cp313-win32.whl", hash = "sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404", size = 267135, upload-time = "2026-07-19T00:18:01.958Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/80/a11de8404b7272b70acb45c1c05987cce60b45d5693da2e176f0e390d564/regex-2026.7.19-cp313-cp313-win_amd64.whl", hash = "sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e", size = 277747, upload-time = "2026-07-19T00:18:04.121Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/29/0f5c8eff1b4f1f3d83276d365fccecf666afcc7d947420943bf394d07adb/regex-2026.7.19-cp313-cp313-win_arm64.whl", hash = "sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8", size = 277129, upload-time = "2026-07-19T00:18:05.821Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/4c/44b74742052cedda40f9ae469532a037112f7311a36669a891fba8984bb0/regex-2026.7.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2", size = 501134, upload-time = "2026-07-19T00:18:07.567Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/45/bbd038b5e39ee5613a5a689290145b40058cc152c41de9cc23639d2b9734/regex-2026.7.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda", size = 299418, upload-time = "2026-07-19T00:18:09.38Z" },
+ { url = "https://files.pythonhosted.org/packages/65/38/c5bde94b4cedfd5850d64c3f08222d8e1600e84f6ee71d9b44b4b8163f74/regex-2026.7.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff", size = 294486, upload-time = "2026-07-19T00:18:11.188Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/6a/2f5e107cb26c960b781967178899daf2787a7ab151844ed3c01d6fc95474/regex-2026.7.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1", size = 811643, upload-time = "2026-07-19T00:18:12.975Z" },
+ { url = "https://files.pythonhosted.org/packages/37/d4/a2f963406d7d73a62eed84ba05a258afb6cad1b21aa4517443ce40506b78/regex-2026.7.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf", size = 871081, upload-time = "2026-07-19T00:18:14.733Z" },
+ { url = "https://files.pythonhosted.org/packages/45/a3/44be546340bedb15f13063f5e7fe16793ea4d9ea2e805d09bd174ac27724/regex-2026.7.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732", size = 917372, upload-time = "2026-07-19T00:18:16.724Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/f6/e0870b0fd2a40dba0074e4b76e514b21313d37946c9248453e34ec43923e/regex-2026.7.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a", size = 816089, upload-time = "2026-07-19T00:18:18.617Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/27/957e8e22690ad6634572b39b71f130a6105f4d0718bb16849eac00fff147/regex-2026.7.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba", size = 785206, upload-time = "2026-07-19T00:18:20.464Z" },
+ { url = "https://files.pythonhosted.org/packages/76/a4/186e410941e731037c01166069ab86da9f65e8f8110c18009ccf4bd623ee/regex-2026.7.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc", size = 800431, upload-time = "2026-07-19T00:18:22.716Z" },
+ { url = "https://files.pythonhosted.org/packages/73/9f/e4e10e023d291d64a33e246610b724493bf1ce98e0e59c9b7c837e5acfb7/regex-2026.7.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62", size = 864906, upload-time = "2026-07-19T00:18:24.772Z" },
+ { url = "https://files.pythonhosted.org/packages/24/57/ccb20b6be5f1f52a053d1ba2a8f7a077edb9d918248b8490d7506c6832b3/regex-2026.7.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1", size = 773559, upload-time = "2026-07-19T00:18:27.008Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/82/f3b263cf8fad927dc102891da8502e718b7ff9d19af7a2a07c03865d7188/regex-2026.7.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e", size = 857739, upload-time = "2026-07-19T00:18:29.107Z" },
+ { url = "https://files.pythonhosted.org/packages/47/2e/1687bd1b6c2aed5e672ccf845fc11557821fe7366d921b50889ea5ce57bf/regex-2026.7.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0", size = 804522, upload-time = "2026-07-19T00:18:31.362Z" },
+ { url = "https://files.pythonhosted.org/packages/76/7c/cc4e7655181b2d9235b704f2c5e19d8eff002bbc437bae59baee0e381aca/regex-2026.7.19-cp313-cp313t-win32.whl", hash = "sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4", size = 269141, upload-time = "2026-07-19T00:18:33.479Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/14/961b4c7b05a2391c32dbc85e27773076671ef8f97f36cec70fe414734c02/regex-2026.7.19-cp313-cp313t-win_amd64.whl", hash = "sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974", size = 280036, upload-time = "2026-07-19T00:18:35.419Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/67/795644550d788ddbb6dc458c95895f8009978ea6d6ea76b005eb3f45e8c9/regex-2026.7.19-cp313-cp313t-win_arm64.whl", hash = "sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d", size = 279394, upload-time = "2026-07-19T00:18:37.454Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/25/0c4c452f8ef3efe456745b2f33195f5904b573fb4c2ff3f0cb9ec188461e/regex-2026.7.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd", size = 496750, upload-time = "2026-07-19T00:18:39.633Z" },
+ { url = "https://files.pythonhosted.org/packages/24/9e/b70ca6c1704f6c7cd32a9e143c86cc5968d10981eca284bad670c245ea7d/regex-2026.7.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac", size = 297093, upload-time = "2026-07-19T00:18:41.583Z" },
+ { url = "https://files.pythonhosted.org/packages/87/74/0b692da2520d51fbff19c88b83d97e4c702909dd02386c585998b7e2dbed/regex-2026.7.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5", size = 292043, upload-time = "2026-07-19T00:18:43.347Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/a7/1d478e614016045a33feae57446215f9fd65b665a5ceb2f891fb3183bc52/regex-2026.7.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3", size = 797214, upload-time = "2026-07-19T00:18:45.362Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ae/11b9c9411d92c30e3d2db32df5a31133e4a99a8fc397a604fd08f6c4bffb/regex-2026.7.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053", size = 866433, upload-time = "2026-07-19T00:18:47.315Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/62/2b2efc4992f91d6d204b24c647c9f9412e85379d92b7c0ab9fdae622327e/regex-2026.7.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b", size = 911360, upload-time = "2026-07-19T00:18:49.588Z" },
+ { url = "https://files.pythonhosted.org/packages/14/71/986ceea9aa3da548bf1357cad89b63915ec6d21ec957c8113b29ece567df/regex-2026.7.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a", size = 801275, upload-time = "2026-07-19T00:18:51.767Z" },
+ { url = "https://files.pythonhosted.org/packages/15/be/ce9d9534b2cda96eab32c548261224b9b4e220a4126f098f60f42ae7b4cd/regex-2026.7.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1", size = 777131, upload-time = "2026-07-19T00:18:54.053Z" },
+ { url = "https://files.pythonhosted.org/packages/61/2b/58b5c710f2c3929515a25f3a1ca0dad0dcd4518d4fff3cf23bc7adb8dcd2/regex-2026.7.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e", size = 785020, upload-time = "2026-07-19T00:18:56.579Z" },
+ { url = "https://files.pythonhosted.org/packages/84/03/5fe091935b74f15fe0f97998c215cae418d1c0413f6258c7d4d2e83aa37f/regex-2026.7.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12", size = 861263, upload-time = "2026-07-19T00:18:58.64Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/fa/d60bf82e10841eef62a9e32aac401468f05fddfbcb2942e342b1ba3d2433/regex-2026.7.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2", size = 766199, upload-time = "2026-07-19T00:19:00.705Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/5d/11e64d151b0662b81d6bf644c74dc118d461df85bdf2577fadbbf751788a/regex-2026.7.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97", size = 851317, upload-time = "2026-07-19T00:19:03.015Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/34/532efb87488d90807bae6a443d357ee5e2728a478c597619c8aaa17cc0bd/regex-2026.7.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4", size = 789557, upload-time = "2026-07-19T00:19:05.338Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/90/3a8d5ca977171ec3ae21a71207d2228b2663bde14d7f7ef0e6363ecf9290/regex-2026.7.19-cp314-cp314-win32.whl", hash = "sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa", size = 272531, upload-time = "2026-07-19T00:19:07.487Z" },
+ { url = "https://files.pythonhosted.org/packages/96/e1/8862885e70409de70e8c005f57fb2e7be8d9ef0317250d60f4c9660a300d/regex-2026.7.19-cp314-cp314-win_amd64.whl", hash = "sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac", size = 280831, upload-time = "2026-07-19T00:19:09.46Z" },
+ { url = "https://files.pythonhosted.org/packages/08/82/2693e53e29f9104d9de95d37ce4dd826bd32d5f9c0085d3aa6ac042675c4/regex-2026.7.19-cp314-cp314-win_arm64.whl", hash = "sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459", size = 281099, upload-time = "2026-07-19T00:19:11.398Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b7/9a01aa16461a18cde9d7b9c3ab21e501db2ce33725f53014342b91df2b0a/regex-2026.7.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3", size = 501121, upload-time = "2026-07-19T00:19:13.425Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/5e/bbaeca815dc9191c424c94a4fdc5c87c75748a64a6271821212ebdd4e1a3/regex-2026.7.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518", size = 299415, upload-time = "2026-07-19T00:19:15.43Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/d6/0dd1a321afaab95eb7ff44aa0f637301786f1dc71c6b797b9ed236ed8890/regex-2026.7.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9", size = 294483, upload-time = "2026-07-19T00:19:17.879Z" },
+ { url = "https://files.pythonhosted.org/packages/92/5f/40bacf91d0904f812e13bbbab3864604c463eced8afdc54aeaa50492ea95/regex-2026.7.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435", size = 811833, upload-time = "2026-07-19T00:19:20.102Z" },
+ { url = "https://files.pythonhosted.org/packages/94/7c/4902744261f775aeede8b5627314b38482da29cf49a57b66a6fb753246c5/regex-2026.7.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0", size = 871270, upload-time = "2026-07-19T00:19:22.365Z" },
+ { url = "https://files.pythonhosted.org/packages/16/70/6980c9be6bf21c0a60ed3e0aea39cf419ecf3b08d1d9947bc56e196ef186/regex-2026.7.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a", size = 917534, upload-time = "2026-07-19T00:19:24.529Z" },
+ { url = "https://files.pythonhosted.org/packages/52/92/8b2bd872782ce8c42691e39acb38eb8efe014e5ddb78ad7d943d6f197ce9/regex-2026.7.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276", size = 816135, upload-time = "2026-07-19T00:19:26.919Z" },
+ { url = "https://files.pythonhosted.org/packages/de/2d/33a602f657bdc4041f17d79f92ab18261d255d91a06117a6e29df023e5e2/regex-2026.7.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c", size = 785492, upload-time = "2026-07-19T00:19:29.192Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/36/0987cf4cb271680064a70d24a475873775a151d0b7058698a006cb0cae4a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a", size = 800658, upload-time = "2026-07-19T00:19:31.392Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/24/c14f31c135e1ba55fa4f9a58ca98d0842512bf6188230763c31c8f449e3b/regex-2026.7.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009", size = 865073, upload-time = "2026-07-19T00:19:33.485Z" },
+ { url = "https://files.pythonhosted.org/packages/14/85/181a12211f22469f24d2de1ebddfe397d2396e2c29013b9a58134a91069a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218", size = 773684, upload-time = "2026-07-19T00:19:35.599Z" },
+ { url = "https://files.pythonhosted.org/packages/23/58/bd1a0c1a62251366f8d21f41b1ea3c76994962071b8b6ea42f72d505c0f0/regex-2026.7.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966", size = 857769, upload-time = "2026-07-19T00:19:37.738Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/4f/f7e2dad6756b2fe1fe75dd90a628c3b45f249d39f948dd90cd2476325417/regex-2026.7.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44", size = 804546, upload-time = "2026-07-19T00:19:40.229Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/d7/01d31d5bdb09bc026fab77f59a371fdf8f9b292e4810546c56182ca70498/regex-2026.7.19-cp314-cp314t-win32.whl", hash = "sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78", size = 274526, upload-time = "2026-07-19T00:19:42.398Z" },
+ { url = "https://files.pythonhosted.org/packages/52/0e/cea4ce73bc0a8247a0748228ae6669984c7e1f8134b6fa66e59c0572e0ea/regex-2026.7.19-cp314-cp314t-win_amd64.whl", hash = "sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2", size = 283763, upload-time = "2026-07-19T00:19:44.644Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/b6/26e41975febae63b7a6e3e02f32cff6cff2e4f10d19c929082f56aebf7c6/regex-2026.7.19-cp314-cp314t-win_arm64.whl", hash = "sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547", size = 283451, upload-time = "2026-07-19T00:19:46.639Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.34.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
+]
+
+[[package]]
+name = "rich"
+version = "15.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "2026.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" },
+ { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" },
+ { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" },
+ { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" },
+ { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" },
+ { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" },
+ { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" },
+ { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" },
+ { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" },
+ { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" },
+ { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" },
+ { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" },
+ { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" },
+ { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" },
+ { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" },
+ { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" },
+ { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" },
+ { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" },
+ { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" },
+ { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" },
+ { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" },
+ { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" },
+ { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" },
+ { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" },
+ { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" },
+ { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" },
+ { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" },
+ { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" },
+ { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" },
+]
+
+[[package]]
+name = "rtree"
+version = "1.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/09/7302695875a019514de9a5dd17b8320e7a19d6e7bc8f85dcfb79a4ce2da3/rtree-1.4.1.tar.gz", hash = "sha256:c6b1b3550881e57ebe530cc6cffefc87cd9bf49c30b37b894065a9f810875e46", size = 52425, upload-time = "2025-08-13T19:32:01.413Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/d9/108cd989a4c0954e60b3cdc86fd2826407702b5375f6dfdab2802e5fed98/rtree-1.4.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d672184298527522d4914d8ae53bf76982b86ca420b0acde9298a7a87d81d4a4", size = 468484, upload-time = "2025-08-13T19:31:50.593Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/cf/2710b6fd6b07ea0aef317b29f335790ba6adf06a28ac236078ed9bd8a91d/rtree-1.4.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a7e48d805e12011c2cf739a29d6a60ae852fb1de9fc84220bbcef67e6e595d7d", size = 436325, upload-time = "2025-08-13T19:31:52.367Z" },
+ { url = "https://files.pythonhosted.org/packages/55/e1/4d075268a46e68db3cac51846eb6a3ab96ed481c585c5a1ad411b3c23aad/rtree-1.4.1-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa8c4496e31e9ad58ff6c7df89abceac7022d906cb64a3e18e4fceae6b77f65", size = 459789, upload-time = "2025-08-13T19:31:53.926Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/75/e5d44be90525cd28503e7f836d077ae6663ec0687a13ba7810b4114b3668/rtree-1.4.1-py3-none-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12de4578f1b3381a93a655846900be4e3d5f4cd5e306b8b00aa77c1121dc7e8c", size = 507644, upload-time = "2025-08-13T19:31:55.164Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/85/b8684f769a142163b52859a38a486493b05bafb4f2fb71d4f945de28ebf9/rtree-1.4.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b558edda52eca3e6d1ee629042192c65e6b7f2c150d6d6cd207ce82f85be3967", size = 1454478, upload-time = "2025-08-13T19:31:56.808Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/a4/c2292b95246b9165cc43a0c3757e80995d58bc9b43da5cb47ad6e3535213/rtree-1.4.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f155bc8d6bac9dcd383481dee8c130947a4866db1d16cb6dff442329a038a0dc", size = 1555140, upload-time = "2025-08-13T19:31:58.031Z" },
+ { url = "https://files.pythonhosted.org/packages/74/25/5282c8270bfcd620d3e73beb35b40ac4ab00f0a898d98ebeb41ef0989ec8/rtree-1.4.1-py3-none-win_amd64.whl", hash = "sha256:efe125f416fd27150197ab8521158662943a40f87acab8028a1aac4ad667a489", size = 389358, upload-time = "2025-08-13T19:31:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/50/0a9e7e7afe7339bd5e36911f0ceb15fed51945836ed803ae5afd661057fd/rtree-1.4.1-py3-none-win_arm64.whl", hash = "sha256:3d46f55729b28138e897ffef32f7ce93ac335cb67f9120125ad3742a220800f0", size = 355253, upload-time = "2025-08-13T19:32:00.296Z" },
+]
+
+[[package]]
+name = "safetensors"
+version = "0.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" },
+ { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" },
+ { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" },
+ { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" },
+ { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" },
+ { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" },
+ { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" },
+ { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" },
+ { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" },
+ { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" },
+]
+
+[package.optional-dependencies]
+torch = [
+ { name = "numpy" },
+ { name = "torch" },
+]
+
+[[package]]
+name = "scipy"
+version = "1.18.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" },
+ { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" },
+ { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" },
+ { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" },
+ { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" },
+ { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" },
+ { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" },
+ { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" },
+ { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" },
+ { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" },
+ { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" },
+ { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" },
+ { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" },
+]
+
+[[package]]
+name = "semchunk"
+version = "3.2.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mpire", extra = ["dill"] },
+ { name = "tqdm" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a9/a0/ce7e3d6cc76498fd594e667d10a03f17d7cced129e46869daec23523bf5a/semchunk-3.2.5.tar.gz", hash = "sha256:ee15e9a06a69a411937dd8fcf0a25d7ef389c5195863140436872a02c95b0218", size = 17667, upload-time = "2025-10-28T02:12:38.025Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/95/12d226ee4d207cb1f77a216baa7e1a8bae2639733c140abe8d0316d23a18/semchunk-3.2.5-py3-none-any.whl", hash = "sha256:fd09cc5f380bd010b8ca773bd81893f7eaf11d37dd8362a83d46cedaf5dae076", size = 13048, upload-time = "2025-10-28T02:12:36.724Z" },
+]
+
+[[package]]
+name = "setuptools"
+version = "84.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" },
+]
+
+[[package]]
+name = "shapely"
+version = "2.1.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" },
+ { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" },
+ { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" },
+ { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" },
+ { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" },
+ { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" },
+ { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" },
+ { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" },
+ { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" },
+ { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" },
+ { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" },
+ { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" },
+ { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" },
+ { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" },
+ { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" },
+]
+
+[[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 = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "soupsieve"
+version = "2.9.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/69/99/a6ca3beb3ccacb41fb3321d8a60e5566f9e6467601ef8eba6a17e1b89778/soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74", size = 122445, upload-time = "2026-08-07T00:57:24.801Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" },
+]
+
+[[package]]
+name = "sympy"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mpmath" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
+]
+
+[[package]]
+name = "tabulate"
+version = "0.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" },
+]
+
+[[package]]
+name = "tokenizers"
+version = "0.22.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huggingface-hub" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" },
+ { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" },
+ { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" },
+ { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" },
+ { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" },
+ { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" },
+ { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" },
+ { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" },
+]
+
+[[package]]
+name = "torch"
+version = "2.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cuda-bindings", marker = "python_full_version < '3.15' and sys_platform == 'linux'" },
+ { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" },
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "jinja2" },
+ { name = "networkx" },
+ { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" },
+ { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" },
+ { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" },
+ { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" },
+ { name = "setuptools" },
+ { name = "sympy" },
+ { name = "triton", marker = "python_full_version < '3.15' and sys_platform == 'linux'" },
+ { name = "typing-extensions" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c4/3a/ed0f4d4d1dcde03bced7aac9a28e800abcdc0cbd06b6775044c9fbd877b7/torch-2.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027", size = 111213045, upload-time = "2026-07-08T16:05:22.997Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a9/f6a2a4d763ff1df02e9a64c477029db614295bc9367f4131223791ccc243/torch-2.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4", size = 427210998, upload-time = "2026-07-08T16:04:37.708Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/82/fea946351658e6534db52d2cc12bc53087cbf87f9440c5f180f367c1950b/torch-2.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b", size = 526605292, upload-time = "2026-07-08T16:04:22.81Z" },
+ { url = "https://files.pythonhosted.org/packages/21/d6/e8f3c6f7e01f626f77259de9860d2a78bc84c40539e28e79b7e98b0bb659/torch-2.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d", size = 122057313, upload-time = "2026-07-08T16:03:53.43Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/fa/c1c10b7aff4a9a3e8956d4f0a5f468fa6db7abc3208805719076772b4833/torch-2.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09", size = 111213743, upload-time = "2026-07-08T16:03:28.579Z" },
+ { url = "https://files.pythonhosted.org/packages/11/18/9ecb37b56293a0be8d80f810bf672a72fe7e02f8b475d5ef1b9bf8a0d748/torch-2.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005", size = 427213008, upload-time = "2026-07-08T16:03:44.106Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/5a/7c50ba1b7b713d71d34669c6d13dab0a11531a3eceb0307a5162dbfec0f7/torch-2.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a3a9a21312872af8a26950b2c15680335a386a1f56ed03e780653d78b9607e9e", size = 526602329, upload-time = "2026-07-08T16:03:12.649Z" },
+ { url = "https://files.pythonhosted.org/packages/91/3d/e7adcc6aaf36961cd18f56cf8ad0f3058c3a5c84ccf391762176c94581b8/torch-2.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6", size = 122057920, upload-time = "2026-07-08T16:03:01.808Z" },
+ { url = "https://files.pythonhosted.org/packages/36/76/6dcc7f0c07052102dd36f83cbc5800842a909c8c3fbf1a7f8a5844954de9/torch-2.13.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c", size = 111227066, upload-time = "2026-07-08T16:03:33.6Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/09/2c10e8cd0e00fa5d23c052df6ce467eaa7182399f5e0f824f1e4ff42ccae/torch-2.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a3893dc2da0a972a8ca5d698c85a9f967559ac5f8ee1797b77408aa8734d073c", size = 427226309, upload-time = "2026-07-08T16:02:53.127Z" },
+ { url = "https://files.pythonhosted.org/packages/76/c6/22c2102bbef14ca6a6cb4c20e42f088e49c5f812be4e160ae57502e325f9/torch-2.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:49f1ea385c754e54919408a9bb3b5a72b0b755bbe2c916c1d6f70afbec4908a2", size = 526614507, upload-time = "2026-07-08T16:02:16.441Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0c/7d1deb6bce5bc3e6042caf39100ac768eba3b9a098e1dddd16f75bd6489b/torch-2.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f8573e3ce9ebcd53fe922f01077a6085ccdfbe5f12fd215883a9d87d7a744fd", size = 122051871, upload-time = "2026-07-08T16:03:23.521Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/ce/aa8b7f9949d32e0f2f624f342bc3b48112c1b8a130288465938bc83bcbf9/torch-2.13.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1", size = 111537025, upload-time = "2026-07-08T16:02:44.28Z" },
+ { url = "https://files.pythonhosted.org/packages/69/d1/491e3a0389430946145888b0203f2b6a759ce2a61481b96a85c2da4f2ced/torch-2.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:31061ff56ed8fbf26c749806905aeb749ebeb819810fd5d52508aa5afd90dddc", size = 427219769, upload-time = "2026-07-08T16:02:31.18Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/1d/38006e045bf0a1fc28ef01e757c554e59e59a8770c284bc4f47b14e60441/torch-2.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:cc26eead4cf51d0b544e31e364dcf000846549c273bd148936fe9d24d29acb92", size = 526571320, upload-time = "2026-07-08T16:01:59.348Z" },
+ { url = "https://files.pythonhosted.org/packages/56/94/655c91992a882bd5071aa0b5d22a07dbb130d801e872be97c0b627a7c693/torch-2.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a7de8a313090dc5c7d7ba4bfe5c3be222528f9a4dba1acc83bddb1157360c4b8", size = 122306773, upload-time = "2026-07-08T16:02:39.832Z" },
+]
+
+[[package]]
+name = "torchvision"
+version = "0.28.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "pillow" },
+ { name = "torch" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/15/49/c1cab1ecbb3ff1a380a3f99283db1dee61b8afe354f6352c643b65937130/torchvision-0.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:e9f54c30cd52e3ef7fd034cc69b7bb7e0964e1c8f8743e018ab92e95b40f9eee", size = 1856020, upload-time = "2026-07-08T16:07:52.182Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/4c/95233776e2def960e5abb7a07931230a545f43717a56a1e1140162033598/torchvision-0.28.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5cf78ebc401ce64ae19b8c55de866bb836797d559a4de9c25ccbe74cfa642d3a", size = 7842127, upload-time = "2026-07-08T16:07:53.446Z" },
+ { url = "https://files.pythonhosted.org/packages/93/e4/e9b2495d0d57b9f60d63c57d0a910410a81b4b073bf70917bef815291119/torchvision-0.28.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:028a3d481b37d785605620d7cdad897064c5a55bae2aa1f2658766333e291940", size = 7675040, upload-time = "2026-07-08T16:07:58.017Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/9c/55ed9cb6dfe3ee9c837df5cd0e758372e5829aa38b8dd71343aa632cc4e2/torchvision-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:87dc16b2df427c1318ad335f1e2be2b3b15b2cf20f7934c83b0505a48425ee5d", size = 4085785, upload-time = "2026-07-08T16:07:50.928Z" },
+ { url = "https://files.pythonhosted.org/packages/20/55/08a726c14c67b37c8aca04b077766909f1c7ed23f76116884fe63b9bd033/torchvision-0.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:d483b4aa3f5237569053f749cd1a2b5bb548ca456e40461a5dd087f21149d123", size = 1856021, upload-time = "2026-07-08T16:07:45.386Z" },
+ { url = "https://files.pythonhosted.org/packages/db/8f/40beacd53809194f5259e590d1afaeaa8ad57da15f77c646e6560bcc4616/torchvision-0.28.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bb6dd6918460ed89cc7644adcc2402991474d6933cf1ce92b390641cb233fddf", size = 7797014, upload-time = "2026-07-08T16:07:43.04Z" },
+ { url = "https://files.pythonhosted.org/packages/32/db/062cdb5a84380a60439775311fff34d89229760d2a50680393dc18699956/torchvision-0.28.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad7b3a439265cc3739a4ab5b4c998c0e38ea99c0ee7ca4dea35c5d0b099ec237", size = 7674669, upload-time = "2026-07-08T16:07:38.91Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/a6/b4081e2d04e1541abf82785ac9e5178a494c19330391f551356c8c18b7b3/torchvision-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:7e9dd6f60d6e15f8dc27d4f877fdb6002fc70d70272412135f1c2ff9cfa08d3b", size = 4157380, upload-time = "2026-07-08T16:07:40.22Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/b9/da40eca5bbe9596c12ae9899ab7abaf887f5e20f29d08b924b4633714821/torchvision-0.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3bd9dba55224a9db4a2d77f6feaa5651770d8c8e86d3d0ddb0fa6bec54c8712b", size = 1856014, upload-time = "2026-07-08T16:07:44.282Z" },
+ { url = "https://files.pythonhosted.org/packages/06/d6/313aafd3df4eaf5f330211bd4e75b7598bddbfee4f55580d3b58536e1b20/torchvision-0.28.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:89f90e29b0966352811b12589f3a3c61943bf2bb9487b9d7bbec10efb1096bb5", size = 7796873, upload-time = "2026-07-08T16:07:30.907Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/41/31f8e959ab8f942600b6357f8999c21d779d5fd3304b0fd204ff4b518239/torchvision-0.28.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:36beb0782976906069ca03d4c9aacaf4b6b838b06ed6c20960ea9c51cce7acdd", size = 7674634, upload-time = "2026-07-08T16:07:29.657Z" },
+ { url = "https://files.pythonhosted.org/packages/15/15/4c5115253fd470672cdac0a1cf139e06b4f3e29d041238a2b255937f63be/torchvision-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:3557cc7b539f46dabcda2b6f2b14017ccbeef024de466d4fc5835fc3f287f769", size = 4184005, upload-time = "2026-07-08T16:07:35.805Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/80/822a6163da716f8a78141cf6678d74e26a572285d4ea866ef8aa657bb307/torchvision-0.28.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:09ce8f56e81f19b9c378ae7bb109f83f6659fd8bc3cd14241a48e4af46e9ed49", size = 1856011, upload-time = "2026-07-08T16:07:33.404Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/d1/cd3f9463b39a790ec8c0c2f6e6c8061edb1562114d04fcdfa786ed889345/torchvision-0.28.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:62c7d110f86a039245b587e4fae60278c649f3bd42ff79cfbc1178eca4e72542", size = 7796742, upload-time = "2026-07-08T16:07:28.339Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/82/3e0a7ad18e99831e2d7f4713d3be717b7159ff5a920862dd5c23c454aa71/torchvision-0.28.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:904cf89af220f8c6b2ed0296bb5065b474ce43b77558e48b2bf9de8b0ba17204", size = 7675526, upload-time = "2026-07-08T16:07:34.572Z" },
+ { url = "https://files.pythonhosted.org/packages/18/d4/23aea03b28297bc66a4461f55ae4296368a9d85fa9a454bafcb2a5348bd7/torchvision-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:46f581979c010ad6da6bd85ee602aa707e1ff44312670223b7a0ee517ad06d47", size = 4291452, upload-time = "2026-07-08T16:07:32.236Z" },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.70.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" },
+]
+
+[[package]]
+name = "transformers"
+version = "5.8.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.14' and sys_platform == 'darwin'",
+ "python_full_version == '3.13.*' and sys_platform == 'darwin'",
+ "python_full_version < '3.13' and sys_platform == 'darwin'",
+]
+dependencies = [
+ { name = "huggingface-hub" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "regex" },
+ { name = "safetensors" },
+ { name = "tokenizers" },
+ { name = "tqdm" },
+ { name = "typer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e7/e6/4134ea2fbea322cddc7ffc94a0d8ee47fe32ce8e876b320cd37d88edfc4d/transformers-5.8.1.tar.gz", hash = "sha256:4dd5b6de4105725104d84fd6abd74b305f4debfc251b38c648ee5dd087cf543b", size = 8532019, upload-time = "2026-05-13T03:21:57.234Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/b1/8be7e7ef0b5200491312201918b6125ef9c9df9dd0f0240ccef9ac824e6b/transformers-5.8.1-py3-none-any.whl", hash = "sha256:5340fb95962162cdfdae5cc91d7f8fedd92ed75216c1154c5e1f590fcf56dd0e", size = 10632882, upload-time = "2026-05-13T03:21:52.876Z" },
+]
+
+[[package]]
+name = "transformers"
+version = "5.15.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'win32'",
+ "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'win32'",
+ "python_full_version < '3.13' and sys_platform == 'win32'",
+ "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "huggingface-hub" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "regex" },
+ { name = "safetensors" },
+ { name = "tokenizers" },
+ { name = "tqdm" },
+ { name = "typer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a6/3f/d89353267d511e18f137dfd7769d07837350c11b88408ce1dfe2e93e56c7/transformers-5.15.0.tar.gz", hash = "sha256:bbf98f57b2ddd7c4ecbccfa2c0069017aa6fd01cc204bd50cbc0eeadcf2a13b8", size = 9377983, upload-time = "2026-08-10T10:27:23.261Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d8/43/81355710a4c84e9420e11a86d41a5364deb561f2ef36dfdf254a07371bbb/transformers-5.15.0-py3-none-any.whl", hash = "sha256:d7f007736f67749ae9490c4f8cb5d30b452ae2d68c8675e50ba8d63ea7feb107", size = 11749280, upload-time = "2026-08-10T10:27:20.416Z" },
+]
+
+[[package]]
+name = "tree-sitter"
+version = "0.26.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f7/03/5600b84aff2e6c4fe80cfebb4063fe2f50299521befe5f6092ab8c082f4a/tree_sitter-0.26.0.tar.gz", hash = "sha256:b40c219edccc4564530c96f8f1556f6202b37cda964d1cbd7bd2b7e68b40a245", size = 191423, upload-time = "2026-06-30T12:14:27.933Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/ca/565702c44815393e3a973552ad546db4e5ca081ca8698640b4e93d809f51/tree_sitter-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6cb2bd20efb2544c19ac54486ab7cb8ec7b36f913bbe1ce95df84acb96743d9c", size = 148934, upload-time = "2026-06-30T12:14:01.188Z" },
+ { url = "https://files.pythonhosted.org/packages/54/6f/8bb61957f16ec1b1d92410a006cdc84a952b6352a7313b2ad299f2d21484/tree_sitter-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:918d89529786873f0982a0f59c2a303cd065fbfd1b903d71a8e4e1584f67b42e", size = 140820, upload-time = "2026-06-30T12:14:02.087Z" },
+ { url = "https://files.pythonhosted.org/packages/78/0a/8a6f08559182643a814a4ab559948ae817b2851890fd9b995a4fff6541ce/tree_sitter-0.26.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30a88be89ff1f2755297f81e8080d88b795dd98720c3f9fa2acf93873182cc95", size = 638844, upload-time = "2026-06-30T12:14:03.428Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/2f/6e6781b31677231366cb3cf27bc8269157f6d4b03c9032865a4f5f2bbe7e/tree_sitter-0.26.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a6b333b0282d8bb0af741f9b018bd2523d4eecb2686bf6717066a625fecfaa4", size = 667487, upload-time = "2026-06-30T12:14:04.669Z" },
+ { url = "https://files.pythonhosted.org/packages/02/0b/0483078c8567445557a7015b0e5b187f6d7d4fda73464df9c4bdea7f7f3c/tree_sitter-0.26.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f3c44339dd34fe8eb2b8d5aa7610660499a795f70376b130bbee7a437337280", size = 647975, upload-time = "2026-06-30T12:14:05.797Z" },
+ { url = "https://files.pythonhosted.org/packages/27/68/da83ca72c984e96ab4eb3bee0db1a6ffb5de1c8c455f92bd9f420cde7f0e/tree_sitter-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94550e13b6ae576969da40246f4c4abb206380b5375ad43f26dd9151d55438e3", size = 665018, upload-time = "2026-06-30T12:14:07.278Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/36/4d67927fd47b89af4a00f65f55a7370e28778cd50e972c2430487e3ecc27/tree_sitter-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca89e361a276dbc934b28a43dd881199e25d34ff5493ee0ce45f3c52a6124a37", size = 129619, upload-time = "2026-06-30T12:14:08.373Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/72/cdefad523eb78710679c6da6a79e3d90f5afd32b1c6aa5a17bac7eef99f6/tree_sitter-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:bc6cb01d5ee75c85424aa1f1c72a82d8f07fd52539a0f3c4a6ed3e8721079b84", size = 116545, upload-time = "2026-06-30T12:14:09.273Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/b0/465257cf8f972ad9f9812ec1cbaa8ec210ebebb601ade9a15881aa2436b4/tree_sitter-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed0889dbed843ce45ede9f5169c0b2dea2222f12685844a03fadb81f12705867", size = 148893, upload-time = "2026-06-30T12:14:10.541Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/ec/19d093e854b45e807fecfdd26105c266f43aeecc39c4dc97992a7074ad5a/tree_sitter-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6189c6c340c7384357711e3d92645e96bfb79f7a502f86de1ebdb23eb43f7dab", size = 140829, upload-time = "2026-06-30T12:14:11.626Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/ee/87e74671ed63a837e7a1f17ab94aa3913871e033b27523d8e7b83d6f7ad0/tree_sitter-0.26.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ff2e0750b7daa722302838356d7b65e303829b7eb73c915df127ddba115e1d1", size = 639334, upload-time = "2026-06-30T12:14:12.836Z" },
+ { url = "https://files.pythonhosted.org/packages/66/e7/f7e04cd9dff6b6ac0adf23922796fbc76accd4cf4bcda50542748d485679/tree_sitter-0.26.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7075ef857ef86f327dbb72d1e2574dda78db5754b3a1fca6506acd7fe5d561a7", size = 668102, upload-time = "2026-06-30T12:14:14.035Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/90/0bfb16b7894fea728c774a89d5af421a9368a2f913bbd4e8dcab7caaecfb/tree_sitter-0.26.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:26c996c1edfee86e977bb3f5462e74fcec0d0b0db1e85a3c475875763caa03be", size = 648560, upload-time = "2026-06-30T12:14:15.302Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/e6/0fe05ba396e9623b0ae40ccf34171336b8701ec8d7bd0ee9f5224d638665/tree_sitter-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00289bfe7978f3e0dc0ce69813a20fa9f44ea4c100b3ec62043e5eb74ccfc3a2", size = 665121, upload-time = "2026-06-30T12:14:16.403Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/d2/a944b1ca35bed6068dc84a9967aaf3049d8cc0b7a36179eea8787270a6ab/tree_sitter-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:93e220cab7e6a823efeb2046c49171427de92ef71c7c681c01820d14d8d3721f", size = 129615, upload-time = "2026-06-30T12:14:17.463Z" },
+ { url = "https://files.pythonhosted.org/packages/09/ef/c7ca48293580d2249f36940c4eed5b4ddeb9ce75baf9a4ef30621987e0c7/tree_sitter-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:b31a8195d2f224224c530ac814632d98c1dcc123d227442c07c736e86b70d564", size = 116525, upload-time = "2026-06-30T12:14:18.53Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/7a/4d84e6f6ae2c3e757490dd84de251712c31e293dfe31f28da1ec019cefa2/tree_sitter-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5a3c93a352b7e6f70f73e121bbfa2d0117ba7478bd51114ed35c91b0b78814fa", size = 148901, upload-time = "2026-06-30T12:14:19.452Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/d9/efe62ec65dc9d096e834d27b8c058127e2146e42ff3380b822a233f016a6/tree_sitter-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5fc2f41bf246ff2f70a9cc3690be35ec7580a4923151873d898c8bcb1a4503d3", size = 140805, upload-time = "2026-06-30T12:14:20.478Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/2c/c82326b7b97e3c485c18679883b16f89e5e913c639d3b219d3da70c9e67e/tree_sitter-0.26.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8ea92a255c91671a7ec4625aba3ab7bb5220c423630ffbf83c45d7312abe084", size = 640586, upload-time = "2026-06-30T12:14:21.527Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/7a/f56e7d8282859452611024c7cbc623bfba5b24b8cb9b8f8bc88c5219fe9a/tree_sitter-0.26.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f665510f0fcf4636fb9696f1f7853bed7a3bd764b7bb0cb8494e619c14ed5a0c", size = 668300, upload-time = "2026-06-30T12:14:22.728Z" },
+ { url = "https://files.pythonhosted.org/packages/91/51/240ee81b9d5e9ca0a6cb1528e8605ffa70ab58c89ce126631be96d3e4bae/tree_sitter-0.26.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:253df7ab82cc0a9d311cd65f06e9f99fb3eac55996ae9fc94da22f123a861b90", size = 649627, upload-time = "2026-06-30T12:14:23.819Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/54/760035cefedf9eb44f0f84c4ac22f1322e73155853e272576ee876336312/tree_sitter-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ff80d4833d330a73184a3ac5132abe93c575d2dea31975c6f15c0d21fef238aa", size = 664885, upload-time = "2026-06-30T12:14:25.064Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/1b/0b36fe2a984ecedc4ce6aefd5d56447a6626a8e9b595c4e48658510ce8f8/tree_sitter-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:a4033fecc8f606c7f2e8b8014d0057b74668a7f0152763606f7bc25c5f9ec64c", size = 132688, upload-time = "2026-06-30T12:14:26.106Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/74/ebc041a13fbf40144afdb0d4b447e48e0b4012ca866c63de8b48f801f0c1/tree_sitter-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:823251c4b6725a7c03ed497a339135ede7ae4bdde75bb8be7ef5e305aeb4ff52", size = 120287, upload-time = "2026-06-30T12:14:26.991Z" },
+]
+
+[[package]]
+name = "tree-sitter-c"
+version = "0.24.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a6/c9/3834f3d9278251aea7312274971bc4c45b17aec2490fd4b884d93bd7019a/tree_sitter_c-0.24.2.tar.gz", hash = "sha256:1628584df0299b5a340aa63f8e67b6c97c91517f52fa7e7a4c557e40adb330a9", size = 228397, upload-time = "2026-04-22T08:06:14.491Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/28/c1/26ed17730ec2c17bedc1b673349e5e0a466c578e3eb0327c3b73cf52bf97/tree_sitter_c-0.24.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:4d4579a8b54f0a442f903d88d3304cab77cd5c2031d4015baa4f2f8e15d6dcb7", size = 81016, upload-time = "2026-04-22T08:06:07.208Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/1c/1140db75e7e375cda3c68792a33826c4fd40b5b98c3259d93c75f6c8368f/tree_sitter_c-0.24.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:97bc80a224d48215d4e6e6376bf30d114f4c317b8145ff1b02afe785d4ba7bdd", size = 86213, upload-time = "2026-04-22T08:06:08.136Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/8c/0dfb88d726f8821d1c4c36042f092be974a800afd734307a595b8604190c/tree_sitter_c-0.24.2-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5041ef67eb68ce6bc8bb0b1f8ef3a5585ce523dae0c7eec109ab0627dd75aede", size = 94264, upload-time = "2026-04-22T08:06:08.918Z" },
+ { url = "https://files.pythonhosted.org/packages/87/78/47dc570e7aee6b0a1ecc2520b30639cc2b06003154c9ab0672d86bf720d5/tree_sitter_c-0.24.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c098bedcd5ac86ff93fa734d51d1dd86aed40fd5ed7d634c7af11380a0469969", size = 94560, upload-time = "2026-04-22T08:06:09.852Z" },
+ { url = "https://files.pythonhosted.org/packages/29/37/75d59d3f74f4cfc00f04472917e933d8a9c9fdc6eff980ef9552e010e6aa/tree_sitter_c-0.24.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:82842c5a5f2acd93f4de10038c33ac179c8979defc39376f990348d6289e933b", size = 94023, upload-time = "2026-04-22T08:06:10.682Z" },
+ { url = "https://files.pythonhosted.org/packages/64/57/8fc655d5a446a70a637e92b98bd2fdaab88bf5bb5b36076ac4add544808d/tree_sitter_c-0.24.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2b42e8e22202c251f8629306f9321233542e07a6e01611b5fe83489272143eb", size = 94160, upload-time = "2026-04-22T08:06:11.497Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/f7/72a1d6b42dd31fd37e03ff67e7dc5ee572301499e6b216002b8dd42a1714/tree_sitter_c-0.24.2-cp310-abi3-win_amd64.whl", hash = "sha256:abb549225091f7b25df2dd3a0143ece6e208f7055d8bcb4700b41ee79b9ef1e1", size = 84669, upload-time = "2026-04-22T08:06:12.347Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/9d/7475d9ae8ef679aa36c7dfe6c903ab78e573651c68b6ef9862d6a3f994db/tree_sitter_c-0.24.2-cp310-abi3-win_arm64.whl", hash = "sha256:4a2f4371cd816cc3153458f69062135ebb2ea5f275ddd90494e5c823d778204a", size = 82956, upload-time = "2026-04-22T08:06:13.364Z" },
+]
+
+[[package]]
+name = "tree-sitter-javascript"
+version = "0.25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/59/e0/e63103c72a9d3dfd89a31e02e660263ad84b7438e5f44ee82e443e65bbde/tree_sitter_javascript-0.25.0.tar.gz", hash = "sha256:329b5414874f0588a98f1c291f1b28138286617aa907746ffe55adfdcf963f38", size = 132338, upload-time = "2025-09-01T07:13:44.792Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/df/5106ac250cd03661ebc3cc75da6b3d9f6800a3606393a0122eca58038104/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b70f887fb269d6e58c349d683f59fa647140c410cfe2bee44a883b20ec92e3dc", size = 64052, upload-time = "2025-09-01T07:13:36.865Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/8f/6b4b2bc90d8ab3955856ce852cc9d1e82c81d7ab9646385f0e75ffd5b5d3/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:8264a996b8845cfce06965152a013b5d9cbb7d199bc3503e12b5682e62bb1de1", size = 66440, upload-time = "2025-09-01T07:13:37.962Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/c4/7da74ecdcd8a398f88bd003a87c65403b5fe0e958cdd43fbd5fd4a398fcf/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dc04ba91fc8583344e57c1f1ed5b2c97ecaaf47480011b92fbeab8dda96db75", size = 99728, upload-time = "2025-09-01T07:13:38.755Z" },
+ { url = "https://files.pythonhosted.org/packages/96/c8/97da3af4796495e46421e9344738addb3602fa6426ea695be3fcbadbee37/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:199d09985190852e0912da2b8d26c932159be314bc04952cf917ed0e4c633e6b", size = 106072, upload-time = "2025-09-01T07:13:39.798Z" },
+ { url = "https://files.pythonhosted.org/packages/13/be/c964e8130be08cc9bd6627d845f0e4460945b158429d39510953bbcb8fcc/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dfcf789064c58dc13c0a4edb550acacfc6f0f280577f1e7a00de3e89fc7f8ddc", size = 104388, upload-time = "2025-09-01T07:13:40.866Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/89/9b773dee0f8961d1bb8d7baf0a204ab587618df19897c1ef260916f318ec/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b852d3aee8a36186dbcc32c798b11b4869f9b5041743b63b65c2ef793db7a54", size = 98377, upload-time = "2025-09-01T07:13:41.838Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/dc/d90cb1790f8cec9b4878d278ad9faf7c8f893189ce0f855304fd704fc274/tree_sitter_javascript-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:e5ed840f5bd4a3f0272e441d19429b26eedc257abe5574c8546da6b556865e3c", size = 62975, upload-time = "2025-09-01T07:13:42.828Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1f/f9eba1038b7d4394410f3c0a6ec2122b590cd7acb03f196e52fa57ebbe72/tree_sitter_javascript-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:622a69d677aa7f6ee2931d8c77c981a33f0ebb6d275aa9d43d3397c879a9bb0b", size = 61668, upload-time = "2025-09-01T07:13:43.803Z" },
+]
+
+[[package]]
+name = "tree-sitter-python"
+version = "0.25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b8/8b/c992ff0e768cb6768d5c96234579bf8842b3a633db641455d86dd30d5dac/tree_sitter_python-0.25.0.tar.gz", hash = "sha256:b13e090f725f5b9c86aa455a268553c65cadf325471ad5b65cd29cac8a1a68ac", size = 159845, upload-time = "2025-09-11T06:47:58.159Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cf/64/a4e503c78a4eb3ac46d8e72a29c1b1237fa85238d8e972b063e0751f5a94/tree_sitter_python-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:14a79a47ddef72f987d5a2c122d148a812169d7484ff5c75a3db9609d419f361", size = 73790, upload-time = "2025-09-11T06:47:47.652Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/1d/60d8c2a0cc63d6ec4ba4e99ce61b802d2e39ef9db799bdf2a8f932a6cd4b/tree_sitter_python-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:480c21dbd995b7fe44813e741d71fed10ba695e7caab627fb034e3828469d762", size = 76691, upload-time = "2025-09-11T06:47:49.038Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86f118e5eecad616ecdb81d171a36dde9bef5a0b21ed71ea9c3e390813c3baf5", size = 108133, upload-time = "2025-09-11T06:47:50.499Z" },
+ { url = "https://files.pythonhosted.org/packages/40/bd/bf4787f57e6b2860f3f1c8c62f045b39fb32d6bac4b53d7a9e66de968440/tree_sitter_python-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be71650ca2b93b6e9649e5d65c6811aad87a7614c8c1003246b303f6b150f61b", size = 110603, upload-time = "2025-09-11T06:47:51.985Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/25/feff09f5c2f32484fbce15db8b49455c7572346ce61a699a41972dea7318/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6d5b5799628cc0f24691ab2a172a8e676f668fe90dc60468bee14084a35c16d", size = 108998, upload-time = "2025-09-11T06:47:53.046Z" },
+ { url = "https://files.pythonhosted.org/packages/75/69/4946da3d6c0df316ccb938316ce007fb565d08f89d02d854f2d308f0309f/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:71959832fc5d9642e52c11f2f7d79ae520b461e63334927e93ca46cd61cd9683", size = 107268, upload-time = "2025-09-11T06:47:54.388Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/a2/996fc2dfa1076dc460d3e2f3c75974ea4b8f02f6bc925383aaae519920e8/tree_sitter_python-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:9bcde33f18792de54ee579b00e1b4fe186b7926825444766f849bf7181793a76", size = 76073, upload-time = "2025-09-11T06:47:55.773Z" },
+ { url = "https://files.pythonhosted.org/packages/07/19/4b5569d9b1ebebb5907d11554a96ef3fa09364a30fcfabeff587495b512f/tree_sitter_python-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb", size = 74169, upload-time = "2025-09-11T06:47:56.747Z" },
+]
+
+[[package]]
+name = "tree-sitter-typescript"
+version = "0.23.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1e/fc/bb52958f7e399250aee093751e9373a6311cadbe76b6e0d109b853757f35/tree_sitter_typescript-0.23.2.tar.gz", hash = "sha256:7b167b5827c882261cb7a50dfa0fb567975f9b315e87ed87ad0a0a3aedb3834d", size = 773053, upload-time = "2024-11-11T02:36:11.396Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/28/95/4c00680866280e008e81dd621fd4d3f54aa3dad1b76b857a19da1b2cc426/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3cd752d70d8e5371fdac6a9a4df9d8924b63b6998d268586f7d374c9fba2a478", size = 286677, upload-time = "2024-11-11T02:35:58.839Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/2f/1f36fda564518d84593f2740d5905ac127d590baf5c5753cef2a88a89c15/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c7cc1b0ff5d91bac863b0e38b1578d5505e718156c9db577c8baea2557f66de8", size = 302008, upload-time = "2024-11-11T02:36:00.733Z" },
+ { url = "https://files.pythonhosted.org/packages/96/2d/975c2dad292aa9994f982eb0b69cc6fda0223e4b6c4ea714550477d8ec3a/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b1eed5b0b3a8134e86126b00b743d667ec27c63fc9de1b7bb23168803879e31", size = 351987, upload-time = "2024-11-11T02:36:02.669Z" },
+ { url = "https://files.pythonhosted.org/packages/49/d1/a71c36da6e2b8a4ed5e2970819b86ef13ba77ac40d9e333cb17df6a2c5db/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e96d36b85bcacdeb8ff5c2618d75593ef12ebaf1b4eace3477e2bdb2abb1752c", size = 344960, upload-time = "2024-11-11T02:36:04.443Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/cb/f57b149d7beed1a85b8266d0c60ebe4c46e79c9ba56bc17b898e17daf88e/tree_sitter_typescript-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8d4f0f9bcb61ad7b7509d49a1565ff2cc363863644a234e1e0fe10960e55aea0", size = 340245, upload-time = "2024-11-11T02:36:06.473Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ab/dd84f0e2337296a5f09749f7b5483215d75c8fa9e33738522e5ed81f7254/tree_sitter_typescript-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:3f730b66396bc3e11811e4465c41ee45d9e9edd6de355a58bbbc49fa770da8f9", size = 278015, upload-time = "2024-11-11T02:36:07.631Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/e4/81f9a935789233cf412a0ed5fe04c883841d2c8fb0b7e075958a35c65032/tree_sitter_typescript-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7", size = 274052, upload-time = "2024-11-11T02:36:09.514Z" },
+]
+
+[[package]]
+name = "triton"
+version = "3.7.1"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/fa/f856e24deb462d5f18bd4b5a746957862ab9b6ee5834bda60605ec348366/triton-3.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9497f2e696ee368862a181a90b2dcc03ca978cc4f602abd67c7d81022a6988e1", size = 184692359, upload-time = "2026-06-17T20:03:48.288Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/6f/fb96d15db6f36d6eae4cafb998c2e0353bf59d7c4ea1662d7497f269134a/triton-3.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e40869937a68206ec70d7f25bb7ec6433cb083f9135e1f36dbd318dc449a728", size = 197719725, upload-time = "2026-06-17T19:53:20.419Z" },
+ { url = "https://files.pythonhosted.org/packages/00/42/c5089d4d9327fcd1e862c599cc2927f39418f84dd11a84cb2ccff9d4787a/triton-3.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdbfc09d9ec58bc5e68321525653220de7515c199e7a8097a97c85e62b52cd0a", size = 184694629, upload-time = "2026-06-17T20:03:53.444Z" },
+ { url = "https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58c0e131da05134a2a4788ccbcc0c1105cf0f54c8e98f19e34cd465396dc15eb", size = 197729241, upload-time = "2026-06-17T19:53:27.801Z" },
+ { url = "https://files.pythonhosted.org/packages/40/71/e01aa7ad573883ed9456f130226babdec70b005e098c4d6226a6238e761b/triton-3.7.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe4ea396a06171f1f1f58cbd39c70b09294398f7dd7c620939bab54ad6f934fa", size = 184705764, upload-time = "2026-06-17T20:03:59.064Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/09/5683146fda6a2b569deb78ccfd8fbfea8bfe55f726b081c0a6bb18dd6f28/triton-3.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2020153b08280415ec0da6607834e79166442147e78e144df06b508c75b186d2", size = 197729537, upload-time = "2026-06-17T19:53:35.516Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/f8/448220c3092019f9fdfab39ec47985968181d67da34b44f6a7f6280a5cbb/triton-3.7.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c58e4c61f0c73b5dba3b5d19b4a7093c32f90dc18b2a7f121a7c16ccd31107b7", size = 184814760, upload-time = "2026-06-17T20:04:04.984Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/ac/229b7d4589d2e5937310e72c6d46e89599d16a4a12b479ffa1499fee8eb8/triton-3.7.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10ba85fa2cca4a2fbdeb36bf1cb082f2c252bda55bf9fccd74f65ec5bc647e68", size = 197824404, upload-time = "2026-06-17T19:53:42.772Z" },
+]
+
+[[package]]
+name = "typer"
+version = "0.26.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "rich" },
+ { name = "shellingham" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6d/bc/4eae18cd40c65798a16267572ba346c11f599d44b01603dbd843342042bc/typing_inspection-0.4.3.tar.gz", hash = "sha256:c5f9ec1530b5c1e2c9bc34a84d9a3466ed1b2f3f2fa9f901368d9c5596210e4d", size = 76711, upload-time = "2026-08-10T09:39:18.063Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/f7/7a3935abdebd5cf18705a5f0335dd6a3a18bef3baa7cb9edc3b6b9922cc8/typing_inspection-0.4.3-py3-none-any.whl", hash = "sha256:5f42b23858a91e0b4ef521f5418f03a0da3c9216fd2995ef5e73463100e676cd", size = 14693, upload-time = "2026-08-10T09:39:16.693Z" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2026.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
+]
+
+[[package]]
+name = "websockets"
+version = "16.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" },
+ { url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" },
+ { url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" },
+ { url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" },
+ { url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" },
+ { url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" },
+ { url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" },
+ { url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" },
+ { url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" },
+ { url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" },
+ { url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" },
+ { url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" },
+ { url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" },
+ { url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" },
+ { url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" },
+ { url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" },
+ { url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" },
+ { url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" },
+ { url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" },
+ { url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" },
+ { url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" },
+ { url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" },
+ { url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" },
+ { url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" },
+ { url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" },
+ { url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" },
+ { url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" },
+ { url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" },
+ { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" },
+]
+
+[[package]]
+name = "xlsxwriter"
+version = "3.2.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940, upload-time = "2025-09-16T00:16:21.63Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" },
]
From f220bcd95ae5ca3f1bffde21a36d7111af3034e7 Mon Sep 17 00:00:00 2001
From: Andriy Oblivantsev
Date: Tue, 11 Aug 2026 23:57:39 +0100
Subject: [PATCH 04/19] kbsearch: Go implementation with daemon model serving
- New nested module bin/kbsearch with Go implementation of bin/kb/search
- Embedding model (potion-multilingual-128M) served by localhost daemon
so repeated CLI calls reuse the loaded model
- Bash launcher bin/kb/search builds binary on first run, caches to var/bin/
- Hybrid FTS + vector search (RRF k=60) matching Python kblib behavior
- YAML output via port of yamlout.py (ordered keys, same format)
- JSON output with proper field order
- All flags: --root, --repo, -n, --json, --list-model
- Root go.mod reverted to 1.25.0 (kbsearch is isolated nested module)
- CI passes: go test ./... and go vet ./... unaffected by kbsearch
---
.gitignore | 3 +-
bin/kb/search | 110 +++------
bin/kbsearch/brain.go | 88 ++++++++
bin/kbsearch/go.mod | 23 ++
bin/kbsearch/go.sum | 44 ++++
bin/kbsearch/main.go | 44 ++++
bin/kbsearch/model.go | 236 ++++++++++++++++++++
bin/kbsearch/modeldir.go | 60 +++++
bin/kbsearch/search.go | 465 +++++++++++++++++++++++++++++++++++++++
bin/kbsearch/types.go | 16 ++
bin/kbsearch/yaml.go | 107 +++++++++
11 files changed, 1119 insertions(+), 77 deletions(-)
create mode 100644 bin/kbsearch/brain.go
create mode 100644 bin/kbsearch/go.mod
create mode 100644 bin/kbsearch/go.sum
create mode 100644 bin/kbsearch/main.go
create mode 100644 bin/kbsearch/model.go
create mode 100644 bin/kbsearch/modeldir.go
create mode 100644 bin/kbsearch/search.go
create mode 100644 bin/kbsearch/types.go
create mode 100644 bin/kbsearch/yaml.go
diff --git a/.gitignore b/.gitignore
index 20bf5c2..defce95 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,5 @@ __pycache__/
.DS_Store
*.env
.env
-.secrets/
\ No newline at end of file
+.secrets/
+lib-ladybug/
diff --git a/bin/kb/search b/bin/kb/search
index e759371..3c24a47 100755
--- a/bin/kb/search
+++ b/bin/kb/search
@@ -1,76 +1,34 @@
-#!/usr/bin/env python3
-"""kb/search - deduction search over the 2dph brain.
-
- bin/kb/search "query" # hybrid facts+info, YAML out
- bin/kb/search "query" --root facts # confirmed facts only
- bin/kb/search "query" --hop 1 # follow graph edges after hitting
- bin/kb/search "query" --json | yq '.'
- bin/kb/search "query" -n 5 # more results
-
-Deduction order: facts root first (confirmed answers with evidence links),
-then info root (marked `(not confirmed)`). --root restricts to one root.
---hop N walks FROM_FILE edges (sibling leafs in the same source file).
-"""
-from __future__ import annotations
-
-import json
-import sys
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-sys.path.insert(0, str(ROOT / "bin" / "tools"))
-
-from kblib import connect, hybrid_search, init_schema, open_readonly, query_fts # noqa: E402
-from yamlout import to_yaml # noqa: E402
-import ladybug # noqa: E402
-
-
-def main(argv: list[str]) -> int:
- import argparse
- p = argparse.ArgumentParser(description="deduction search over the brain")
- p.add_argument("query")
- p.add_argument("--root", choices=("facts", "info", None), default=None)
- p.add_argument("--repo", default=None, help="filter results to one repo (source prefix)")
- p.add_argument("--hop", type=int, default=0)
- p.add_argument("-n", "--limit", type=int, default=10)
- p.add_argument("--json", action="store_true")
- a = p.parse_args(argv)
-
- try:
- db, conn = open_readonly()
- except FileNotFoundError as e:
- print(e, file=sys.stderr)
- return 1
-
- from model2vec import StaticModel
- model = StaticModel.from_pretrained("minishlab/potion-multilingual-128M")
- emb = model.encode([a.query])[0].astype(float).tolist()
-
- rhs: list[dict] = []
- try:
- rhs = query_fts(conn, a.query, a.limit * 2)
- except Exception:
- rhs = []
-
- results = hybrid_search(conn, emb, rhs, a.limit)
- if a.root:
- results = [h for h in results if h["root"] == a.root]
- if a.repo:
- repo = a.repo
- results = [h for h in results if repo in (h.get("source") or "")]
-
- for hit in results:
- hit.pop("rrf", None)
- if hit.get("text"):
- hit["snippet"] = hit["text"][:280]
-
- out = {"query": a.query, "root_filter": a.root or "facts+info",
- "count": len(results), "results": results}
- print(json.dumps(out, indent=2, ensure_ascii=False) if a.json else to_yaml(out))
- conn.close()
- db.close()
- return 0
-
-
-if __name__ == "__main__":
- sys.exit(main(sys.argv[1:]))
\ No newline at end of file
+#!/usr/bin/env bash
+# bin/kb/search - Go deduction search over the brain (model served by daemon).
+# Builds the kbsearch binary on first run / when source changes, then execs it.
+set -euo pipefail
+
+KB="$(cd "$(dirname "$0")/../.." && pwd)"
+BIN="$KB/var/bin/kbsearch"
+SRC="$KB/bin/kbsearch"
+
+mkdir -p "$KB/var/bin"
+
+# Rebuild if binary missing or any .go source newer
+need_build=0
+if [ ! -x "$BIN" ]; then
+ need_build=1
+else
+ # Check if any .go in kbsearch is newer than binary
+ while IFS= read -r -d '' f; do
+ if [ "$f" -nt "$BIN" ]; then
+ need_build=1
+ break
+ fi
+ done < <(find "$SRC" -name '*.go' -print0 2>/dev/null)
+fi
+
+if [ "$need_build" -eq 1 ]; then
+ echo "Building kbsearch..." >&2
+ (cd "$SRC" && \
+ CGO_CFLAGS="-I$KB/lib-ladybug" \
+ CGO_LDFLAGS="-L$KB/lib-ladybug -Wl,-rpath,$KB/lib-ladybug" \
+ go build -tags system_ladybug -o "$BIN" .) || exit 1
+fi
+
+exec "$BIN" "$@"
\ No newline at end of file
diff --git a/bin/kbsearch/brain.go b/bin/kbsearch/brain.go
new file mode 100644
index 0000000..e9697ff
--- /dev/null
+++ b/bin/kbsearch/brain.go
@@ -0,0 +1,88 @@
+// Brain connection management using go-ladybug.
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ lbug "github.com/LadybugDB/go-ladybug"
+)
+
+var (
+ db *lbug.Database
+ conn *lbug.Connection
+)
+
+func repoRoot() string {
+ // Try KB_ROOT env, then walk up from binary
+ if v := os.Getenv("KB_ROOT"); v != "" {
+ return v
+ }
+ self, err := os.Executable()
+ if err == nil {
+ dir := filepath.Dir(self)
+ for i := 0; i < 5; i++ {
+ if _, err := os.Stat(filepath.Join(dir, "var")); err == nil {
+ return dir
+ }
+ if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
+ return dir
+ }
+ parent := filepath.Dir(dir)
+ if parent == dir {
+ break
+ }
+ dir = parent
+ }
+ }
+ return "."
+}
+
+func dbPath() string {
+ return filepath.Join(repoRoot(), "var", "kb.lbug")
+}
+
+func openBrain() error {
+ return openWithOpts(2, eps())
+}
+
+func openWithOpts(allow int, epsv string) error {
+ cfg := lbug.DefaultSystemConfig()
+ cfg.MaxNumThreads = 8
+ cfg.BufferPoolSize = 1 << 30 // 1GB
+
+ var err error
+ db, err = lbug.OpenDatabase(dbPath(), cfg)
+ if err != nil {
+ return fmt.Errorf("OpenDatabase: %w", err)
+ }
+ if epsv != "" {
+ if _, err := conn.Query("SET STREAM_SANDBOX = '" + epsv + "'"); err != nil {
+ return err
+ }
+ }
+
+ conn, err = lbug.OpenConnection(db)
+ if err != nil {
+ return fmt.Errorf("OpenConnection: %w", err)
+ }
+ if _, err := conn.Query("LOAD EXTENSION FTS"); err != nil {
+ return fmt.Errorf("LOAD EXTENSION FTS: %w", err)
+ }
+ if _, err := conn.Query("LOAD EXTENSION VECTOR"); err != nil {
+ return fmt.Errorf("LOAD EXTENSION VECTOR: %w", err)
+ }
+ return nil
+}
+
+func closeBrain() {
+ if conn != nil {
+ conn.Close()
+ conn = nil
+ }
+ if db != nil {
+ db.Close()
+ db = nil
+ }
+}
\ No newline at end of file
diff --git a/bin/kbsearch/go.mod b/bin/kbsearch/go.mod
new file mode 100644
index 0000000..5054e8e
--- /dev/null
+++ b/bin/kbsearch/go.mod
@@ -0,0 +1,23 @@
+module github.com/eSlider/2dph/bin/kbsearch
+
+go 1.26.0
+
+require (
+ github.com/LadybugDB/go-ladybug v0.17.0
+ github.com/chewxy/math32 v1.11.2
+ github.com/daulet/tokenizers v1.27.0
+)
+
+require (
+ github.com/apache/arrow-go/v18 v18.6.0 // indirect
+ github.com/goccy/go-json v0.10.6 // indirect
+ github.com/google/flatbuffers v25.12.19+incompatible // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/klauspost/compress v1.18.5 // indirect
+ github.com/klauspost/cpuid/v2 v2.3.0 // indirect
+ github.com/pierrec/lz4/v4 v4.1.26 // indirect
+ github.com/shopspring/decimal v1.4.0 // indirect
+ github.com/zeebo/xxh3 v1.1.0 // indirect
+ golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
+ golang.org/x/sys v0.43.0 // indirect
+)
diff --git a/bin/kbsearch/go.sum b/bin/kbsearch/go.sum
new file mode 100644
index 0000000..0064c1b
--- /dev/null
+++ b/bin/kbsearch/go.sum
@@ -0,0 +1,44 @@
+github.com/LadybugDB/go-ladybug v0.17.0 h1:RXDbkBjrbRmLdEbhGl4CLOIEzSt09gbP0n9UbKDEfwI=
+github.com/LadybugDB/go-ladybug v0.17.0/go.mod h1:GeIXmE8XyF5TFS94NAuTag7vgCC+no/HTBMRA6Rd5Cs=
+github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
+github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
+github.com/apache/arrow-go/v18 v18.6.0 h1:GX/Jyd3R7mCLiECAwY9FWbbaYblie2WXBSz4Sw8fNpM=
+github.com/apache/arrow-go/v18 v18.6.0/go.mod h1:gm3MiPpY82fLYK5VKPB3WoJbsiLVDfT7flD5/vHReKw=
+github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
+github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g=
+github.com/chewxy/math32 v1.11.2 h1:IufN08Zwr1NKuWfY+4Tz55BcwKmyKKNdOP7KtumehnM=
+github.com/chewxy/math32 v1.11.2/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs=
+github.com/daulet/tokenizers v1.27.0 h1:MmFYAEDFz69s/nNQfHg59DWqHz3v94m99kEZ/JbL+s4=
+github.com/daulet/tokenizers v1.27.0/go.mod h1:YjFY1o1HGMyWkQgbXJDghhvke/yFDp2vGdIO2hYs4MQ=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
+github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
+github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
+github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
+github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
+github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
+github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
+github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
+golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
+golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
+golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
+gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/bin/kbsearch/main.go b/bin/kbsearch/main.go
new file mode 100644
index 0000000..c896925
--- /dev/null
+++ b/bin/kbsearch/main.go
@@ -0,0 +1,44 @@
+// bin/kbsearch - the Go implementation of bin/kb/search (nested module so the
+// root `go test ./...` and CI never compile it against native ladyships).
+//
+// Usage (built/run by ./bin/kb/search):
+//
+// kbsearch "query" [--root facts|info] [--repo P] [-n N] [--json]
+// kbsearch serve [port] start the embedding daemon
+// kbsearch --list-model print the resolved model dir
+//
+// The potion-multilingual model is loaded only in `serve`; a CLI reuses the
+// daemon over localhost HTTP (falling back to in-process embedding).
+package main
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "strconv"
+)
+
+func main() {
+ if len(os.Args) > 1 && os.Args[1] == "serve" {
+ port := 17830
+ if len(os.Args) > 2 {
+ if p, err := strconv.Atoi(os.Args[2]); err == nil {
+ port = p
+ }
+ }
+ if err := serve(port); err != nil {
+ log.Fatalf("kbsearch serve: %v", err)
+ }
+ return
+ }
+ if len(os.Args) > 1 && os.Args[1] == "--list-model" {
+ dir, err := modelDir()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ fmt.Println(dir)
+ return
+ }
+ os.Exit(runSearch(os.Args[1:]))
+}
\ No newline at end of file
diff --git a/bin/kbsearch/model.go b/bin/kbsearch/model.go
new file mode 100644
index 0000000..c8735fa
--- /dev/null
+++ b/bin/kbsearch/model.go
@@ -0,0 +1,236 @@
+// StaticModel wraps the potion-multilingual-128m embedding model.
+//
+// Mirrors model2vec.StaticModel: tokenizer (daulet Unigram) + safetensors matrix.
+// Embed(text) applies the same preprocessing: median_token_length pre-truncation,
+// add_special_tokens=false, drop unk (id=1), truncate to 512, mean pool, L2 normalize +1e-32.
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "math"
+ "os"
+ "path/filepath"
+ "sort"
+
+ "github.com/chewxy/math32"
+ "github.com/daulet/tokenizers"
+)
+
+type StaticModel struct {
+ tok *tokenizers.Tokenizer
+ mat []float32 // row-major: vocab_size x 128
+ medianLen int
+ vocabSize int
+ dim int
+}
+
+func loadModel() (*StaticModel, error) {
+ dir, err := modelDir()
+ if err != nil {
+ return nil, err
+ }
+
+ tok, err := tokenizers.FromFile(filepath.Join(dir, "tokenizer.json"))
+ if err != nil {
+ return nil, fmt.Errorf("tokenizer: %w", err)
+ }
+
+ mat, vocabSize, dim, err := loadMatrix(filepath.Join(dir, "model.safetensors"))
+ if err != nil {
+ return nil, fmt.Errorf("safetensors: %w", err)
+ }
+
+ median := medianTokenLength(filepath.Join(dir, "tokenizer.json"))
+
+ return &StaticModel{
+ tok: tok,
+ mat: mat,
+ vocabSize: vocabSize,
+ dim: dim,
+ medianLen: median,
+ }, nil
+}
+
+func (m *StaticModel) Close() error {
+ if m.tok != nil {
+ m.tok.Close()
+ m.tok = nil
+ }
+ return nil
+}
+
+func (m *StaticModel) Embed(text string) ([]float64, error) {
+ const maxLen = 512
+
+ if m.medianLen > 0 {
+ maxChars := maxLen * m.medianLen
+ runes := []rune(text)
+ if len(runes) > maxChars {
+ text = string(runes[:maxChars])
+ }
+ }
+
+ ids, _, err := m.tok.EncodeErr(text, false)
+ if err != nil {
+ return nil, fmt.Errorf("encode: %w", err)
+ }
+
+ filtered := make([]uint32, 0, len(ids))
+ for _, id := range ids {
+ if id != 1 {
+ filtered = append(filtered, id)
+ }
+ if len(filtered) >= maxLen {
+ break
+ }
+ }
+ if len(filtered) == 0 {
+ return make([]float64, m.dim), nil
+ }
+
+ acc := make([]float32, m.dim)
+ for _, id := range filtered {
+ if int(id) >= m.vocabSize {
+ continue
+ }
+ off := int(id) * m.dim
+ for d := 0; d < m.dim; d++ {
+ acc[d] += m.mat[off+d]
+ }
+ }
+ inv := 1.0 / float32(len(filtered))
+ for d := 0; d < m.dim; d++ {
+ acc[d] *= inv
+ }
+
+ var norm float32
+ for d := 0; d < m.dim; d++ {
+ norm += acc[d] * acc[d]
+ }
+ norm = math32.Sqrt(norm) + 1e-32
+ for d := 0; d < m.dim; d++ {
+ acc[d] /= norm
+ }
+
+ out := make([]float64, m.dim)
+ for d := 0; d < m.dim; d++ {
+ out[d] = float64(acc[d])
+ }
+ return out, nil
+}
+
+func medianTokenLength(tokenizerPath string) int {
+ data, err := os.ReadFile(tokenizerPath)
+ if err != nil {
+ return 0
+ }
+ var parsed struct {
+ Model struct {
+ Vocab [][]json.RawMessage `json:"vocab"`
+ } `json:"model"`
+ }
+ if err := json.Unmarshal(data, &parsed); err != nil {
+ return 0
+ }
+ vocab := parsed.Model.Vocab
+ if len(vocab) == 0 {
+ return 0
+ }
+ lengths := make([]int, 0, len(vocab))
+ for _, pair := range vocab {
+ if len(pair) < 1 {
+ continue
+ }
+ var tok string
+ if err := json.Unmarshal(pair[0], &tok); err != nil {
+ continue
+ }
+ lengths = append(lengths, len([]rune(tok)))
+ }
+ if len(lengths) == 0 {
+ return 0
+ }
+ sort.Ints(lengths)
+ return lengths[len(lengths)/2]
+}
+
+func loadMatrix(path string) ([]float32, int, int, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, 0, 0, err
+ }
+ defer f.Close()
+
+ var hdrLen uint64
+ if err := binaryRead(f, &hdrLen); err != nil {
+ return nil, 0, 0, err
+ }
+ hdrBytes := make([]byte, hdrLen)
+ if _, err := io.ReadFull(f, hdrBytes); err != nil {
+ return nil, 0, 0, err
+ }
+
+ var hdr struct {
+ Embeddings struct {
+ Dtype string `json:"dtype"`
+ Shape []int `json:"shape"`
+ Offset []uint64 `json:"data_offsets"`
+ } `json:"embeddings"`
+ }
+ if err := json.Unmarshal(hdrBytes, &hdr); err != nil {
+ return nil, 0, 0, err
+ }
+ if hdr.Embeddings.Dtype != "F32" {
+ return nil, 0, 0, fmt.Errorf("unsupported dtype %s", hdr.Embeddings.Dtype)
+ }
+ if len(hdr.Embeddings.Shape) != 2 {
+ return nil, 0, 0, fmt.Errorf("expected 2D shape, got %v", hdr.Embeddings.Shape)
+ }
+ vocabSize := hdr.Embeddings.Shape[0]
+ dim := hdr.Embeddings.Shape[1]
+ if len(hdr.Embeddings.Offset) != 2 {
+ return nil, 0, 0, fmt.Errorf("bad offsets")
+ }
+ start := hdr.Embeddings.Offset[0]
+ end := hdr.Embeddings.Offset[1]
+ size := end - start
+ if size != uint64(vocabSize*dim*4) {
+ return nil, 0, 0, fmt.Errorf("size mismatch")
+ }
+
+ if _, err := f.Seek(int64(8+hdrLen+start), io.SeekStart); err != nil {
+ return nil, 0, 0, err
+ }
+
+ buf := make([]byte, size)
+ if _, err := io.ReadFull(f, buf); err != nil {
+ return nil, 0, 0, err
+ }
+
+ mat := make([]float32, vocabSize*dim)
+ for i := 0; i < len(mat); i++ {
+ off := i * 4
+ mat[i] = math.Float32frombits(
+ uint32(buf[off]) |
+ uint32(buf[off+1])<<8 |
+ uint32(buf[off+2])<<16 |
+ uint32(buf[off+3])<<24,
+ )
+ }
+ return mat, vocabSize, dim, nil
+}
+
+func binaryRead(r io.Reader, v any) error {
+ switch p := v.(type) {
+ case *uint64:
+ var b [8]byte
+ if _, err := io.ReadFull(r, b[:]); err != nil {
+ return err
+ }
+ *p = uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
+ uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/bin/kbsearch/modeldir.go b/bin/kbsearch/modeldir.go
new file mode 100644
index 0000000..7913745
--- /dev/null
+++ b/bin/kbsearch/modeldir.go
@@ -0,0 +1,60 @@
+// modelDir returns the resolved potion-multilingual-128m model directory.
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+func modelDir() (string, error) {
+ // 1. Explicit env
+ if v := os.Getenv("KBSEARCH_MODEL"); v != "" {
+ return v, nil
+ }
+ // 2. Next to the binary (dev or installed)
+ self, err := os.Executable()
+ if err == nil {
+ if dir, err := filepath.EvalSymlinks(filepath.Dir(self)); err == nil {
+ cand := filepath.Join(dir, "potion-multilingual-128m")
+ if st, err := os.Stat(cand); err == nil && st.IsDir() {
+ return cand, nil
+ }
+ }
+ }
+ // 3. Repo root lib/ (where other scripts expect it)
+ if v := os.Getenv("KB_ROOT"); v != "" {
+ cand := filepath.Join(v, "lib", "potion-multilingual-128m")
+ if st, err := os.Stat(cand); err == nil && st.IsDir() {
+ return cand, nil
+ }
+ }
+ // 4. HF cache (new layout: models--*/snapshots/*)
+ if v := os.Getenv("HF_HOME"); v != "" {
+ base := filepath.Join(v, "hub")
+ if entries, err := os.ReadDir(base); err == nil {
+ for _, e := range entries {
+ if strings.HasPrefix(e.Name(), "models--") {
+ snapDir := filepath.Join(base, e.Name(), "snapshots")
+ if snaps, err := os.ReadDir(snapDir); err == nil {
+ for _, s := range snaps {
+ cand := filepath.Join(snapDir, s.Name())
+ if st, _ := os.Stat(cand); st != nil && st.IsDir() {
+ return cand, nil
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ // 5. Legacy HF cache (symlinked model dir)
+ if v := os.Getenv("HF_HOME"); v != "" {
+ cand := filepath.Join(v, "potion-multilingual-128m")
+ if st, err := os.Stat(cand); err == nil && st.IsDir() {
+ return cand, nil
+ }
+ }
+ return "", fmt.Errorf("model not found (set KBSEARCH_MODEL or KB_ROOT, or download to HF cache)")
+}
\ No newline at end of file
diff --git a/bin/kbsearch/search.go b/bin/kbsearch/search.go
new file mode 100644
index 0000000..e8ff2f2
--- /dev/null
+++ b/bin/kbsearch/search.go
@@ -0,0 +1,465 @@
+// Hybrid FTS + vector search implementation, plus daemon client/server.
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ lbug "github.com/LadybugDB/go-ladybug"
+)
+
+const defaultPort = 17830
+const daemonPath = "/embed"
+const healthPath = "/health"
+
+func runSearch(args []string) int {
+ // Manual flag parsing to allow flags after query (like Python argparse)
+ root := ""
+ repo := ""
+ limit := 20
+ jsonOut := false
+ listModel := false
+
+ var queryArgs []string
+ for i := 0; i < len(args); i++ {
+ switch args[i] {
+ case "--root":
+ if i+1 < len(args) {
+ root = args[i+1]
+ i++
+ }
+ case "--repo":
+ if i+1 < len(args) {
+ repo = args[i+1]
+ i++
+ }
+ case "-n":
+ if i+1 < len(args) {
+ if n, err := strconv.Atoi(args[i+1]); err == nil {
+ limit = n
+ }
+ i++
+ }
+ case "--json":
+ jsonOut = true
+ case "--list-model":
+ listModel = true
+ default:
+ if !strings.HasPrefix(args[i], "-") {
+ queryArgs = append(queryArgs, args[i])
+ }
+ }
+ }
+
+ if listModel {
+ dir, err := modelDir()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ return 1
+ }
+ fmt.Println(dir)
+ return 0
+ }
+
+ query := strings.TrimSpace(strings.Join(queryArgs, " "))
+ if query == "" {
+ fmt.Fprintln(os.Stderr, "usage: kbsearch \"query\" [--root facts|info] [--repo REPO] [-n N] [--json]")
+ return 1
+ }
+
+ if err := openBrain(); err != nil {
+ fmt.Fprintf(os.Stderr, "open brain: %v\n", err)
+ return 1
+ }
+ defer closeBrain()
+
+ emb, err := embedQuery(query)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "embed: %v\n", err)
+ return 1
+ }
+
+ fts, err := queryFTS(query, limit*3)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "fts: %v\n", err)
+ return 1
+ }
+
+ var vec []Hit
+ if vec, err = queryVector(emb, limit*3); err != nil {
+ fmt.Fprintf(os.Stderr, "vec: %v\n", err)
+ }
+
+ results := hybrid(fts, vec, limit)
+
+ if root != "" {
+ results = filterRoot(results, root)
+ }
+ if repo != "" {
+ results = filterRepo(results, repo)
+ }
+ if len(results) > limit {
+ results = results[:limit]
+ }
+
+ for i := range results {
+ if results[i].Text != "" {
+ runes := []rune(results[i].Text)
+ if len(runes) > 280 {
+ runes = runes[:280]
+ }
+ results[i].Snippet = string(runes)
+ }
+ }
+
+ out := Dict{
+ {"query", query},
+ {"root_filter", root},
+ {"count", len(results)},
+ {"results", resultsToDicts(results)},
+ }
+
+ if jsonOut {
+ enc := json.NewEncoder(os.Stdout)
+ enc.SetIndent("", " ")
+ enc.SetEscapeHTML(false)
+ return b2i(enc.Encode(toJSONOut(results, query, root)))
+ }
+ fmt.Print(toYAML(out, 0))
+ return 0
+}
+
+func b2i(err error) int {
+ if err != nil {
+ return 1
+ }
+ return 0
+}
+
+func queryFTS(text string, limit int) ([]Hit, error) {
+ stmt, err := conn.Prepare(
+ "CALL QUERY_FTS_INDEX('Leaf', 'id', $q) " +
+ "RETURN node.id, node.text, node.root, node.source, score ORDER BY score LIMIT $n",
+ )
+ if err != nil {
+ return nil, err
+ }
+ defer stmt.Close()
+ res, err := conn.Execute(stmt, map[string]any{"q": text, "n": limit})
+ if err != nil {
+ return nil, err
+ }
+ return rowsToHits(res)
+}
+
+func queryVector(emb []float64, limit int) ([]Hit, error) {
+ embList := make([]any, len(emb))
+ for i, v := range emb {
+ embList[i] = v
+ }
+ stmt, err := conn.Prepare(
+ "CALL QUERY_VECTOR_INDEX('Leaf', 'Leaf_vec', $q, $n) " +
+ "RETURN node.id, node.text, node.root, node.source, distance ORDER BY distance LIMIT $n",
+ )
+ if err != nil {
+ return nil, err
+ }
+ defer stmt.Close()
+ res, err := conn.Execute(stmt, map[string]any{"q": embList, "n": limit})
+ if err != nil {
+ return nil, err
+ }
+ hits, err := rowsToHits(res)
+ if err != nil {
+ return nil, err
+ }
+ for i := range hits {
+ hits[i].Score = 1.0 - hits[i].Score
+ }
+ return hits, nil
+}
+
+func rowsToHits(res *lbug.QueryResult) ([]Hit, error) {
+ var hits []Hit
+ for res.HasNext() {
+ row, err := res.Next()
+ if err != nil {
+ return nil, err
+ }
+ vals, err := row.GetAsSlice()
+ if err != nil || len(vals) < 5 {
+ continue
+ }
+ id := fmt.Sprint(vals[0])
+ text := fmt.Sprint(vals[1])
+ root := fmt.Sprint(vals[2])
+ source := fmt.Sprint(vals[3])
+ score := float64(vals[4].(float64))
+ hits = append(hits, Hit{ID: id, Text: text, Root: root, Source: source, Score: score})
+ }
+ return hits, nil
+}
+
+// JSON output types
+type jsonOut struct {
+ Query string `json:"query"`
+ RootFilter string `json:"root_filter"`
+ Count int `json:"count"`
+ Results []jsonHit `json:"results"`
+}
+
+type jsonHit struct {
+ ID string `json:"id"`
+ Text string `json:"text"`
+ Root string `json:"root"`
+ Score float64 `json:"score"`
+ Snippet string `json:"snippet,omitempty"`
+}
+
+func toJSONOut(hits []Hit, query, rootFilter string) *jsonOut {
+ out := make([]jsonHit, len(hits))
+ for i, h := range hits {
+ out[i] = jsonHit{
+ ID: h.ID,
+ Text: h.Text,
+ Root: h.Root,
+ Score: h.Score,
+ Snippet: h.Snippet,
+ }
+ }
+ return &jsonOut{
+ Query: query,
+ RootFilter: rootFilter,
+ Count: len(hits),
+ Results: out,
+ }
+}
+
+func hybrid(fts, vec []Hit, limit int) []Hit {
+ byID := make(map[string]Hit)
+ rrf := make(map[string]float64)
+
+ for rank, h := range fts {
+ byID[h.ID] = h
+ rrf[h.ID] += 1.0 / (60 + float64(rank+1))
+ }
+ for rank, h := range vec {
+ if _, ok := byID[h.ID]; !ok {
+ byID[h.ID] = h
+ } else {
+ existing := byID[h.ID]
+ if existing.Score == 0 {
+ existing.Score = h.Score
+ byID[h.ID] = existing
+ }
+ }
+ rrf[h.ID] += 1.0 / (60 + float64(rank+1))
+ }
+
+ type scored struct {
+ id string
+ rrf float64
+ }
+ var scoredList []scored
+ for id, v := range rrf {
+ scoredList = append(scoredList, scored{id, v})
+ }
+ sort.Slice(scoredList, func(i, j int) bool {
+ return scoredList[i].rrf > scoredList[j].rrf
+ })
+
+ var out []Hit
+ for i, s := range scoredList {
+ if i >= limit {
+ break
+ }
+ h := byID[s.id]
+ out = append(out, h)
+ }
+ return out
+}
+
+func filterRoot(hits []Hit, root string) []Hit {
+ var out []Hit
+ for _, h := range hits {
+ if h.Root == root {
+ out = append(out, h)
+ }
+ }
+ return out
+}
+
+func filterRepo(hits []Hit, repo string) []Hit {
+ var out []Hit
+ for _, h := range hits {
+ if strings.Contains(h.Source, repo) {
+ out = append(out, h)
+ }
+ }
+ return out
+}
+
+func resultsToDicts(hits []Hit) []any {
+ out := make([]any, len(hits))
+ for i, h := range hits {
+ d := Dict{
+ {"id", h.ID},
+ {"text", h.Text},
+ {"root", h.Root},
+ {"score", h.Score},
+ }
+ if h.Snippet != "" {
+ d = append(d, KV{"snippet", h.Snippet})
+ }
+ out[i] = d
+ }
+ return out
+}
+
+// --- Daemon server ---
+func serve(port int) error {
+ model, err := loadModel()
+ if err != nil {
+ return fmt.Errorf("load model: %w", err)
+ }
+ defer model.Close()
+
+ mux := http.NewServeMux()
+ mux.HandleFunc(healthPath, func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+ mux.HandleFunc(daemonPath, func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+ var req struct {
+ Text string `json:"text"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ vec, err := model.Embed(req.Text)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
+ return
+ }
+ json.NewEncoder(w).Encode(map[string]any{"vector": vec})
+ })
+
+ addr := fmt.Sprintf("127.0.0.1:%d", port)
+ log.Printf("kbsearch daemon listening on %s", addr)
+ return http.ListenAndServe(addr, mux)
+}
+
+// --- Daemon client ---
+var daemonClient = &http.Client{
+ Timeout: 5 * time.Second,
+ Transport: &http.Transport{
+ DialContext: (&net.Dialer{Timeout: 2 * time.Second}).DialContext,
+ },
+}
+
+func embedQuery(text string) ([]float64, error) {
+ port := defaultPort
+ if envPort := os.Getenv("KBSEARCH_PORT"); envPort != "" {
+ if p, err := strconv.Atoi(envPort); err == nil {
+ port = p
+ }
+ }
+ emb, err := tryDaemon(text, port)
+ if err == nil {
+ return emb, nil
+ }
+
+ model, err := loadModel()
+ if err != nil {
+ return nil, fmt.Errorf("fallback load model: %w", err)
+ }
+ defer model.Close()
+ return model.Embed(text)
+}
+
+func tryDaemon(text string, port int) ([]float64, error) {
+ url := fmt.Sprintf("http://127.0.0.1:%d%s", port, daemonPath)
+ payload := map[string]string{"text": text}
+ body, _ := json.Marshal(payload)
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
+ req.Header.Set("Content-Type", "application/json")
+ resp, err := daemonClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("daemon HTTP %d", resp.StatusCode)
+ }
+ var r struct {
+ Vector []float64 `json:"vector"`
+ Error string `json:"error"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
+ return nil, err
+ }
+ if r.Error != "" {
+ return nil, errors.New(r.Error)
+ }
+ return r.Vector, nil
+}
+
+func ensureDaemon(port int) error {
+ url := fmt.Sprintf("http://127.0.0.1:%d%s", port, healthPath)
+ ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
+ defer cancel()
+ req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
+ if resp, err := daemonClient.Do(req); err == nil {
+ resp.Body.Close()
+ if resp.StatusCode == http.StatusOK {
+ return nil
+ }
+ }
+
+ self, err := os.Executable()
+ if err != nil {
+ return err
+ }
+ cmd := exec.Command(self, "serve", strconv.Itoa(port))
+ cmd.Dir, _ = filepath.Split(self)
+ cmd.Stdout = nil
+ cmd.Stderr = nil
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+
+ for i := 0; i < 40; i++ {
+ time.Sleep(250 * time.Millisecond)
+ req, _ := http.NewRequestWithContext(context.Background(), "GET", url, nil)
+ if resp, err := daemonClient.Do(req); err == nil {
+ resp.Body.Close()
+ if resp.StatusCode == http.StatusOK {
+ return nil
+ }
+ }
+ }
+ return fmt.Errorf("daemon failed to start on port %d", port)
+}
\ No newline at end of file
diff --git a/bin/kbsearch/types.go b/bin/kbsearch/types.go
new file mode 100644
index 0000000..2a403bd
--- /dev/null
+++ b/bin/kbsearch/types.go
@@ -0,0 +1,16 @@
+// Common types and helpers for kbsearch.
+package main
+
+import "os"
+
+func eps() string { return os.Getenv("KBTEST_EPS") }
+
+// Hit is one search result, mirroring the python script's dict shape.
+type Hit struct {
+ ID string `json:"id"`
+ Text string `json:"text"`
+ Root string `json:"root"`
+ Source string `json:"-"` // for repo filtering, not in output
+ Score float64 `json:"score"`
+ Snippet string `json:"snippet,omitempty"`
+}
\ No newline at end of file
diff --git a/bin/kbsearch/yaml.go b/bin/kbsearch/yaml.go
new file mode 100644
index 0000000..74c4f18
--- /dev/null
+++ b/bin/kbsearch/yaml.go
@@ -0,0 +1,107 @@
+// YAML emitter ported from bin/kb/yamlout.py — preserves insertion order.
+package main
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+// KV is an ordered key-value pair for maps.
+type KV struct {
+ K string
+ V any
+}
+
+// Dict is an ordered map (slice of KV).
+type Dict []KV
+
+func toYAML(node any, indent int) string {
+ pad := strings.Repeat(" ", indent)
+ switch n := node.(type) {
+ case Dict:
+ if len(n) == 0 {
+ return pad + "{}\n"
+ }
+ var b strings.Builder
+ for _, kv := range n {
+ switch nv := kv.V.(type) {
+ case Dict:
+ if len(nv) == 0 {
+ b.WriteString(pad + kv.K + ": {}\n")
+ } else {
+ b.WriteString(pad + kv.K + ":\n" + toYAML(nv, indent+1))
+ }
+ case []any:
+ if len(nv) == 0 {
+ b.WriteString(pad + kv.K + ": []\n")
+ } else {
+ b.WriteString(pad + kv.K + ":\n" + toYAML(nv, indent+1))
+ }
+ default:
+ b.WriteString(pad + kv.K + ": " + scalar(nv) + "\n")
+ }
+ }
+ return b.String()
+
+ case []any:
+ if len(n) == 0 {
+ return pad + "[]\n"
+ }
+ var b strings.Builder
+ for _, item := range n {
+ if d, ok := item.(Dict); ok {
+ b.WriteString(pad + "-\n" + toYAML(d, indent+1))
+ } else {
+ b.WriteString(pad + "- " + scalar(item) + "\n")
+ }
+ }
+ return b.String()
+
+ default:
+ return pad + scalar(node) + "\n"
+ }
+}
+
+func scalar(v any) string {
+ switch t := v.(type) {
+ case nil:
+ return "null"
+ case bool:
+ if t {
+ return "true"
+ }
+ return "false"
+ case int:
+ return strconv.Itoa(t)
+ case int64:
+ return strconv.FormatInt(t, 10)
+ case float64:
+ return fmtFloat(t)
+ case float32:
+ return fmtFloat(float64(t))
+ case string:
+ return quoteIfNeeded(t)
+ default:
+ // fallback
+ return fmt.Sprintf("%v", v)
+ }
+}
+
+func fmtFloat(f float64) string {
+ s := strconv.FormatFloat(f, 'g', -1, 64)
+ if !strings.ContainsAny(s, ".eE") {
+ s += ".0"
+ }
+ return s
+}
+
+func quoteIfNeeded(s string) string {
+ if strings.Contains(s, "\n") {
+ return strconv.Quote(s)
+ }
+ if s == "" || strings.ContainsAny(s, ":#'\"[]{}&*!|>%@`") || s != strings.TrimSpace(s) {
+ return strconv.Quote(s)
+ }
+ return s
+}
\ No newline at end of file
From 1d1f6a90fff3f09e7eb12a015129e55f49e7f2ea Mon Sep 17 00:00:00 2001
From: Andriy Oblivantsev
Date: Wed, 12 Aug 2026 13:46:54 +0100
Subject: [PATCH 05/19] Remove curasoft references, rename to detective method
- PLAN.md: replace 'curasoft-detective' with 'detective method'
- README.md: replace curasoft-detective link with plain reference
- test_websearch.py: fix test domain from ticket.curasoft.de to example.com
- Rewrote git history with git-filter-repo to remove all traces
---
PLAN.md | 2 +-
README.md | 2 +-
bin/tools/web-search/test_websearch.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/PLAN.md b/PLAN.md
index b6ad21b..625c7ea 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -25,7 +25,7 @@ detective method: **a fact needs ≥2 independent sources or it is
| # | Question | Answer |
|---|----------|--------|
| D1 | RAG corpus | ops stack (chat, onlyoffice, gitea/NPM, searchxng, observability, ai-bot, mcp-servers, `~/.ssh/config`) + portfolio. Exclude `office.dev` + jobs/applications. |
-| D2 | skill merging | integrate skills **in this project** `skills/`; skip gitea / brain-detective-depe ndent skills. |
+| D2 | skill merging | integrate skills **in this project** `skills/`; skip gitea / brain-dependent skills. |
| D3 | web search | import `web-search`, retire local `searxng-ops`. Vendored here, no remote link. |
| D4 | embeddings | **model2vec** `minishlab/potion-multilingual-128M` instead of embeddinggemma. |
| D5 | parser | **mistune** for MD → leaf extraction (duckdb-md documented as future optional SQL/export layer, not v1). |
diff --git a/README.md b/README.md
index ab9901e..307568c 100644
--- a/README.md
+++ b/README.md
@@ -143,6 +143,6 @@ docker compose up brain-watch # auto re-index on change
Neo4j + Qdrant + Matrix RAG brain
- [agent-skills](https://github.com/eSlider/agent-skills) — upstream
skills (`web-search`, `db-yaml`, …) that 2dph integrates
-- [detective](https://github.com/detective) — the two-source method
+- detective method — the two-source method
See [PLAN.md](PLAN.md) for decisions, execution status, and v2 open questions.
\ No newline at end of file
diff --git a/bin/tools/web-search/test_websearch.py b/bin/tools/web-search/test_websearch.py
index a4d131f..96807a0 100644
--- a/bin/tools/web-search/test_websearch.py
+++ b/bin/tools/web-search/test_websearch.py
@@ -76,7 +76,7 @@ class PhiGuard(unittest.TestCase):
def test_plain_technical_query_passes(self):
self.assertIsNone(ws.phi_reason("Pflegegrad SGB XI Einstufung"))
- self.assertIsNone(ws.phi_reason("site:ticket.detective.de Toureffizienz"))
+ self.assertIsNone(ws.phi_reason("site:example.com technical query"))
def test_long_digit_run_is_refused(self):
self.assertIsNotNone(ws.phi_reason("Kunde 4711220385 Adresse"))
From 13b8b01b4fe558b116d0c90f1c661ecbd85c835f Mon Sep 17 00:00:00 2001
From: Jochen Schultz
Date: Wed, 12 Aug 2026 20:38:30 +0200
Subject: [PATCH 06/19] fix(kbsearch): order FTS hits by score DESC
QUERY_FTS_INDEX was ordered `ORDER BY score LIMIT $n`, i.e. ascending, so
the Go search took the *worst* BM25 matches and fed them into the RRF fusion
in reverse rank order. kblib.py has always used `ORDER BY score DESC`.
Both Cypher statements now live in named consts next to each other so the
FTS (descending BM25) vs vector (ascending cosine distance) asymmetry is
visible in one place.
Co-Authored-By: Claude Opus 5
---
bin/kbsearch/search.go | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/bin/kbsearch/search.go b/bin/kbsearch/search.go
index e8ff2f2..d3fb6b7 100644
--- a/bin/kbsearch/search.go
+++ b/bin/kbsearch/search.go
@@ -25,6 +25,14 @@ const defaultPort = 17830
const daemonPath = "/embed"
const healthPath = "/health"
+// BM25 ranks best-first, so the top hits are the *highest* scores; cosine
+// distance ranks best-first ascending. Both mirror kblib.py.
+const ftsStmt = "CALL QUERY_FTS_INDEX('Leaf', 'id', $q) " +
+ "RETURN node.id, node.text, node.root, node.source, score ORDER BY score DESC LIMIT $n"
+
+const vecStmt = "CALL QUERY_VECTOR_INDEX('Leaf', 'Leaf_vec', $q, $n) " +
+ "RETURN node.id, node.text, node.root, node.source, distance ORDER BY distance LIMIT $n"
+
func runSearch(args []string) int {
// Manual flag parsing to allow flags after query (like Python argparse)
root := ""
@@ -150,10 +158,7 @@ func b2i(err error) int {
}
func queryFTS(text string, limit int) ([]Hit, error) {
- stmt, err := conn.Prepare(
- "CALL QUERY_FTS_INDEX('Leaf', 'id', $q) " +
- "RETURN node.id, node.text, node.root, node.source, score ORDER BY score LIMIT $n",
- )
+ stmt, err := conn.Prepare(ftsStmt)
if err != nil {
return nil, err
}
@@ -170,10 +175,7 @@ func queryVector(emb []float64, limit int) ([]Hit, error) {
for i, v := range emb {
embList[i] = v
}
- stmt, err := conn.Prepare(
- "CALL QUERY_VECTOR_INDEX('Leaf', 'Leaf_vec', $q, $n) " +
- "RETURN node.id, node.text, node.root, node.source, distance ORDER BY distance LIMIT $n",
- )
+ stmt, err := conn.Prepare(vecStmt)
if err != nil {
return nil, err
}
From 6ddb56cef9e2ab71018e12ea3c127f3aa16a60c6 Mon Sep 17 00:00:00 2001
From: Jochen Schultz
Date: Wed, 12 Aug 2026 20:39:44 +0200
Subject: [PATCH 07/19] fix(kbsearch): open the connection before running SET
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
openWithOpts ran `SET STREAM_SANDBOX` on the package-level `conn` while it
was still nil — OpenConnection happened afterwards. Any run with KBTEST_EPS
set panicked instead of opening the brain.
Also: drop the `allow` parameter (never read, hence the rename to
openWithSandbox) and close the database on the error paths so a failed
extension load doesn't leak the open handle.
Not covered by a test: everything in this function needs the native ladybug
library, which the nested kbsearch module cannot build offline.
Co-Authored-By: Claude Opus 5
---
bin/kbsearch/brain.go | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/bin/kbsearch/brain.go b/bin/kbsearch/brain.go
index e9697ff..b57b340 100644
--- a/bin/kbsearch/brain.go
+++ b/bin/kbsearch/brain.go
@@ -44,10 +44,10 @@ func dbPath() string {
}
func openBrain() error {
- return openWithOpts(2, eps())
+ return openWithSandbox(eps())
}
-func openWithOpts(allow int, epsv string) error {
+func openWithSandbox(epsv string) error {
cfg := lbug.DefaultSystemConfig()
cfg.MaxNumThreads = 8
cfg.BufferPoolSize = 1 << 30 // 1GB
@@ -57,20 +57,26 @@ func openWithOpts(allow int, epsv string) error {
if err != nil {
return fmt.Errorf("OpenDatabase: %w", err)
}
- if epsv != "" {
- if _, err := conn.Query("SET STREAM_SANDBOX = '" + epsv + "'"); err != nil {
- return err
- }
- }
conn, err = lbug.OpenConnection(db)
if err != nil {
+ closeBrain()
return fmt.Errorf("OpenConnection: %w", err)
}
+ // Session settings need a live connection; running this before
+ // OpenConnection dereferenced a nil *Connection.
+ if epsv != "" {
+ if _, err := conn.Query("SET STREAM_SANDBOX = '" + epsv + "'"); err != nil {
+ closeBrain()
+ return fmt.Errorf("SET STREAM_SANDBOX: %w", err)
+ }
+ }
if _, err := conn.Query("LOAD EXTENSION FTS"); err != nil {
+ closeBrain()
return fmt.Errorf("LOAD EXTENSION FTS: %w", err)
}
if _, err := conn.Query("LOAD EXTENSION VECTOR"); err != nil {
+ closeBrain()
return fmt.Errorf("LOAD EXTENSION VECTOR: %w", err)
}
return nil
From 23f0ee6c8c3ffa8a30f3b11e81b1e977041e74e1 Mon Sep 17 00:00:00 2001
From: Jochen Schultz
Date: Wed, 12 Aug 2026 20:41:19 +0200
Subject: [PATCH 08/19] fix(kbsearch): filter by root/repo before applying the
limit
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
runSearch fused the FTS and vector hits, cut the list to -n, and only then
applied --root/--repo. Every matching leaf ranked below the cut was thrown
away before the filter ever saw it, so `--root facts` returned nothing as
soon as info leafs filled the top N — the deduction order (facts first) was
unreachable from the Go CLI.
Fusion + filtering now live in rank.go as rankAndFilter(): fuse everything,
filter, then truncate. hybrid() takes limit <= 0 for "keep all" and breaks
RRF ties by id, so the output no longer depends on map iteration order.
rank.go has no cgo dependency; rank_test.go covers filter-before-limit for
both filters, the RRF fusion order and the tie determinism. Verified with a
stub module (the package itself needs the native ladybug lib to build).
Co-Authored-By: Claude Opus 5
---
bin/kbsearch/rank.go | 92 ++++++++++++++++++++++++++++++++
bin/kbsearch/rank_test.go | 108 ++++++++++++++++++++++++++++++++++++++
bin/kbsearch/search.go | 77 +--------------------------
3 files changed, 201 insertions(+), 76 deletions(-)
create mode 100644 bin/kbsearch/rank.go
create mode 100644 bin/kbsearch/rank_test.go
diff --git a/bin/kbsearch/rank.go b/bin/kbsearch/rank.go
new file mode 100644
index 0000000..cf7be33
--- /dev/null
+++ b/bin/kbsearch/rank.go
@@ -0,0 +1,92 @@
+// Ranking of hybrid search hits: reciprocal rank fusion plus the root/repo
+// filters. Pure logic, no database and no model, so rank_test.go covers it.
+package main
+
+import (
+ "sort"
+ "strings"
+)
+
+// rrfK dampens the contribution of low ranks; same constant as kblib.py.
+const rrfK = 60
+
+// rankAndFilter fuses the two hit lists, applies the --root/--repo filters and
+// only then cuts to limit. Cutting first dropped every matching leaf ranked
+// below the cut in the unfiltered list, so `--root facts` came back empty
+// whenever info leafs filled the top N. limit <= 0 keeps everything.
+func rankAndFilter(fts, vec []Hit, root, repo string, limit int) []Hit {
+ out := hybrid(fts, vec, 0)
+ if root != "" {
+ out = filterRoot(out, root)
+ }
+ if repo != "" {
+ out = filterRepo(out, repo)
+ }
+ if limit > 0 && len(out) > limit {
+ out = out[:limit]
+ }
+ return out
+}
+
+// hybrid merges FTS and vector hits by reciprocal rank fusion.
+// limit <= 0 returns the full fused list.
+func hybrid(fts, vec []Hit, limit int) []Hit {
+ byID := make(map[string]Hit, len(fts)+len(vec))
+ rrf := make(map[string]float64, len(fts)+len(vec))
+
+ for rank, h := range fts {
+ byID[h.ID] = h
+ rrf[h.ID] += 1.0 / (rrfK + float64(rank+1))
+ }
+ for rank, h := range vec {
+ if existing, ok := byID[h.ID]; !ok {
+ byID[h.ID] = h
+ } else if existing.Score == 0 {
+ // FTS carried no score for this leaf; keep the cosine one.
+ existing.Score = h.Score
+ byID[h.ID] = existing
+ }
+ rrf[h.ID] += 1.0 / (rrfK + float64(rank+1))
+ }
+
+ ids := make([]string, 0, len(rrf))
+ for id := range rrf {
+ ids = append(ids, id)
+ }
+ // Ties broken by id so output never depends on map iteration order.
+ sort.Slice(ids, func(i, j int) bool {
+ if rrf[ids[i]] != rrf[ids[j]] {
+ return rrf[ids[i]] > rrf[ids[j]]
+ }
+ return ids[i] < ids[j]
+ })
+ if limit > 0 && len(ids) > limit {
+ ids = ids[:limit]
+ }
+
+ out := make([]Hit, 0, len(ids))
+ for _, id := range ids {
+ out = append(out, byID[id])
+ }
+ return out
+}
+
+func filterRoot(hits []Hit, root string) []Hit {
+ var out []Hit
+ for _, h := range hits {
+ if h.Root == root {
+ out = append(out, h)
+ }
+ }
+ return out
+}
+
+func filterRepo(hits []Hit, repo string) []Hit {
+ var out []Hit
+ for _, h := range hits {
+ if strings.Contains(h.Source, repo) {
+ out = append(out, h)
+ }
+ }
+ return out
+}
diff --git a/bin/kbsearch/rank_test.go b/bin/kbsearch/rank_test.go
new file mode 100644
index 0000000..de2fb20
--- /dev/null
+++ b/bin/kbsearch/rank_test.go
@@ -0,0 +1,108 @@
+// Unit tests for the pure ranking/filtering stage (no db, no model, offline).
+package main
+
+import "testing"
+
+func h(id, root, source string) Hit {
+ return Hit{ID: id, Text: id, Root: root, Source: source}
+}
+
+func ids(hits []Hit) []string {
+ out := make([]string, len(hits))
+ for i, hit := range hits {
+ out[i] = hit.ID
+ }
+ return out
+}
+
+func eq(t *testing.T, got []Hit, want ...string) {
+ t.Helper()
+ g := ids(got)
+ if len(g) != len(want) {
+ t.Fatalf("got %v, want %v", g, want)
+ }
+ for i := range want {
+ if g[i] != want[i] {
+ t.Fatalf("got %v, want %v", g, want)
+ }
+ }
+}
+
+// A facts leaf that ranks below the limit in the unfiltered list must still
+// be returned for --root facts. Filtering after truncation loses it.
+func TestRankAndFilterFiltersBeforeLimit(t *testing.T) {
+ fts := []Hit{
+ h("i1", "info", "docs/a.md"),
+ h("i2", "info", "docs/b.md"),
+ h("i3", "info", "docs/c.md"),
+ h("f1", "facts", "docker ps x compose"),
+ }
+ eq(t, rankAndFilter(fts, nil, "facts", "", 2), "f1")
+}
+
+func TestRankAndFilterRepoFiltersBeforeLimit(t *testing.T) {
+ fts := []Hit{
+ h("a", "info", "eSlider/2dph:README.md"),
+ h("b", "info", "eSlider/2dph:PLAN.md"),
+ h("c", "info", "eSlider/ops:compose.yaml"),
+ }
+ eq(t, rankAndFilter(fts, nil, "", "ops", 2), "c")
+}
+
+func TestRankAndFilterTruncatesToLimit(t *testing.T) {
+ fts := []Hit{h("a", "info", "x"), h("b", "info", "x"), h("c", "info", "x")}
+ eq(t, rankAndFilter(fts, nil, "", "", 2), "a", "b")
+}
+
+func TestRankAndFilterLimitZeroKeepsAll(t *testing.T) {
+ fts := []Hit{h("a", "info", "x"), h("b", "info", "x")}
+ eq(t, rankAndFilter(fts, nil, "", "", 0), "a", "b")
+}
+
+// A leaf found by both retrievers outranks one found by a single retriever,
+// even when the single-retriever hit sits at rank 1 of its list.
+func TestHybridFusesBothRetrievers(t *testing.T) {
+ fts := []Hit{h("only-fts", "info", "x"), h("both", "info", "x")}
+ vec := []Hit{h("only-vec", "info", "x"), h("both", "info", "x")}
+ eq(t, hybrid(fts, vec, 0), "both", "only-fts", "only-vec")
+}
+
+// Equal RRF scores must not depend on Go's randomized map iteration.
+func TestHybridTiesAreDeterministic(t *testing.T) {
+ fts := []Hit{h("b", "info", "x"), h("a", "info", "x")}
+ first := ids(hybrid(fts, nil, 0))
+ for i := 0; i < 50; i++ {
+ got := ids(hybrid(fts, nil, 0))
+ for j := range first {
+ if got[j] != first[j] {
+ t.Fatalf("unstable order: %v then %v", first, got)
+ }
+ }
+ }
+}
+
+// Vector hits keep the similarity score when FTS contributed none.
+func TestHybridKeepsVectorScoreForSharedHit(t *testing.T) {
+ fts := []Hit{{ID: "x", Root: "info", Score: 0}}
+ vec := []Hit{{ID: "x", Root: "info", Score: 0.87}}
+ got := hybrid(fts, vec, 0)
+ if len(got) != 1 || got[0].Score != 0.87 {
+ t.Fatalf("got %+v, want score 0.87", got)
+ }
+}
+
+// Regression guard for the FTS statement: BM25 scores rank best-first.
+func TestFTSQueryOrdersByScoreDescending(t *testing.T) {
+ if !contains(ftsStmt, "ORDER BY score DESC") {
+ t.Fatalf("FTS query must order by score DESC, got:\n%s", ftsStmt)
+ }
+}
+
+func contains(s, sub string) bool {
+ for i := 0; i+len(sub) <= len(s); i++ {
+ if s[i:i+len(sub)] == sub {
+ return true
+ }
+ }
+ return false
+}
diff --git a/bin/kbsearch/search.go b/bin/kbsearch/search.go
index d3fb6b7..f364a4a 100644
--- a/bin/kbsearch/search.go
+++ b/bin/kbsearch/search.go
@@ -13,7 +13,6 @@ import (
"os"
"os/exec"
"path/filepath"
- "sort"
"strconv"
"strings"
"time"
@@ -111,17 +110,7 @@ func runSearch(args []string) int {
fmt.Fprintf(os.Stderr, "vec: %v\n", err)
}
- results := hybrid(fts, vec, limit)
-
- if root != "" {
- results = filterRoot(results, root)
- }
- if repo != "" {
- results = filterRepo(results, repo)
- }
- if len(results) > limit {
- results = results[:limit]
- }
+ results := rankAndFilter(fts, vec, root, repo, limit)
for i := range results {
if results[i].Text != "" {
@@ -250,70 +239,6 @@ func toJSONOut(hits []Hit, query, rootFilter string) *jsonOut {
}
}
-func hybrid(fts, vec []Hit, limit int) []Hit {
- byID := make(map[string]Hit)
- rrf := make(map[string]float64)
-
- for rank, h := range fts {
- byID[h.ID] = h
- rrf[h.ID] += 1.0 / (60 + float64(rank+1))
- }
- for rank, h := range vec {
- if _, ok := byID[h.ID]; !ok {
- byID[h.ID] = h
- } else {
- existing := byID[h.ID]
- if existing.Score == 0 {
- existing.Score = h.Score
- byID[h.ID] = existing
- }
- }
- rrf[h.ID] += 1.0 / (60 + float64(rank+1))
- }
-
- type scored struct {
- id string
- rrf float64
- }
- var scoredList []scored
- for id, v := range rrf {
- scoredList = append(scoredList, scored{id, v})
- }
- sort.Slice(scoredList, func(i, j int) bool {
- return scoredList[i].rrf > scoredList[j].rrf
- })
-
- var out []Hit
- for i, s := range scoredList {
- if i >= limit {
- break
- }
- h := byID[s.id]
- out = append(out, h)
- }
- return out
-}
-
-func filterRoot(hits []Hit, root string) []Hit {
- var out []Hit
- for _, h := range hits {
- if h.Root == root {
- out = append(out, h)
- }
- }
- return out
-}
-
-func filterRepo(hits []Hit, repo string) []Hit {
- var out []Hit
- for _, h := range hits {
- if strings.Contains(h.Source, repo) {
- out = append(out, h)
- }
- }
- return out
-}
-
func resultsToDicts(hits []Hit) []any {
out := make([]any, len(hits))
for i, h := range hits {
From a60f695fa2126e5992da8bc5456278551b68bb19 Mon Sep 17 00:00:00 2001
From: Jochen Schultz
Date: Wed, 12 Aug 2026 21:06:59 +0200
Subject: [PATCH 09/19] fix(kbsearch): actually start the embedding daemon
ensureDaemon() was written but never called: every query fell straight
through to the in-process fallback and loaded the ~100MB potion matrix
again, which is exactly what the daemon exists to avoid.
embedQuery now tries the daemon, starts one in the background when nothing
answers, and retries once before falling back. The daemon gets its own
session (Setsid) so a Ctrl+C in the launching terminal does not kill it with
the foreground process group. KBSEARCH_NO_DAEMON=1 opts out for one-shot
containers and CI.
Verified against a stub-model build of the daemon half: cold run spawns the
daemon and answers, the daemon survives the CLI exit (health 200), the next
run reuses it, and the opt-out path stays in-process.
Co-Authored-By: Claude Opus 5
---
bin/kbsearch/main.go | 4 +++-
bin/kbsearch/search.go | 18 ++++++++++++++++--
2 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/bin/kbsearch/main.go b/bin/kbsearch/main.go
index c896925..8815356 100644
--- a/bin/kbsearch/main.go
+++ b/bin/kbsearch/main.go
@@ -8,7 +8,9 @@
// kbsearch --list-model print the resolved model dir
//
// The potion-multilingual model is loaded only in `serve`; a CLI reuses the
-// daemon over localhost HTTP (falling back to in-process embedding).
+// daemon over localhost HTTP (KBSEARCH_PORT, default 17830) and starts one in
+// the background when none answers. KBSEARCH_NO_DAEMON=1 skips that and embeds
+// in-process instead, which costs a full model load per query.
package main
import (
diff --git a/bin/kbsearch/search.go b/bin/kbsearch/search.go
index f364a4a..29a20e5 100644
--- a/bin/kbsearch/search.go
+++ b/bin/kbsearch/search.go
@@ -15,6 +15,7 @@ import (
"path/filepath"
"strconv"
"strings"
+ "syscall"
"time"
lbug "github.com/LadybugDB/go-ladybug"
@@ -309,11 +310,21 @@ func embedQuery(text string) ([]float64, error) {
port = p
}
}
- emb, err := tryDaemon(text, port)
- if err == nil {
+ if emb, err := tryDaemon(text, port); err == nil {
return emb, nil
}
+ // No daemon yet: start one in the background (it outlives this process and
+ // serves every later query from RAM) and retry once. KBSEARCH_NO_DAEMON=1
+ // keeps a run self-contained, e.g. in CI or a one-shot container.
+ if os.Getenv("KBSEARCH_NO_DAEMON") == "" {
+ if err := ensureDaemon(port); err == nil {
+ if emb, err := tryDaemon(text, port); err == nil {
+ return emb, nil
+ }
+ }
+ }
+ // Last resort: load the ~100MB matrix into this process, once per query.
model, err := loadModel()
if err != nil {
return nil, fmt.Errorf("fallback load model: %w", err)
@@ -374,6 +385,9 @@ func ensureDaemon(port int) error {
cmd.Dir, _ = filepath.Split(self)
cmd.Stdout = nil
cmd.Stderr = nil
+ // Own session: a Ctrl+C in the terminal that launched the search must not
+ // take the daemon down with the foreground process group.
+ cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
if err := cmd.Start(); err != nil {
return err
}
From 4916638328b5b0eccb6c1461928c2c9aad2221e1 Mon Sep 17 00:00:00 2001
From: Jochen Schultz
Date: Thu, 13 Aug 2026 02:09:44 +0200
Subject: [PATCH 10/19] refactor(facts): move the 2-source pairing rule into
bin/tools/factsrules.py
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The evidence rule (D8) lived inline in facts/extract, tangled with docker
subprocess calls and file reads, so nothing could test it and facts/audit
could not re-check it.
factsrules is pure: observations in, Facts out. make_fact() refuses fewer
than 2 *distinct* sources, so a single observation cannot reach the facts
root by accident. Compose candidates are now scoped per container — the flat
{file: services} shape I started with would have paired a container `db`
against an unrelated project's compose.yaml that also declares `db`, which
is not a second source at all.
extract keeps collecting the observations; behaviour is unchanged, verified
by diffing --dry-run --json output of both versions against a fixture ssh
config (byte identical, 2 facts).
Co-Authored-By: Claude Opus 5
---
bin/facts/extract | 98 ++++++++----------------
bin/tools/factsrules.py | 140 +++++++++++++++++++++++++++++++++++
bin/tools/test_factsrules.py | 90 ++++++++++++++++++++++
3 files changed, 261 insertions(+), 67 deletions(-)
create mode 100644 bin/tools/factsrules.py
create mode 100644 bin/tools/test_factsrules.py
diff --git a/bin/facts/extract b/bin/facts/extract
index 62a9d88..7f2d6a2 100755
--- a/bin/facts/extract
+++ b/bin/facts/extract
@@ -101,78 +101,42 @@ def mentions(term: str, files: list[Path]) -> bool:
def build_facts() -> list[dict]:
- facts: list[dict] = []
- doc_files = [ROOT / m for m in DOC_MARKERS]
+ """Gather observations; factsrules decides which pairings become facts."""
+ import factsrules
+ doc_files = [ROOT / m for m in DOC_MARKERS]
running = read_docker_ps()
- # Pair each running container against its own compose file (2 independent
- # sources: runtime state docker ps × declared state compose).
- runtime_facts = 0
+
+ # Compose candidates stay scoped to the container they were found for.
+ compose_by_container: dict[str, dict[str, list[str]]] = {}
for name in running:
cdir = container_compose_dir(name)
- for cfile in compose_files_in(cdir) if cdir else []:
- if name in read_compose_services(cfile):
- facts.append({
- "text": f"container '{name}' is running and declared in {cfile.name}",
- "source": f"docker ps x compose:{cfile.name}",
- "loc": f"{cfile}:{name}",
- "how": "facts/extract",
- })
- runtime_facts += 1
- break
- if runtime_facts:
- print(f"facts/extract: paired {runtime_facts}/{len(running)} running containers to compose", file=sys.stderr)
-
- compose = [Path(p) for p in COMPOSE_FILES]
- compose_services = set()
- for c in compose:
- compose_services.update(read_compose_services(c))
- if compose_services and running:
- overlap = sorted(compose_services & set(running))
- for name in overlap:
- facts.append({
- "text": f"container '{name}' is running and declared in compose",
- "source": f"docker ps x {compose[0].name}",
- "loc": "docker ps; docker compose config",
- "how": "facts/extract",
- })
+ compose_by_container[name] = {
+ str(cfile): read_compose_services(cfile)
+ for cfile in (compose_files_in(cdir) if cdir else [])
+ }
+
+ repo_services: list[str] = []
+ for c in COMPOSE_FILES:
+ repo_services.extend(read_compose_services(Path(c)))
hosts = read_ssh_hosts(SSH_CONFIG)
- for host in hosts:
- if mentions(host, doc_files):
- facts.append({
- "text": f"host '{host}' is configured in ~/.ssh/config and referenced in this repo",
- "source": f"ssh config x docs({', '.join(DOC_MARKERS)})",
- "loc": f"~/.ssh/config:{host}",
- "how": "facts/extract",
- })
-
- # Single-docker-container facts still need 2 sources: running + hostname hint
- for name in running:
- known_hosts = set(hosts)
- if not known_hosts:
- break
- # a running container name that also matches a configured host
- if name in known_hosts:
- facts.append({
- "text": f"container '{name}' is running and matches configured host '{name}'",
- "source": "docker ps x ssh config",
- "loc": f"docker ps:{name}; ~/.ssh/config:{name}",
- "how": "facts/extract",
- })
- return facts
-
-
-def dedupe(facts: list[dict]) -> list[dict]:
- seen = set()
- out = []
- for f in facts:
- key = f["text"]
- if key in seen:
- continue
- seen.add(key)
- out.append(f)
- return out
+ doc_terms = {h for h in hosts if mentions(h, doc_files)}
+
+ facts = factsrules.pair_all(
+ running=running,
+ compose_by_container=compose_by_container,
+ repo_services=repo_services,
+ repo_compose_name=Path(COMPOSE_FILES[0]).name,
+ ssh_hosts=hosts,
+ doc_terms=doc_terms,
+ doc_markers=DOC_MARKERS,
+ )
+ paired = sum(1 for f in facts if any(s.startswith("compose:") for s in f.sources))
+ if paired:
+ print(f"facts/extract: paired {paired}/{len(running)} running containers to compose",
+ file=sys.stderr)
+ return [f.as_dict() for f in facts]
def write_facts(facts: list[dict]) -> None:
@@ -207,7 +171,7 @@ def main(argv: list[str]) -> int:
if a.ssh:
SSH_CONFIG = Path(a.ssh)
- facts = dedupe(build_facts())
+ facts = build_facts() # factsrules.pair_all already dedupes by text
if not a.dry_run and facts:
write_facts(facts)
diff --git a/bin/tools/factsrules.py b/bin/tools/factsrules.py
new file mode 100644
index 0000000..68c057b
--- /dev/null
+++ b/bin/tools/factsrules.py
@@ -0,0 +1,140 @@
+"""factsrules - the 2-source pairing rule behind bin/facts/extract.
+
+Pure functions: no subprocess, no filesystem, no database. bin/facts/extract
+gathers the observations (docker ps, compose services, ssh config, doc
+mentions) and this module decides which pairings are strong enough to become a
+`facts` leaf. bin/facts/audit re-checks the rule against a fixture, so the
+gate fails when someone loosens it.
+
+The rule (AGENTS.md D8): an assertion needs >=2 *independent* sources or it is
+`(not confirmed)`. make_fact() refuses to build a Fact from fewer, so a single
+observation cannot reach the facts root by accident.
+"""
+from __future__ import annotations
+
+from dataclasses import dataclass
+from pathlib import PurePath
+
+MIN_SOURCES = 2
+HOW = "facts/extract"
+
+
+@dataclass(frozen=True)
+class Fact:
+ """One confirmed assertion plus the sources it was paired from."""
+
+ text: str
+ sources: tuple[str, ...]
+ loc: str
+ how: str = HOW
+
+ @property
+ def source(self) -> str:
+ """Evidence string as stored on the leaf; facts/audit db greps ' x '."""
+ return " x ".join(self.sources)
+
+ def as_dict(self) -> dict:
+ return {"text": self.text, "source": self.source, "loc": self.loc, "how": self.how}
+
+
+def make_fact(text: str, sources: list[str], loc: str, how: str = HOW) -> Fact:
+ """Build a Fact or refuse. Independent means distinct: the same observation
+ named twice is one source, not two."""
+ distinct = {s for s in sources if s}
+ if len(distinct) < MIN_SOURCES:
+ raise ValueError(
+ f"fact needs >={MIN_SOURCES} independent sources, got {sorted(distinct)}: {text!r}"
+ )
+ return Fact(text=text, sources=tuple(sources), loc=loc, how=how)
+
+
+def pair_container_compose(compose_by_container: dict[str, dict[str, list[str]]]) -> list[Fact]:
+ """S1 runtime (docker ps) x S2 declared (the container's *own* compose file).
+
+ Takes {container: {compose_path: [services]}} — candidates are scoped per
+ container, because a service name like `db` occurs in many unrelated
+ projects and pairing across them would not be a second source at all.
+ First matching compose file wins, one fact per container.
+ """
+ facts: list[Fact] = []
+ for name, files in compose_by_container.items():
+ for path, services in files.items():
+ if name in services:
+ fname = PurePath(path).name
+ facts.append(make_fact(
+ text=f"container '{name}' is running and declared in {fname}",
+ sources=["docker ps", f"compose:{fname}"],
+ loc=f"{path}:{name}",
+ ))
+ break
+ return facts
+
+
+def pair_container_repo_compose(running: list[str], repo_services: list[str],
+ compose_name: str) -> list[Fact]:
+ """S1 runtime x S2 the repo's own compose file."""
+ overlap = sorted(set(repo_services) & set(running))
+ return [
+ make_fact(
+ text=f"container '{name}' is running and declared in compose",
+ sources=["docker ps", compose_name],
+ loc="docker ps; docker compose config",
+ )
+ for name in overlap
+ ]
+
+
+def pair_host_docs(ssh_hosts: list[str], doc_terms: set[str], doc_markers: list[str]) -> list[Fact]:
+ """S1 ~/.ssh/config x S2 a doc in this repo naming the same host."""
+ marker = ", ".join(doc_markers)
+ return [
+ make_fact(
+ text=f"host '{host}' is configured in ~/.ssh/config and referenced in this repo",
+ sources=["ssh config", f"docs({marker})"],
+ loc=f"~/.ssh/config:{host}",
+ )
+ for host in ssh_hosts if host in doc_terms
+ ]
+
+
+def pair_container_host(running: list[str], ssh_hosts: list[str]) -> list[Fact]:
+ """S1 docker ps x S2 ~/.ssh/config naming the same thing."""
+ known = set(ssh_hosts)
+ if not known:
+ return []
+ return [
+ make_fact(
+ text=f"container '{name}' is running and matches configured host '{name}'",
+ sources=["docker ps", "ssh config"],
+ loc=f"docker ps:{name}; ~/.ssh/config:{name}",
+ )
+ for name in running if name in known
+ ]
+
+
+DOC_MARKERS = ["README.md", "PLAN.md", "AGENTS.md"]
+
+
+def pair_all(*, running: list[str], compose_by_container: dict[str, dict[str, list[str]]],
+ repo_services: list[str], repo_compose_name: str,
+ ssh_hosts: list[str], doc_terms: set[str],
+ doc_markers: list[str] | None = None) -> list[Fact]:
+ """Every pairing, deduped by text, order preserved."""
+ doc_markers = doc_markers or DOC_MARKERS
+ facts = pair_container_compose(compose_by_container)
+ if repo_services and running:
+ facts += pair_container_repo_compose(running, repo_services, repo_compose_name)
+ facts += pair_host_docs(ssh_hosts, doc_terms, doc_markers)
+ facts += pair_container_host(running, ssh_hosts)
+ return dedupe(facts)
+
+
+def dedupe(facts: list[Fact]) -> list[Fact]:
+ seen: set[str] = set()
+ out: list[Fact] = []
+ for fact in facts:
+ if fact.text in seen:
+ continue
+ seen.add(fact.text)
+ out.append(fact)
+ return out
diff --git a/bin/tools/test_factsrules.py b/bin/tools/test_factsrules.py
new file mode 100644
index 0000000..5bb7c9b
--- /dev/null
+++ b/bin/tools/test_factsrules.py
@@ -0,0 +1,90 @@
+import sys
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+
+import factsrules # noqa: E402
+
+
+class MakeFactTest(unittest.TestCase):
+ def test_two_independent_sources_are_accepted(self):
+ fact = factsrules.make_fact("x runs", ["docker ps", "compose:a.yaml"], "a.yaml:x")
+ self.assertEqual(fact.sources, ("docker ps", "compose:a.yaml"))
+
+ def test_single_source_is_rejected(self):
+ with self.assertRaises(ValueError):
+ factsrules.make_fact("x runs", ["docker ps"], "docker ps:x")
+
+ def test_the_same_source_twice_is_not_two_sources(self):
+ with self.assertRaises(ValueError):
+ factsrules.make_fact("x runs", ["docker ps", "docker ps"], "docker ps:x")
+
+ def test_source_string_keeps_the_x_separator(self):
+ # facts/audit db asserts " x " is present in Leaf.source.
+ fact = factsrules.make_fact("x runs", ["docker ps", "ssh config"], "loc")
+ self.assertEqual(fact.source, "docker ps x ssh config")
+ self.assertEqual(fact.as_dict()["source"], "docker ps x ssh config")
+
+
+class PairingTest(unittest.TestCase):
+ def test_container_is_paired_with_its_compose_file(self):
+ facts = factsrules.pair_container_compose(
+ {"chat": {"/srv/chat/compose.yaml": ["chat", "db"]}}
+ )
+ self.assertEqual(len(facts), 1)
+ self.assertIn("declared in compose.yaml", facts[0].text)
+ self.assertEqual(facts[0].sources, ("docker ps", "compose:compose.yaml"))
+
+ def test_running_container_without_any_second_source_stays_out(self):
+ facts = factsrules.pair_all(
+ running=["onlyoffice"],
+ compose_by_container={"onlyoffice": {"/srv/chat/compose.yaml": ["chat"]}},
+ repo_services=[],
+ repo_compose_name="compose.yaml",
+ ssh_hosts=["arc-2"],
+ doc_terms={"arc-2"},
+ )
+ self.assertNotIn("onlyoffice", " ".join(f.text for f in facts))
+
+ def test_host_needs_a_doc_mention(self):
+ paired = factsrules.pair_host_docs(["arc-2"], {"arc-2"}, ["README.md"])
+ self.assertEqual(len(paired), 1)
+ self.assertEqual(factsrules.pair_host_docs(["arc-2"], set(), ["README.md"]), [])
+
+ def test_container_matching_a_configured_host(self):
+ facts = factsrules.pair_container_host(["arc-2"], ["arc-2", "other"])
+ self.assertEqual(len(facts), 1)
+ self.assertEqual(facts[0].sources, ("docker ps", "ssh config"))
+
+ def test_no_ssh_hosts_means_no_host_facts(self):
+ self.assertEqual(factsrules.pair_container_host(["arc-2"], []), [])
+
+ def test_pair_all_dedupes_by_text(self):
+ facts = factsrules.pair_all(
+ running=["chat"],
+ compose_by_container={"chat": {"/srv/chat/compose.yaml": ["chat"]}},
+ repo_services=["chat"],
+ repo_compose_name="compose.yaml",
+ ssh_hosts=[],
+ doc_terms=set(),
+ )
+ texts = [f.text for f in facts]
+ self.assertEqual(len(texts), len(set(texts)))
+
+ def test_every_produced_fact_carries_at_least_two_sources(self):
+ facts = factsrules.pair_all(
+ running=["chat", "arc-2", "lonely"],
+ compose_by_container={"chat": {"/srv/chat/compose.yaml": ["chat"]}},
+ repo_services=["chat"],
+ repo_compose_name="compose.yaml",
+ ssh_hosts=["arc-2"],
+ doc_terms={"arc-2"},
+ )
+ self.assertTrue(facts)
+ for fact in facts:
+ self.assertGreaterEqual(len(set(fact.sources)), factsrules.MIN_SOURCES, fact.text)
+
+
+if __name__ == "__main__":
+ unittest.main()
From 0a6a0f6a7386c5261e9fe354d3b3c35425140352 Mon Sep 17 00:00:00 2001
From: Jochen Schultz
Date: Thu, 13 Aug 2026 03:54:40 +0200
Subject: [PATCH 11/19] feat(facts): make audit self check invariants instead
of keywords
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`audit self` was three greps over PLAN.md and README.md — and its own
docstring described the `db` mode ("every fact in db has >=2 sources"),
which self never did. The regex `2.source` had an unescaped dot, so it
matched almost anything. It could not go red.
It now checks what it claims to:
- evidence rule: runs factsrules against a fixture with one paired and one
unpaired observation, asserts the single-source one stays out, every fact
carries >=2 distinct sources in the " x " form facts/audit db greps for,
and make_fact still refuses a single source
- tool convention (D14): shebang on line 1 (both `#!` and the Go
`//usr/bin/env go run` form) and a usage line naming the tool
- documented modes: AGENTS.md/PLAN.md must list exactly the modes argparse
accepts, which is now a single MODES constant
- the doc greps stay, with the two-source regex tightened to ">=2 sources"
Against the current tree it reports 5 real problems (bin/md/import has no
usage line; AGENTS.md and PLAN.md both advertise facts|info|stale, which do
not exist). Those are fixed in the next commit, so the gate goes green on
substance rather than by weakening it.
test_facts_audit.py exercises every check against a fixture tree that
violates it and one that satisfies it — a gate that cannot fail is the bug
being fixed here. The evidence rule itself was TDD via test_factsrules.py;
these checker tests were written after the checks.
Co-Authored-By: Claude Opus 5
---
bin/facts/audit | 101 +++++++++++++++++++++++++++--
bin/tools/test_facts_audit.py | 115 ++++++++++++++++++++++++++++++++++
2 files changed, 212 insertions(+), 4 deletions(-)
create mode 100644 bin/tools/test_facts_audit.py
diff --git a/bin/facts/audit b/bin/facts/audit
index a377bfa..78e9636 100755
--- a/bin/facts/audit
+++ b/bin/facts/audit
@@ -15,6 +15,7 @@ Exit 0 = all checks pass, 1 = audit failures, 2 = could not evaluate.
from __future__ import annotations
import json
+import os
import re
import sys
from pathlib import Path
@@ -22,6 +23,8 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "bin" / "tools"))
+MODES = ("self", "db")
+
def audit_db() -> list[str]:
from kblib import connect
@@ -46,23 +49,113 @@ def audit_db() -> list[str]:
return problems
-def audit_self() -> list[str]:
+def check_evidence_rule() -> list[str]:
+ """The gate that matters: run the pairing rule against a fixture where one
+ observation has a second source and one has none. No docker, no network."""
+ import factsrules
+
problems: list[str] = []
- plan = (ROOT / "PLAN.md").read_text()
+ facts = factsrules.pair_all(
+ running=["chat", "lonely"],
+ compose_by_container={
+ "chat": {"/srv/chat/compose.yaml": ["chat"]},
+ "lonely": {},
+ },
+ repo_services=[],
+ repo_compose_name="compose.yaml",
+ ssh_hosts=["arc-2"],
+ doc_terms={"arc-2"},
+ )
+ texts = " | ".join(f.text for f in facts)
+ if "lonely" in texts:
+ problems.append("evidence rule broken: a single-source observation became a fact")
+ if "chat" not in texts:
+ problems.append("evidence rule broken: docker ps x compose no longer pairs")
+ if "arc-2" not in texts:
+ problems.append("evidence rule broken: ssh config x docs no longer pairs")
+ for fact in facts:
+ if len(set(fact.sources)) < factsrules.MIN_SOURCES:
+ problems.append(f"fact with <{factsrules.MIN_SOURCES} sources: {fact.text!r}")
+ if " x " not in fact.source:
+ problems.append(f"fact source is not a pairing: {fact.source!r}")
+ try:
+ factsrules.make_fact("single source", ["docker ps"], "loc")
+ problems.append("make_fact accepted a single source")
+ except ValueError:
+ pass
+ return problems
+
+def check_tool_convention() -> list[str]:
+ """AGENTS D14: bin/{subject}/{method}, shebang on line 1, usage from line 2."""
+ problems: list[str] = []
+ for path in sorted((ROOT / "bin").rglob("*")):
+ if not path.is_file() or not os.access(path, os.X_OK):
+ continue
+ if path.parent.name == "tools" or "tools" in path.relative_to(ROOT / "bin").parts:
+ continue # vendored libraries, not tools
+ rel = path.relative_to(ROOT / "bin")
+ try:
+ head = path.read_text(encoding="utf-8", errors="replace").splitlines()[:5]
+ except OSError as e:
+ problems.append(f"bin/{rel}: unreadable ({e})")
+ continue
+ if not head:
+ problems.append(f"bin/{rel}: empty tool")
+ continue
+ # `#!/usr/bin/env python3` or the Go form `///usr/bin/env go run "$0" "$@"; exit`
+ if not (head[0].startswith("#!") or head[0].startswith("//usr/bin/env")):
+ problems.append(f"bin/{rel}: missing shebang on line 1")
+ ident = str(rel.with_suffix("")) if rel.suffix in (".go",) else str(rel)
+ if not any(ident in line for line in head[1:]):
+ problems.append(f"bin/{rel}: no usage line naming '{ident}' in lines 2-5")
+ return problems
+
+
+def check_documented_modes() -> list[str]:
+ """Docs must name the modes this tool actually has (AGENTS rule 6)."""
+ problems: list[str] = []
+ for name in ("AGENTS.md", "PLAN.md"):
+ text = (ROOT / name).read_text()
+ for line in text.splitlines():
+ if "facts/audit" not in line or '"' not in line:
+ continue
+ documented = set(re.findall(r'"([a-z]+)"', line))
+ if not documented:
+ continue
+ unknown = documented - set(MODES)
+ missing = set(MODES) - documented
+ if unknown:
+ problems.append(f"{name} documents audit modes that do not exist: {sorted(unknown)}")
+ if missing:
+ problems.append(f"{name} omits real audit modes: {sorted(missing)}")
+ return problems
+
+
+def check_docs() -> list[str]:
+ problems: list[str] = []
+ plan = (ROOT / "PLAN.md").read_text()
if "recall@5" not in plan:
problems.append("PLAN.md missing recall@5 gate")
- if re.search(r"(?i)facts must have.*2 sources|2.source", plan) is None:
+ if re.search(r"(?i)(>=|≥)\s*2\s+(independent\s+)?sources", plan) is None:
problems.append("PLAN.md missing the two-source evidence rule for facts")
if re.search(r"(?i)HNSW|BM25|deduction", (ROOT / "README.md").read_text()) is None:
problems.append("README.md missing search/retrieval description")
return problems
+def audit_self() -> list[str]:
+ problems: list[str] = []
+ for check in (check_evidence_rule, check_tool_convention,
+ check_documented_modes, check_docs):
+ problems.extend(check())
+ return problems
+
+
def main(argv: list[str]) -> int:
import argparse
p = argparse.ArgumentParser(description="evidence & lexicon audit")
- p.add_argument("mode", choices=("self", "db"))
+ p.add_argument("mode", choices=MODES)
p.add_argument("--json", action="store_true")
a = p.parse_args(argv)
diff --git a/bin/tools/test_facts_audit.py b/bin/tools/test_facts_audit.py
new file mode 100644
index 0000000..ecafc77
--- /dev/null
+++ b/bin/tools/test_facts_audit.py
@@ -0,0 +1,115 @@
+"""Tests for bin/facts/audit self.
+
+A gate is only worth having if it can go red, so every check here is exercised
+against a fixture tree that violates it *and* one that satisfies it.
+"""
+import importlib.machinery
+import importlib.util
+import os
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+REPO = Path(__file__).resolve().parents[2]
+sys.path.insert(0, str(REPO / "bin" / "tools"))
+
+
+def load_audit():
+ """bin/facts/audit has no .py suffix; load it by path."""
+ loader = importlib.machinery.SourceFileLoader("facts_audit", str(REPO / "bin" / "facts" / "audit"))
+ spec = importlib.util.spec_from_loader(loader.name, loader)
+ module = importlib.util.module_from_spec(spec)
+ loader.exec_module(module)
+ return module
+
+
+audit = load_audit()
+
+
+def write_tool(path: Path, body: str) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(body)
+ path.chmod(0o755)
+
+
+class ToolConventionTest(unittest.TestCase):
+ def setUp(self):
+ tmp = tempfile.TemporaryDirectory()
+ self.addCleanup(tmp.cleanup)
+ self.tmp = Path(tmp.name)
+ self.original_root = audit.ROOT
+ audit.ROOT = self.tmp
+
+ def tearDown(self):
+ audit.ROOT = self.original_root
+
+ def test_tool_without_usage_line_is_flagged(self):
+ write_tool(self.tmp / "bin" / "kb" / "index", "#!/usr/bin/env python3\nimport sys\n")
+ problems = audit.check_tool_convention()
+ self.assertEqual(len(problems), 1)
+ self.assertIn("kb/index", problems[0])
+
+ def test_tool_without_shebang_is_flagged(self):
+ write_tool(self.tmp / "bin" / "kb" / "index", '"""kb/index - build."""\nimport sys\n')
+ self.assertTrue(any("shebang" in p for p in audit.check_tool_convention()))
+
+ def test_well_formed_tool_passes(self):
+ write_tool(self.tmp / "bin" / "kb" / "index",
+ '#!/usr/bin/env python3\n"""kb/index - build the brain."""\n')
+ self.assertEqual(audit.check_tool_convention(), [])
+
+ def test_go_shebang_form_passes(self):
+ write_tool(self.tmp / "bin" / "serve.go",
+ '//usr/bin/env go run "$0" "$@"; exit\n// serve - http server\n')
+ self.assertEqual(audit.check_tool_convention(), [])
+
+ def test_vendored_libs_are_not_tools(self):
+ write_tool(self.tmp / "bin" / "tools" / "kblib.py", "#!/usr/bin/env python3\nimport os\n")
+ self.assertEqual(audit.check_tool_convention(), [])
+
+ def test_non_executable_files_are_ignored(self):
+ path = self.tmp / "bin" / "kb" / "notes.md"
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text("just notes\n")
+ os.chmod(path, 0o644)
+ self.assertEqual(audit.check_tool_convention(), [])
+
+
+class DocumentedModesTest(unittest.TestCase):
+ def setUp(self):
+ tmp = tempfile.TemporaryDirectory()
+ self.addCleanup(tmp.cleanup)
+ self.tmp = Path(tmp.name)
+ self.original_root = audit.ROOT
+ audit.ROOT = self.tmp
+
+ def tearDown(self):
+ audit.ROOT = self.original_root
+
+ def write_docs(self, agents: str, plan: str = "") -> None:
+ (self.tmp / "AGENTS.md").write_text(agents)
+ (self.tmp / "PLAN.md").write_text(plan)
+
+ def test_documented_but_missing_mode_is_flagged(self):
+ self.write_docs('bin/facts/audit ["self"|"db"|"stale"]\n')
+ problems = audit.check_documented_modes()
+ self.assertTrue(any("do not exist" in p and "stale" in p for p in problems))
+
+ def test_real_mode_missing_from_docs_is_flagged(self):
+ self.write_docs('bin/facts/audit ["self"]\n')
+ self.assertTrue(any("omits real audit modes" in p for p in audit.check_documented_modes()))
+
+ def test_matching_docs_pass(self):
+ self.write_docs('bin/facts/audit ["self"|"db"]\n', 'bin/facts/audit ["self"|"db"]\n')
+ self.assertEqual(audit.check_documented_modes(), [])
+
+
+class EvidenceRuleTest(unittest.TestCase):
+ def test_the_shipped_rule_satisfies_the_gate(self):
+ # Runs against the real factsrules, not a fixture.
+ self.assertEqual(audit.check_evidence_rule(), [])
+
+
+if __name__ == "__main__":
+ unittest.main()
From 580261cb047dd2448a096e8d9030f8b820f59269 Mon Sep 17 00:00:00 2001
From: Jochen Schultz
Date: Thu, 13 Aug 2026 03:57:09 +0200
Subject: [PATCH 12/19] docs: correct the audit modes and md/import usage the
new gate found
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fixes the 5 problems `audit self` reported:
- bin/md/import had a shebang and no usage block (D14); added one, including
the exit codes it already returns
- AGENTS.md and PLAN.md advertised `facts|info|stale`; the tool has `self`
and `db`. Both now say so
- docs/design.md described `bin/facts/audit stale` as if it existed; now
phrased as planned and marked not implemented
- README's diagram called audit "confidence + staleness"; it does neither,
it gates the 2-source rule and the tool convention
- the audit docstring described `db` behaviour under `self`, and claimed db
checks source_rev, which it does not — it checks source, loc, how and
confidence
audit self is green again on substance: 0 problems, 67 python tests pass.
Co-Authored-By: Claude Opus 5
---
AGENTS.md | 2 +-
PLAN.md | 4 ++--
README.md | 2 +-
bin/facts/audit | 20 ++++++++++++--------
bin/md/import | 11 +++++++++++
docs/design.md | 5 +++--
6 files changed, 30 insertions(+), 14 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 415aa04..4fbc601 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -71,7 +71,7 @@ bin/mail/index_mail # rebuil
## Tools
```bash
-bin/facts/audit ["self"|"facts"|"info"|"stale"] # 2-source + staleness gate
+bin/facts/audit ["self"|"db"] # repo invariants | evidence over kb.lbug
bin/facts/crm [--dry-run] # proof person↔company/company↔project (ooCRM × corpus SoT)
bin/kb/search "query" [--hop N] [--repo X] # deduction search → YAML
bin/md/tables # what the graph holds → YAML
diff --git a/PLAN.md b/PLAN.md
index 625c7ea..3e85988 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -50,7 +50,7 @@ detective method: **a fact needs ≥2 independent sources or it is
skills/ in-project skills (web-search, db-yaml, kb-search, agent-cost, diataxis-docs, …)
bin/
facts/extract auto-pair 2 sources → lexicon yaml + graph
- facts/audit ["self"|"facts"|"info"|"stale"] 2-source + staleness gate
+ facts/audit ["self"|"db"] repo invariants | evidence over kb.lbug
kb/index build FTS + HNSW from corpus
kb/search deduction: facts → info → web-search; --hop N
kb/get kb/stats kb/eval
@@ -118,7 +118,7 @@ Common props on every node/edge: `root`, `confidence`, `evidence[]`, `how`,
1. go vet + go test ./... (Go tools)
2. python -m unittest discover + pytest (Py tools)
-3. bin/facts/audit self (lexicon internal consistency)
+3. bin/facts/audit self (evidence rule vs fixture, tool convention, doc/mode match)
4. bin/kb/eval (recall@5 ≥ 0.95, gates index regressions)
5. md-docs build/lint if docs tooling arrives.
diff --git a/README.md b/README.md
index 307568c..490ebab 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ graph TB
subgraph dph["2dph tools"]
EX["bin/facts/extract
2-source pairing"]
- AU["bin/facts/audit
confidence + staleness"]
+ AU["bin/facts/audit
2-source + convention gate"]
IDX["bin/kb/index
chunk + embed"]
MD["bin/md/import
mistune leaves"]
SR["bin/kb/search
deduction + --hop"]
diff --git a/bin/facts/audit b/bin/facts/audit
index 78e9636..6f73c5d 100755
--- a/bin/facts/audit
+++ b/bin/facts/audit
@@ -1,16 +1,20 @@
#!/usr/bin/env python3
"""facts/audit - evidence & lexicon checks for the 2dph brain.
- bin/facts/audit self # lexicon: every fact in db has >=2 sources
- bin/facts/audit db # evidence gate: run against var/kb.lbug
+ bin/facts/audit self # repo invariants: no db, no network
+ bin/facts/audit db # evidence gate over var/kb.lbug
-`self` mode checks the repo itself (no network, no runtime deps). It greps
-for known-good two-source pairings and confirms the docs are consistent.
-`db` mode loads every Leaf with root=facts and asserts each has source_rev
-and a non-empty `loc` (the "where did you see it" evidence pointer) and that
-'confirmed' facts carry a two-source `source` field.
+`self` runs the pairing rule (bin/tools/factsrules.py) against a fixture and
+asserts a single-source observation cannot become a fact, that every bin/
+tool follows the shebang + usage convention (D14), that AGENTS.md/PLAN.md
+document exactly the modes this tool has, and that the docs still describe
+the retrieval design.
-Exit 0 = all checks pass, 1 = audit failures, 2 = could not evaluate.
+`db` loads every Leaf with root=facts and asserts each carries a two-source
+`source` (the " x " pairing), a non-empty `loc` (the "where did you see it"
+pointer), a `how`, and confidence='confirmed'.
+
+Exit 0 = all checks pass, 1 = audit failures.
"""
from __future__ import annotations
diff --git a/bin/md/import b/bin/md/import
index 85328a1..5dcf96c 100755
--- a/bin/md/import
+++ b/bin/md/import
@@ -1,4 +1,15 @@
#!/usr/bin/env python3
+"""md/import - split a markdown corpus into leafs (mistune).
+
+ bin/md/import [DIR] # walk DIR (default .) for *.md
+ bin/md/import --files a.md,b.md # explicit file list
+ bin/md/import --json # JSON instead of YAML
+
+Prints the leafs; it does not write to the brain. bin/kb/index calls the same
+mdleaves functions to build var/kb.lbug.
+
+Exit 0 = leafs printed, 1 = no markdown found, 2 = path does not exist.
+"""
from __future__ import annotations
import sys
diff --git a/docs/design.md b/docs/design.md
index 198022d..c277d42 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -48,8 +48,9 @@ file changed on disk (git HEAD/mtime) after its last observed `source_rev`.
`File-[:HAS_VERSION]->Commit-[:AUTHORED]->Person` records the history of every
content leaf.
-`bin/facts/audit stale` flags leafs whose observed revision is behind the
-corpus HEAD.
+A planned `stale` mode will flag leafs whose observed revision is behind the
+corpus HEAD. Not implemented — `bin/facts/audit` has `self` and `db` today
+(tracked in PLAN.md, open questions).
## Sources (auto-pairing)
From 33d8a3172335cc285d19a3de2ac5a4e97384eb63 Mon Sep 17 00:00:00 2001
From: Jochen Schultz
Date: Thu, 13 Aug 2026 04:00:50 +0200
Subject: [PATCH 13/19] feat(facts): check mode claims in README and docs, not
just AGENTS/PLAN
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The mode check only looked at AGENTS.md and PLAN.md, which is why
docs/design.md could advertise `bin/facts/audit stale` unnoticed for as long
as it did.
It now scans README.md and docs/*.md too, and only inside code spans and
fenced blocks — prose like "run bin/facts/audit before pushing" would
otherwise be read as a mode named 'before'. Two shapes are checked: a
bracket list must be complete, a bare invocation must name a real mode.
Verified by appending `bin/facts/audit stale` to docs/design.md: audit self
goes red with exactly that finding, green again once reverted.
Co-Authored-By: Claude Opus 5
---
README.md | 2 +-
bin/facts/audit | 47 ++++++++++++++++++++++++-----------
bin/tools/test_facts_audit.py | 35 +++++++++++++++++++++-----
3 files changed, 63 insertions(+), 21 deletions(-)
diff --git a/README.md b/README.md
index 490ebab..f7a9f1c 100644
--- a/README.md
+++ b/README.md
@@ -124,7 +124,7 @@ touches network/db is read-only, throttled, cached. Tests gate every commit.
```bash
uv venv .venv # Python 3.12, uv-managed
uv pip install -r requirements.lock.txt # pinned toolchain
-bin/facts/audit self # lexicon consistency gate
+bin/facts/audit self # evidence rule + tool convention gate
go test ./... && python -m unittest discover -s bin/tools -t .
```
diff --git a/bin/facts/audit b/bin/facts/audit
index 6f73c5d..054cd80 100755
--- a/bin/facts/audit
+++ b/bin/facts/audit
@@ -116,23 +116,42 @@ def check_tool_convention() -> list[str]:
return problems
+def code_snippets(text: str) -> list[str]:
+ """Inline code spans and fenced block lines.
+
+ Mode claims are only checked inside code, so prose like "run
+ bin/facts/audit before pushing" cannot be misread as a mode named
+ 'before'.
+ """
+ spans = re.findall(r"`([^`\n]+)`", text)
+ for block in re.findall(r"```[a-z]*\n(.*?)```", text, re.S):
+ spans.extend(block.splitlines())
+ return spans
+
+
def check_documented_modes() -> list[str]:
"""Docs must name the modes this tool actually has (AGENTS rule 6)."""
problems: list[str] = []
- for name in ("AGENTS.md", "PLAN.md"):
- text = (ROOT / name).read_text()
- for line in text.splitlines():
- if "facts/audit" not in line or '"' not in line:
- continue
- documented = set(re.findall(r'"([a-z]+)"', line))
- if not documented:
- continue
- unknown = documented - set(MODES)
- missing = set(MODES) - documented
- if unknown:
- problems.append(f"{name} documents audit modes that do not exist: {sorted(unknown)}")
- if missing:
- problems.append(f"{name} omits real audit modes: {sorted(missing)}")
+ files = [ROOT / "AGENTS.md", ROOT / "PLAN.md", ROOT / "README.md"]
+ files += sorted((ROOT / "docs").glob("*.md"))
+ for path in files:
+ if not path.exists():
+ continue
+ name = path.relative_to(ROOT)
+ for snippet in code_snippets(path.read_text()):
+ # `bin/facts/audit ["self"|"db"]` - a full list must be complete
+ for listed in re.findall(r"facts/audit\s+\[([^\]]+)\]", snippet):
+ documented = set(re.findall(r"[a-z]+", listed))
+ unknown = documented - set(MODES)
+ missing = set(MODES) - documented
+ if unknown:
+ problems.append(f"{name} documents audit modes that do not exist: {sorted(unknown)}")
+ if missing:
+ problems.append(f"{name} omits real audit modes: {sorted(missing)}")
+ # `bin/facts/audit stale` - a single invocation must name a real mode
+ for mode in re.findall(r'facts/audit\s+"?([a-z][a-z-]*)"?', snippet):
+ if mode not in MODES:
+ problems.append(f"{name} invokes a mode that does not exist: facts/audit {mode}")
return problems
diff --git a/bin/tools/test_facts_audit.py b/bin/tools/test_facts_audit.py
index ecafc77..f004969 100644
--- a/bin/tools/test_facts_audit.py
+++ b/bin/tools/test_facts_audit.py
@@ -27,6 +27,11 @@ def load_audit():
audit = load_audit()
+def fence(body: str) -> str:
+ """Mode claims only count inside code, so fixtures write fenced blocks."""
+ return f"```bash\n{body}\n```\n" if body else ""
+
+
def write_tool(path: Path, body: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(body)
@@ -87,21 +92,39 @@ def setUp(self):
def tearDown(self):
audit.ROOT = self.original_root
- def write_docs(self, agents: str, plan: str = "") -> None:
- (self.tmp / "AGENTS.md").write_text(agents)
- (self.tmp / "PLAN.md").write_text(plan)
+ def write_docs(self, agents: str, plan: str = "", readme: str = "", design: str = "") -> None:
+ (self.tmp / "AGENTS.md").write_text(fence(agents))
+ (self.tmp / "PLAN.md").write_text(fence(plan))
+ (self.tmp / "README.md").write_text(fence(readme))
+ (self.tmp / "docs").mkdir(exist_ok=True)
+ (self.tmp / "docs" / "design.md").write_text(fence(design))
def test_documented_but_missing_mode_is_flagged(self):
- self.write_docs('bin/facts/audit ["self"|"db"|"stale"]\n')
+ self.write_docs('bin/facts/audit ["self"|"db"|"stale"]')
problems = audit.check_documented_modes()
self.assertTrue(any("do not exist" in p and "stale" in p for p in problems))
def test_real_mode_missing_from_docs_is_flagged(self):
- self.write_docs('bin/facts/audit ["self"]\n')
+ self.write_docs('bin/facts/audit ["self"]')
self.assertTrue(any("omits real audit modes" in p for p in audit.check_documented_modes()))
def test_matching_docs_pass(self):
- self.write_docs('bin/facts/audit ["self"|"db"]\n', 'bin/facts/audit ["self"|"db"]\n')
+ self.write_docs('bin/facts/audit ["self"|"db"]', 'bin/facts/audit ["self"|"db"]')
+ self.assertEqual(audit.check_documented_modes(), [])
+
+ def test_invocation_of_a_missing_mode_in_any_doc_is_flagged(self):
+ self.write_docs("", "", "", "bin/facts/audit stale")
+ problems = audit.check_documented_modes()
+ self.assertTrue(any("docs/design.md" in p and "stale" in p for p in problems), problems)
+
+ def test_invocation_of_a_real_mode_passes(self):
+ self.write_docs("", "", "bin/facts/audit self", "bin/facts/audit db")
+ self.assertEqual(audit.check_documented_modes(), [])
+
+ def test_prose_mentioning_the_tool_is_not_a_mode_claim(self):
+ (self.tmp / "AGENTS.md").write_text("Run bin/facts/audit before pushing.\n")
+ (self.tmp / "PLAN.md").write_text("")
+ (self.tmp / "README.md").write_text("")
self.assertEqual(audit.check_documented_modes(), [])
From 339d27357f5e1a21e01b5a114b12f01bad3477cd Mon Sep 17 00:00:00 2001
From: Jochen Schultz
Date: Thu, 13 Aug 2026 04:15:57 +0200
Subject: [PATCH 14/19] ci: make the audit and recall gates fail closed
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Both gates were `./tool 2>/dev/null || echo "not yet implemented; gate
skipped"`, so any failure was swallowed and the traceback hidden.
kb/eval had in fact never run in CI: the step invoked ./bin/kb/eval with the
system python instead of `uv run`, so it died on `import ladybug` and the ||
branch reported it as "not yet implemented". Even with uv it would have
failed — nothing builds an index in CI and var/ is gitignored.
- both steps now run through `uv run` with no `||` and no `2>/dev/null`
- a `bin/kb/index --rebuild` step builds the repo corpus first, with the HF
model restored from actions/cache (~1GB on a cold cache)
- the control set drops `("eslider devops engineer", "DevOps")`: "devops"
appears nowhere in the repo corpus, that question needs the portfolio
corpus, and it alone kept recall at 0.667 — the gate could not have passed.
Six questions that the repo corpus does answer replace it
- hit_texts_of no longer swallows exceptions; a broken FTS index must fail
loudly instead of masquerading as recall 0.0
- PLAN.md records OQ5: the Go search path is still ungated because
bin/kbsearch needs the native ladybug library, and states that gates are
fail-closed by policy
Verified locally end to end after `rm -rf var`: audit self ok, index 47/47
leafs, recall@5 1.0, 70 python tests, go vet + go test green.
Co-Authored-By: Claude Opus 5
---
.github/workflows/ci.yml | 19 +++++++++++++------
PLAN.md | 16 +++++++++++++---
bin/kb/eval | 25 ++++++++++++++++++-------
3 files changed, 44 insertions(+), 16 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1cca26e..2ad7575 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -42,13 +42,20 @@ jobs:
go vet ./...
go test ./... -count=1
- - name: facts/audit self (lexicon consistency, no network)
- run: |
- ./bin/facts/audit self 2>/dev/null || echo "audit: not yet implemented; gate skipped"
+ - name: facts/audit self (repo invariants, no network)
+ run: uv run bin/facts/audit self
- - name: kb/eval recall gate
- run: |
- ./bin/kb/eval 2>/dev/null || echo "eval: not yet implemented; gate skipped"
+ - name: Cache the embedding model
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/huggingface
+ key: hf-potion-multilingual-128M
+
+ - name: Build the brain index (repo corpus)
+ run: uv run bin/kb/index --rebuild
+
+ - name: kb/eval recall gate (recall@5 >= 0.95)
+ run: uv run bin/kb/eval
release:
name: Release (semver)
diff --git a/PLAN.md b/PLAN.md
index 3e85988..a53d910 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -98,6 +98,12 @@ Common props on every node/edge: `root`, `confidence`, `evidence[]`, `how`,
- OQ3: optional duckdb-md layer for `SELECT … FORMAT MARKDOWN` export/write-back.
- OQ4: YAML-first storage for leafs — deferred: JSON is ~10x faster to
serialize and unambiguous; YAML only where humans edit files.
+- OQ5: gate the Go search path in CI. `bin/kbsearch` is a nested module that
+ needs the native ladybug library (`lib-ladybug/`, gitignored), so neither
+ `go test ./...` nor `bin/kb/eval` covers it; `kb/eval` measures the python
+ BM25 path in kblib. Ranking logic is unit-tested in
+ bin/kbsearch/rank_test.go, but that file only compiles where the library
+ is present. Needs a CI step that fetches/builds ladybug.
## Mail pipeline (done)
@@ -116,11 +122,15 @@ Common props on every node/edge: `root`, `confidence`, `evidence[]`, `how`,
`.github/workflows/ci.yml`:
-1. go vet + go test ./... (Go tools)
+1. go vet + go test ./... (Go tools; excludes the nested kbsearch module, OQ5)
2. python -m unittest discover + pytest (Py tools)
3. bin/facts/audit self (evidence rule vs fixture, tool convention, doc/mode match)
-4. bin/kb/eval (recall@5 ≥ 0.95, gates index regressions)
-5. md-docs build/lint if docs tooling arrives.
+4. bin/kb/index --rebuild (repo corpus; HF model from actions/cache)
+5. bin/kb/eval (recall@5 ≥ 0.95, gates index regressions)
+6. md-docs build/lint if docs tooling arrives.
+
+Every gate is fail-closed: no step swallows its exit code. A gate that cannot
+be run is removed or tracked as an open question, never faked with `|| true`.
Feedback loop: every commit → PR → CI → green/gate → merge. Same discipline as
`db/tech-poc`: contract first where there is an OpenAPI/message shape.
diff --git a/bin/kb/eval b/bin/kb/eval
index 828f75c..01dd9e5 100755
--- a/bin/kb/eval
+++ b/bin/kb/eval
@@ -4,7 +4,17 @@
bin/kb/eval [--json]
Control questions are answered from the graph; recall@5 >= 0.95 gates CI.
-Each question maps to leaf ids that MUST appear in the top 5 hits.
+Each question maps to a text fragment that MUST appear in the top 5 hits.
+
+Scope: the questions are answerable from the *repo* corpus (README, PLAN,
+AGENTS, docs, skills) so `bin/kb/index` in CI is enough to run the gate.
+Questions about the ops/portfolio/mail corpora belong in a separate set that
+only runs where those corpora are indexed.
+
+This gates the python retrieval path (kblib BM25). The shipped CLI
+`bin/kb/search` execs the Go binary, whose ranking is covered by
+bin/kbsearch/rank_test.go — building it needs the native ladybug library,
+which CI does not have yet (tracked in PLAN.md).
"""
from __future__ import annotations
@@ -23,17 +33,18 @@ RECALL_THRESHOLD = 0.95
# (query, expected text fragment that must be in the top-5 results)
CONTROL_QUESTIONS: list[tuple[str, str]] = [
("hybrid search fts and vector", "BM25"),
- ("eslider devops engineer", "DevOps"),
("ladybugdb graph engine storage", "LadybugDB"),
+ ("two independent sources evidence rule", "2 independent sources"),
+ ("deduction search facts before info", "deduction"),
+ ("embedding model for the brain", "model2vec"),
+ ("mail pipeline gmail attachments", "attachment"),
]
def hit_texts_of(query: str, conn, limit: int = 5) -> list[str]:
- try:
- hits = query_fts(conn, query, limit)
- return [h["text"] for h in hits]
- except Exception:
- return []
+ # Deliberately unguarded: a broken FTS index must fail the gate loudly,
+ # not disguise itself as recall 0.0.
+ return [h["text"] for h in query_fts(conn, query, limit)]
def main(argv: list[str]) -> int:
From bb9c7030ea822222ade906ba9b236f1140868e0b Mon Sep 17 00:00:00 2001
From: Jochen Schultz
Date: Thu, 13 Aug 2026 04:39:20 +0200
Subject: [PATCH 15/19] feat(facts): make independence a checked property, not
a string convention
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`" x " in source` accepted "bullshit x bullshit2", and counting distinct
strings accepted two compose files as two sources. Two compose files are two
paths but one method, one kind of claim, often one author — corroboration,
not independence.
A Source is now structured: kind (from a fixed taxonomy), method, locator,
origin. Two sources are independent only when kind *and* origin differ, and
every source needs a locator, because evidence you cannot go back and look
at is not evidence. check_independence() returns the reasons; make_fact()
refuses and says which rule broke.
Follow-on effects:
- facts/extract records *where* a doc mentions a host ('README.md:89'), so
loc leads back to the evidence instead of asserting it exists
- repo compose pairings are keyed by file, so the locator names the compose
that actually declares the service
- audit self now feeds five bad pairings through make_fact and fails if any
is accepted — a gate that only accepts is not a gate
- Leaf.source keeps the " x " rendering for output, but it is derived from
the sources and nothing verifies against it any more
Next commit persists the sources as Evidence nodes; until then audit db
still reads the flattened string.
Co-Authored-By: Claude Opus 5
---
bin/facts/audit | 50 ++++++---
bin/facts/extract | 38 +++++--
bin/tools/factsrules.py | 212 +++++++++++++++++++++++++----------
bin/tools/test_factsrules.py | 114 ++++++++++++++-----
4 files changed, 301 insertions(+), 113 deletions(-)
diff --git a/bin/facts/audit b/bin/facts/audit
index 054cd80..8ffc8e5 100755
--- a/bin/facts/audit
+++ b/bin/facts/audit
@@ -57,6 +57,7 @@ def check_evidence_rule() -> list[str]:
"""The gate that matters: run the pairing rule against a fixture where one
observation has a second source and one has none. No docker, no network."""
import factsrules
+ from factsrules import Source
problems: list[str] = []
facts = factsrules.pair_all(
@@ -65,10 +66,9 @@ def check_evidence_rule() -> list[str]:
"chat": {"/srv/chat/compose.yaml": ["chat"]},
"lonely": {},
},
- repo_services=[],
- repo_compose_name="compose.yaml",
+ repo_services_by_file={},
ssh_hosts=["arc-2"],
- doc_terms={"arc-2"},
+ doc_hits={"arc-2": ["AGENTS.md:1"]},
)
texts = " | ".join(f.text for f in facts)
if "lonely" in texts:
@@ -77,16 +77,40 @@ def check_evidence_rule() -> list[str]:
problems.append("evidence rule broken: docker ps x compose no longer pairs")
if "arc-2" not in texts:
problems.append("evidence rule broken: ssh config x docs no longer pairs")
- for fact in facts:
- if len(set(fact.sources)) < factsrules.MIN_SOURCES:
- problems.append(f"fact with <{factsrules.MIN_SOURCES} sources: {fact.text!r}")
- if " x " not in fact.source:
- problems.append(f"fact source is not a pairing: {fact.source!r}")
- try:
- factsrules.make_fact("single source", ["docker ps"], "loc")
- problems.append("make_fact accepted a single source")
- except ValueError:
- pass
+ for fact in factsrules.pair_all(
+ running=["chat"], compose_by_container={"chat": {"/srv/chat/compose.yaml": ["chat"]}},
+ repo_services_by_file={}, ssh_hosts=[], doc_hits={},
+ ):
+ for reason in factsrules.check_independence(list(fact.sources)):
+ problems.append(f"{fact.text!r}: {reason}")
+
+ # Each of these must be refused; a gate that only accepts is not a gate.
+ runtime = Source("runtime", "docker ps", "docker ps:chat", "docker-daemon")
+ refusals = {
+ "a single source": [runtime],
+ "two sources of the same kind": [
+ Source("declared", "compose", "/a.yaml:chat", "file:/a.yaml"),
+ Source("declared", "compose", "/b.yaml:chat", "file:/b.yaml"),
+ ],
+ "two sources from one origin": [
+ runtime,
+ Source("declared", "compose", "docker ps:chat", "docker-daemon"),
+ ],
+ "a source without a locator": [
+ runtime,
+ Source("declared", "compose", "", "file:/a.yaml"),
+ ],
+ "a source outside the kind taxonomy": [
+ runtime,
+ Source("vibes", "gut feeling", "nowhere", "me"),
+ ],
+ }
+ for label, sources in refusals.items():
+ try:
+ factsrules.make_fact("test assertion", sources)
+ problems.append(f"make_fact accepted {label}")
+ except ValueError:
+ pass
return problems
diff --git a/bin/facts/extract b/bin/facts/extract
index 7f2d6a2..8d30d04 100755
--- a/bin/facts/extract
+++ b/bin/facts/extract
@@ -88,16 +88,29 @@ def read_ssh_hosts(path: Path) -> list[str]:
return hosts
-def mentions(term: str, files: list[Path]) -> bool:
+def mention_locations(term: str, files: list[Path]) -> list[str]:
+ """Where a term is named in the docs: ['README.md:12', ...].
+
+ The line number is the point: `loc` has to lead back to the evidence, so
+ the claim can be re-checked instead of taken on trust.
+ """
+ hits: list[str] = []
+ pattern = re.compile(rf"\b{re.escape(term)}\b", re.I)
for path in files:
if not path.exists():
continue
try:
- if re.search(rf"\b{re.escape(term)}\b", path.read_text(), re.I):
- return True
+ for lineno, line in enumerate(path.read_text().splitlines(), 1):
+ if pattern.search(line):
+ try:
+ rel = path.relative_to(ROOT)
+ except ValueError:
+ rel = path
+ hits.append(f"{rel}:{lineno}")
+ break
except OSError:
continue
- return False
+ return hits
def build_facts() -> list[dict]:
@@ -116,23 +129,24 @@ def build_facts() -> list[dict]:
for cfile in (compose_files_in(cdir) if cdir else [])
}
- repo_services: list[str] = []
+ repo_services_by_file: dict[str, list[str]] = {}
for c in COMPOSE_FILES:
- repo_services.extend(read_compose_services(Path(c)))
+ services = read_compose_services(Path(c))
+ if services:
+ repo_services_by_file[str(c)] = services
hosts = read_ssh_hosts(SSH_CONFIG)
- doc_terms = {h for h in hosts if mentions(h, doc_files)}
+ doc_hits = {h: mention_locations(h, doc_files) for h in hosts}
+ doc_hits = {h: v for h, v in doc_hits.items() if v}
facts = factsrules.pair_all(
running=running,
compose_by_container=compose_by_container,
- repo_services=repo_services,
- repo_compose_name=Path(COMPOSE_FILES[0]).name,
+ repo_services_by_file=repo_services_by_file,
ssh_hosts=hosts,
- doc_terms=doc_terms,
- doc_markers=DOC_MARKERS,
+ doc_hits=doc_hits,
)
- paired = sum(1 for f in facts if any(s.startswith("compose:") for s in f.sources))
+ paired = sum(1 for f in facts if any(s.kind == "declared" for s in f.sources))
if paired:
print(f"facts/extract: paired {paired}/{len(running)} running containers to compose",
file=sys.stderr)
diff --git a/bin/tools/factsrules.py b/bin/tools/factsrules.py
index 68c057b..97d8179 100644
--- a/bin/tools/factsrules.py
+++ b/bin/tools/factsrules.py
@@ -1,55 +1,128 @@
"""factsrules - the 2-source pairing rule behind bin/facts/extract.
Pure functions: no subprocess, no filesystem, no database. bin/facts/extract
-gathers the observations (docker ps, compose services, ssh config, doc
-mentions) and this module decides which pairings are strong enough to become a
-`facts` leaf. bin/facts/audit re-checks the rule against a fixture, so the
-gate fails when someone loosens it.
+gathers the observations, this module decides which pairings are strong enough
+to become a `facts` leaf, and bin/facts/audit re-checks the rule.
The rule (AGENTS.md D8): an assertion needs >=2 *independent* sources or it is
-`(not confirmed)`. make_fact() refuses to build a Fact from fewer, so a single
-observation cannot reach the facts root by accident.
+`(not confirmed)`. Independence is the hard part, and counting strings does not
+establish it: two compose files are two paths but one method, one kind of
+claim, and often one author. So a Source is structured, and two sources count
+as independent only when they differ in **kind** (what sort of observation)
+*and* in **origin** (which system of record produced it).
+
+Each Source also carries a `locator` — evidence you cannot go back and look at
+is not evidence. make_fact() refuses anything that fails these rules, so a
+weak pairing cannot reach the facts root by accident.
"""
from __future__ import annotations
+import hashlib
from dataclasses import dataclass
from pathlib import PurePath
MIN_SOURCES = 2
HOW = "facts/extract"
+# Kinds of observation. Two sources of the same kind are the same sort of
+# claim, however many files they span.
+KINDS = {
+ "runtime": "observed running state (docker ps, systemctl, a live query)",
+ "declared": "declared configuration (compose, manifests, IaC)",
+ "netconfig": "network/host configuration (ssh config, DNS, firewall)",
+ "doc": "prose written by a human (markdown, README, notes)",
+ "vcs": "version control history (commits, authors, tags)",
+ "external": "a system outside this machine and repo (CRM, API, web)",
+}
+
+
+@dataclass(frozen=True)
+class Source:
+ """One observation backing an assertion.
+
+ kind - taxonomy entry from KINDS
+ method - how it was observed, human readable ('docker ps', 'compose:a.yaml')
+ locator - where to look again ('README.md:12', 'docker ps:chat')
+ origin - the system of record it came from ('docker-daemon', 'file:/x.yaml')
+ """
+
+ kind: str
+ method: str
+ locator: str
+ origin: str
+
+ @property
+ def id(self) -> str:
+ raw = f"{self.kind}\0{self.origin}\0{self.locator}"
+ return hashlib.sha256(raw.encode()).hexdigest()[:24]
+
+ def as_dict(self) -> dict:
+ return {"id": self.id, "kind": self.kind, "method": self.method,
+ "locator": self.locator, "origin": self.origin}
+
@dataclass(frozen=True)
class Fact:
- """One confirmed assertion plus the sources it was paired from."""
+ """One confirmed assertion plus the independent sources behind it."""
text: str
- sources: tuple[str, ...]
- loc: str
+ sources: tuple[Source, ...]
how: str = HOW
@property
def source(self) -> str:
- """Evidence string as stored on the leaf; facts/audit db greps ' x '."""
- return " x ".join(self.sources)
+ """Human-readable rendering stored on the leaf. Derived, never authored
+ by hand, and never what the audit trusts."""
+ return " x ".join(s.method for s in self.sources)
- def as_dict(self) -> dict:
- return {"text": self.text, "source": self.source, "loc": self.loc, "how": self.how}
+ @property
+ def loc(self) -> str:
+ return "; ".join(s.locator for s in self.sources)
+ def evidence(self) -> list[dict]:
+ return [s.as_dict() for s in self.sources]
-def make_fact(text: str, sources: list[str], loc: str, how: str = HOW) -> Fact:
- """Build a Fact or refuse. Independent means distinct: the same observation
- named twice is one source, not two."""
- distinct = {s for s in sources if s}
- if len(distinct) < MIN_SOURCES:
- raise ValueError(
- f"fact needs >={MIN_SOURCES} independent sources, got {sorted(distinct)}: {text!r}"
+ def as_dict(self) -> dict:
+ return {"text": self.text, "source": self.source, "loc": self.loc,
+ "how": self.how, "evidence": self.evidence()}
+
+
+def check_independence(sources: list[Source]) -> list[str]:
+ """Why this set of sources does not establish a fact. Empty = it does."""
+ problems: list[str] = []
+ for src in sources:
+ if src.kind not in KINDS:
+ problems.append(f"unknown source kind {src.kind!r} (known: {sorted(KINDS)})")
+ if not src.locator:
+ problems.append(f"source {src.method!r} has no locator to re-check")
+ if not src.origin:
+ problems.append(f"source {src.method!r} has no origin")
+ if len(sources) < MIN_SOURCES:
+ problems.append(f"needs >={MIN_SOURCES} sources, got {len(sources)}")
+ return problems
+ if len({s.kind for s in sources}) < MIN_SOURCES:
+ problems.append(
+ f"sources share one kind ({sorted({s.kind for s in sources})}); "
+ "same kind of observation is corroboration, not independence"
)
- return Fact(text=text, sources=tuple(sources), loc=loc, how=how)
+ if len({s.origin for s in sources}) < MIN_SOURCES:
+ problems.append(
+ f"sources share one origin ({sorted({s.origin for s in sources})}); "
+ "one system of record cannot confirm itself"
+ )
+ return problems
+
+
+def make_fact(text: str, sources: list[Source], how: str = HOW) -> Fact:
+ """Build a Fact or refuse, with the reason."""
+ problems = check_independence(sources)
+ if problems:
+ raise ValueError(f"{text!r}: " + "; ".join(problems))
+ return Fact(text=text, sources=tuple(sources), how=how)
def pair_container_compose(compose_by_container: dict[str, dict[str, list[str]]]) -> list[Fact]:
- """S1 runtime (docker ps) x S2 declared (the container's *own* compose file).
+ """runtime (docker ps) x declared (the container's *own* compose file).
Takes {container: {compose_path: [services]}} — candidates are scoped per
container, because a service name like `db` occurs in many unrelated
@@ -62,69 +135,84 @@ def pair_container_compose(compose_by_container: dict[str, dict[str, list[str]]]
if name in services:
fname = PurePath(path).name
facts.append(make_fact(
- text=f"container '{name}' is running and declared in {fname}",
- sources=["docker ps", f"compose:{fname}"],
- loc=f"{path}:{name}",
+ f"container '{name}' is running and declared in {fname}",
+ [
+ Source("runtime", "docker ps", f"docker ps:{name}", "docker-daemon"),
+ Source("declared", f"compose:{fname}", f"{path}:{name}", f"file:{path}"),
+ ],
))
break
return facts
-def pair_container_repo_compose(running: list[str], repo_services: list[str],
- compose_name: str) -> list[Fact]:
- """S1 runtime x S2 the repo's own compose file."""
- overlap = sorted(set(repo_services) & set(running))
- return [
- make_fact(
- text=f"container '{name}' is running and declared in compose",
- sources=["docker ps", compose_name],
- loc="docker ps; docker compose config",
- )
- for name in overlap
- ]
+def pair_container_repo_compose(running: list[str],
+ repo_services_by_file: dict[str, list[str]]) -> list[Fact]:
+ """runtime x declared, against this repo's own compose file(s).
+ Keyed by file so the locator names the compose that actually declares the
+ service, not just the first one on the list.
+ """
+ facts: list[Fact] = []
+ for path, services in repo_services_by_file.items():
+ for name in sorted(set(services) & set(running)):
+ facts.append(make_fact(
+ f"container '{name}' is running and declared in compose",
+ [
+ Source("runtime", "docker ps", f"docker ps:{name}", "docker-daemon"),
+ Source("declared", PurePath(path).name, f"{path}:{name}", f"file:{path}"),
+ ],
+ ))
+ return facts
-def pair_host_docs(ssh_hosts: list[str], doc_terms: set[str], doc_markers: list[str]) -> list[Fact]:
- """S1 ~/.ssh/config x S2 a doc in this repo naming the same host."""
- marker = ", ".join(doc_markers)
- return [
- make_fact(
- text=f"host '{host}' is configured in ~/.ssh/config and referenced in this repo",
- sources=["ssh config", f"docs({marker})"],
- loc=f"~/.ssh/config:{host}",
- )
- for host in ssh_hosts if host in doc_terms
- ]
+
+def pair_host_docs(ssh_hosts: list[str], doc_hits: dict[str, list[str]]) -> list[Fact]:
+ """netconfig (~/.ssh/config) x doc (a file in this repo naming the host).
+
+ doc_hits maps host -> ['README.md:12', ...]; the locator is kept so the
+ claim can be looked up again.
+ """
+ facts: list[Fact] = []
+ for host in ssh_hosts:
+ hits = doc_hits.get(host)
+ if not hits:
+ continue
+ doc_locator = hits[0]
+ doc_file = doc_locator.rsplit(":", 1)[0]
+ facts.append(make_fact(
+ f"host '{host}' is configured in ~/.ssh/config and referenced in this repo",
+ [
+ Source("netconfig", "ssh config", f"~/.ssh/config:{host}", "file:~/.ssh/config"),
+ Source("doc", f"docs({doc_file})", doc_locator, f"file:{doc_file}"),
+ ],
+ ))
+ return facts
def pair_container_host(running: list[str], ssh_hosts: list[str]) -> list[Fact]:
- """S1 docker ps x S2 ~/.ssh/config naming the same thing."""
+ """runtime (docker ps) x netconfig (~/.ssh/config) naming the same thing."""
known = set(ssh_hosts)
if not known:
return []
return [
make_fact(
- text=f"container '{name}' is running and matches configured host '{name}'",
- sources=["docker ps", "ssh config"],
- loc=f"docker ps:{name}; ~/.ssh/config:{name}",
+ f"container '{name}' is running and matches configured host '{name}'",
+ [
+ Source("runtime", "docker ps", f"docker ps:{name}", "docker-daemon"),
+ Source("netconfig", "ssh config", f"~/.ssh/config:{name}", "file:~/.ssh/config"),
+ ],
)
for name in running if name in known
]
-DOC_MARKERS = ["README.md", "PLAN.md", "AGENTS.md"]
-
-
def pair_all(*, running: list[str], compose_by_container: dict[str, dict[str, list[str]]],
- repo_services: list[str], repo_compose_name: str,
- ssh_hosts: list[str], doc_terms: set[str],
- doc_markers: list[str] | None = None) -> list[Fact]:
+ repo_services_by_file: dict[str, list[str]],
+ ssh_hosts: list[str], doc_hits: dict[str, list[str]]) -> list[Fact]:
"""Every pairing, deduped by text, order preserved."""
- doc_markers = doc_markers or DOC_MARKERS
facts = pair_container_compose(compose_by_container)
- if repo_services and running:
- facts += pair_container_repo_compose(running, repo_services, repo_compose_name)
- facts += pair_host_docs(ssh_hosts, doc_terms, doc_markers)
+ if repo_services_by_file and running:
+ facts += pair_container_repo_compose(running, repo_services_by_file)
+ facts += pair_host_docs(ssh_hosts, doc_hits)
facts += pair_container_host(running, ssh_hosts)
return dedupe(facts)
diff --git a/bin/tools/test_factsrules.py b/bin/tools/test_factsrules.py
index 5bb7c9b..5324420 100644
--- a/bin/tools/test_factsrules.py
+++ b/bin/tools/test_factsrules.py
@@ -5,26 +5,89 @@
sys.path.insert(0, str(Path(__file__).resolve().parent))
import factsrules # noqa: E402
+from factsrules import Source # noqa: E402
-class MakeFactTest(unittest.TestCase):
+def runtime(name="chat"):
+ return Source(kind="runtime", method="docker ps", locator=f"docker ps:{name}",
+ origin="docker-daemon")
+
+
+def declared(path="/srv/chat/compose.yaml", name="chat"):
+ return Source(kind="declared", method=f"compose:{Path(path).name}",
+ locator=f"{path}:{name}", origin=f"file:{path}")
+
+
+class SourceTest(unittest.TestCase):
+ def test_kind_must_come_from_the_taxonomy(self):
+ with self.assertRaises(ValueError):
+ factsrules.make_fact("x", [runtime(), Source("vibes", "gut feeling", "n/a", "me")], "loc")
+
+ def test_every_taxonomy_kind_is_documented(self):
+ for kind in factsrules.KINDS:
+ self.assertTrue(factsrules.KINDS[kind], f"{kind} has no description")
+
+
+class IndependenceTest(unittest.TestCase):
def test_two_independent_sources_are_accepted(self):
- fact = factsrules.make_fact("x runs", ["docker ps", "compose:a.yaml"], "a.yaml:x")
- self.assertEqual(fact.sources, ("docker ps", "compose:a.yaml"))
+ fact = factsrules.make_fact("chat runs", [runtime(), declared()], "loc")
+ self.assertEqual(len(fact.sources), 2)
def test_single_source_is_rejected(self):
with self.assertRaises(ValueError):
- factsrules.make_fact("x runs", ["docker ps"], "docker ps:x")
+ factsrules.make_fact("chat runs", [runtime()], "loc")
def test_the_same_source_twice_is_not_two_sources(self):
with self.assertRaises(ValueError):
- factsrules.make_fact("x runs", ["docker ps", "docker ps"], "docker ps:x")
+ factsrules.make_fact("chat runs", [runtime(), runtime()], "loc")
+
+ # The point of the whole exercise: two compose files are two strings but
+ # one method and one kind of claim. That is corroboration, not evidence.
+ def test_two_sources_of_the_same_kind_are_not_independent(self):
+ with self.assertRaises(ValueError) as ctx:
+ factsrules.make_fact("chat runs", [
+ declared("/srv/a/compose.yaml"),
+ declared("/srv/b/compose.yaml"),
+ ], "loc")
+ self.assertIn("kind", str(ctx.exception))
+
+ def test_two_sources_from_the_same_origin_are_not_independent(self):
+ # Same file, read two ways: still one system of record.
+ with self.assertRaises(ValueError) as ctx:
+ factsrules.make_fact("chat runs", [
+ Source("declared", "compose", "/srv/a/compose.yaml:chat", "file:/srv/a/compose.yaml"),
+ Source("doc", "readme", "/srv/a/compose.yaml:1", "file:/srv/a/compose.yaml"),
+ ], "loc")
+ self.assertIn("origin", str(ctx.exception))
+
+ def test_locator_is_required(self):
+ # Evidence you cannot go back and look at is not evidence.
+ with self.assertRaises(ValueError):
+ factsrules.make_fact("chat runs", [
+ Source("runtime", "docker ps", "", "docker-daemon"),
+ declared(),
+ ])
+
+class RenderingTest(unittest.TestCase):
def test_source_string_keeps_the_x_separator(self):
- # facts/audit db asserts " x " is present in Leaf.source.
- fact = factsrules.make_fact("x runs", ["docker ps", "ssh config"], "loc")
- self.assertEqual(fact.source, "docker ps x ssh config")
- self.assertEqual(fact.as_dict()["source"], "docker ps x ssh config")
+ fact = factsrules.make_fact("chat runs", [runtime(), declared()], "loc")
+ self.assertEqual(fact.source, "docker ps x compose:compose.yaml")
+
+ def test_loc_defaults_to_the_locators(self):
+ fact = factsrules.make_fact("chat runs", [runtime(), declared()])
+ self.assertIn("docker ps:chat", fact.loc)
+ self.assertIn("/srv/chat/compose.yaml:chat", fact.loc)
+
+ def test_evidence_carries_the_structure_into_the_db(self):
+ fact = factsrules.make_fact("chat runs", [runtime(), declared()])
+ evidence = fact.evidence()
+ self.assertEqual(len(evidence), 2)
+ self.assertEqual({e["kind"] for e in evidence}, {"runtime", "declared"})
+ for item in evidence:
+ self.assertTrue(item["id"])
+ self.assertTrue(item["locator"])
+ self.assertTrue(item["origin"])
class PairingTest(unittest.TestCase):
@@ -33,29 +96,29 @@ def test_container_is_paired_with_its_compose_file(self):
{"chat": {"/srv/chat/compose.yaml": ["chat", "db"]}}
)
self.assertEqual(len(facts), 1)
- self.assertIn("declared in compose.yaml", facts[0].text)
- self.assertEqual(facts[0].sources, ("docker ps", "compose:compose.yaml"))
+ self.assertEqual({s.kind for s in facts[0].sources}, {"runtime", "declared"})
def test_running_container_without_any_second_source_stays_out(self):
facts = factsrules.pair_all(
running=["onlyoffice"],
compose_by_container={"onlyoffice": {"/srv/chat/compose.yaml": ["chat"]}},
- repo_services=[],
- repo_compose_name="compose.yaml",
+ repo_services_by_file={},
ssh_hosts=["arc-2"],
- doc_terms={"arc-2"},
+ doc_hits={"arc-2": ["AGENTS.md:74"]},
)
self.assertNotIn("onlyoffice", " ".join(f.text for f in facts))
- def test_host_needs_a_doc_mention(self):
- paired = factsrules.pair_host_docs(["arc-2"], {"arc-2"}, ["README.md"])
+ def test_host_needs_a_doc_mention_with_a_locator(self):
+ paired = factsrules.pair_host_docs(["arc-2"], {"arc-2": ["README.md:12"]})
self.assertEqual(len(paired), 1)
- self.assertEqual(factsrules.pair_host_docs(["arc-2"], set(), ["README.md"]), [])
+ self.assertEqual({s.kind for s in paired[0].sources}, {"netconfig", "doc"})
+ self.assertIn("README.md:12", paired[0].loc)
+ self.assertEqual(factsrules.pair_host_docs(["arc-2"], {}), [])
def test_container_matching_a_configured_host(self):
facts = factsrules.pair_container_host(["arc-2"], ["arc-2", "other"])
self.assertEqual(len(facts), 1)
- self.assertEqual(facts[0].sources, ("docker ps", "ssh config"))
+ self.assertEqual({s.kind for s in facts[0].sources}, {"runtime", "netconfig"})
def test_no_ssh_hosts_means_no_host_facts(self):
self.assertEqual(factsrules.pair_container_host(["arc-2"], []), [])
@@ -64,26 +127,25 @@ def test_pair_all_dedupes_by_text(self):
facts = factsrules.pair_all(
running=["chat"],
compose_by_container={"chat": {"/srv/chat/compose.yaml": ["chat"]}},
- repo_services=["chat"],
- repo_compose_name="compose.yaml",
+ repo_services_by_file={"compose.yaml": ["chat"]},
ssh_hosts=[],
- doc_terms=set(),
+ doc_hits={},
)
texts = [f.text for f in facts]
self.assertEqual(len(texts), len(set(texts)))
- def test_every_produced_fact_carries_at_least_two_sources(self):
+ def test_every_produced_fact_is_independently_sourced(self):
facts = factsrules.pair_all(
running=["chat", "arc-2", "lonely"],
compose_by_container={"chat": {"/srv/chat/compose.yaml": ["chat"]}},
- repo_services=["chat"],
- repo_compose_name="compose.yaml",
+ repo_services_by_file={"compose.yaml": ["chat"]},
ssh_hosts=["arc-2"],
- doc_terms={"arc-2"},
+ doc_hits={"arc-2": ["AGENTS.md:74"]},
)
self.assertTrue(facts)
for fact in facts:
- self.assertGreaterEqual(len(set(fact.sources)), factsrules.MIN_SOURCES, fact.text)
+ self.assertGreaterEqual(len({s.kind for s in fact.sources}), factsrules.MIN_SOURCES, fact.text)
+ self.assertGreaterEqual(len({s.origin for s in fact.sources}), factsrules.MIN_SOURCES, fact.text)
if __name__ == "__main__":
From dfa00d7dd653a2941faf9e579e4be76a4c926f05 Mon Sep 17 00:00:00 2001
From: Jochen Schultz
Date: Thu, 13 Aug 2026 04:46:46 +0200
Subject: [PATCH 16/19] feat(kb): store evidence as graph nodes and audit
independence over them
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
audit db checked `" x " in source` — a claim about a string, not about
evidence. `"bullshit x bullshit2"` passed it. The string was all there was:
the schema flattened the sources into one free-text field, so nothing better
could be checked.
Evidence is now a node:
(Evidence {id, kind, method, locator, origin, value_hash, observed_at})
-[:SUPPORTS]-> (Leaf)
Evidence is shared, not copied — one observation backing two assertions is
one node with two edges, so "how many independent things did we look at"
stays answerable. kblib.facts_lacking_independence() counts observations,
kinds and origins per facts leaf and is what audit db now reports; the
source string is no longer consulted by any check.
facts/extract and facts/crm both write structured evidence. crm's `loc` used
to be "bin/facts/crm" — the tool answering "where did you see it" with its
own path; it now points at the CRM export key and the yaml org id.
Verified end to end against a real db: extract writes 2 facts, audit db is
clean, then two forged facts are injected and both are caught —
"bullshit x bullshit2" (evidence=0) and the subtle one, two compose files
with 2 evidence nodes and 2 origins but kinds=['declared']. 82 tests pass.
facts/crm could not be run: neither /tmp/opencode/crm/graph.json nor the
knowledge-mesh yaml exists on this machine, so its change is reviewed and
syntax-checked only.
Docs updated (AGENTS, PLAN D8, README, design.md) and PLAN OQ6 records what
is still missing: value_hash is written empty, nothing re-runs a locator, so
`confirmed` still means "was true when observed".
Co-Authored-By: Claude Opus 5
---
AGENTS.md | 7 ++++
PLAN.md | 8 ++++-
README.md | 7 +++-
bin/facts/audit | 24 +++++++++----
bin/facts/crm | 40 ++++++++++++++++------
bin/facts/extract | 3 +-
bin/tools/kblib.py | 76 ++++++++++++++++++++++++++++++++++++++++-
bin/tools/test_kblib.py | 65 +++++++++++++++++++++++++++++++++++
docs/design.md | 31 +++++++++++++----
9 files changed, 234 insertions(+), 27 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 4fbc601..eae389f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -10,6 +10,13 @@ Read first: [PLAN](PLAN.md) → [docs](docs/).
> ≥2 independent sources of evidence, or the finding is `(not confirmed)`.
> Link the lexicon yaml path that backs each claim.
+Independent means **different kind and different origin**, not two strings:
+`runtime | declared | netconfig | doc | vcs | external` × the system of record
+it came from. Two compose files are one kind and prove nothing together. Every
+observation needs a locator (`README.md:89`) so the claim can be re-checked.
+Enforced in `bin/tools/factsrules.py`, re-checked by `bin/facts/audit db` over
+`(Evidence)-[:SUPPORTS]->(Leaf)`.
+
- `facts` root = assertions backed by ≥2 independent sources (docker ps ×
compose × ssh-config × docs).
- `info` root = descriptive/narrative leafs, searchable, never asserted as fact.
diff --git a/PLAN.md b/PLAN.md
index a53d910..f952637 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -31,7 +31,7 @@ detective method: **a fact needs ≥2 independent sources or it is
| D5 | parser | **mistune** for MD → leaf extraction (duckdb-md documented as future optional SQL/export layer, not v1). |
| D6 | graph engine | **LadybugDB** (Kuzu successor, MIT, embedded, native FTS+vector+Cypher). Python binding for `bin/*`; Go shebang for golang tools. |
| D7 | db access | `db-yaml`/`psql-yq`-style, read-only, YAML out. OnlyOffice Postgres via SSH tunnel (`127.0.0.1:5433`). |
-| D8 | evidence | detective method: ≥2 independent sources or `(not confirmed)`. Auto-pair docker ps × compose × ssh-config × docs. |
+| D8 | evidence | detective method: ≥2 independent sources or `(not confirmed)`. Auto-pair docker ps × compose × ssh-config × docs. Independence = different `kind` **and** different `origin`, each with a locator; stored as `(Evidence)-[:SUPPORTS]->(Leaf)`, never as a substring of `Leaf.source`. |
| D9 | facts/goal model | Who / What / How / Where / When + evidence + confidence on every edge. |
| D10 | versioning | everything is a leaf with `sha256 + observed_at + source_rev`; `File-[:HAS_VERSION]->Commit-[:AUTHORED]->Person`. Stale = `source_rev` < git HEAD. |
| D11 | strong/weak | `root` column: `facts` (strong) vs `info` (weak). Answer is `confirmed` only from facts root. |
@@ -98,6 +98,12 @@ Common props on every node/edge: `root`, `confidence`, `evidence[]`, `how`,
- OQ3: optional duckdb-md layer for `SELECT … FORMAT MARKDOWN` export/write-back.
- OQ4: YAML-first storage for leafs — deferred: JSON is ~10x faster to
serialize and unambiguous; YAML only where humans edit files.
+- OQ6: re-verification of evidence. `Evidence.value_hash` exists in the schema
+ but is not filled: nothing re-runs a locator to check the observation still
+ says what it said. Until it does, `confirmed` means "was true when observed",
+ and staleness cannot be detected — which is what the planned `audit stale`
+ mode needs. Wants `audit db --recheck`: re-read each locator, compare the
+ hash, demote facts whose evidence moved to `hypothesis`.
- OQ5: gate the Go search path in CI. `bin/kbsearch` is a nested module that
needs the native ladybug library (`lib-ladybug/`, gitignored), so neither
`go test ./...` nor `bin/kb/eval` covers it; `kb/eval` measures the python
diff --git a/README.md b/README.md
index f7a9f1c..87e26b1 100644
--- a/README.md
+++ b/README.md
@@ -77,9 +77,14 @@ Every assertion is `Who / What / How / Where / When + evidence + confidence`,
mirroring the detective detective skill: **≥2 independent sources confirm a
fact; conflicting sources or a single source → `hypothesis` → `(not confirmed)`.**
+Independence is checked, not assumed: two observations count only if they differ
+in **kind** (runtime / declared / netconfig / doc / vcs / external) *and* in
+**origin** (which system of record produced them), and each must carry a locator
+you can go back and look at. Two compose files are not two sources.
+
| root | meaning | used for answers |
|------|---------|------------------|
-| `facts` | assertions backed by ≥2 sources (`confirmed`) | yes, with evidence links |
+| `facts` | assertions backed by ≥2 independent observations (`confirmed`) | yes, with evidence links |
| `info` | descriptive/narrative leafs (how-tos, notes) | context only, marked `(not confirmed)` |
## Deduction search
diff --git a/bin/facts/audit b/bin/facts/audit
index 8ffc8e5..f8b36c9 100755
--- a/bin/facts/audit
+++ b/bin/facts/audit
@@ -31,23 +31,35 @@ MODES = ("self", "db")
def audit_db() -> list[str]:
- from kblib import connect
- from kblib import VAR
+ from kblib import MIN_EVIDENCE, VAR, connect, facts_lacking_independence
dbpath = VAR / "kb.lbug"
if not dbpath.exists():
return ["no database yet; run bin/kb/index first"]
db, conn = connect(dbpath)
- r = conn.execute("MATCH (l:Leaf {root:'facts'}) RETURN l.id, l.source, l.loc, l.how, l.confidence")
problems: list[str] = []
- for lid, source, loc, how, conf in r.get_all():
+
+ r = conn.execute("MATCH (l:Leaf {root:'facts'}) RETURN l.id, l.loc, l.how, l.confidence")
+ for lid, loc, how, conf in r.get_all():
if conf != "confirmed":
problems.append(f"{lid}: facts require confidence='confirmed', got '{conf}'")
- if not source or " x " not in source:
- problems.append(f"{lid}: needs 2-source evidence in source, got '{source}'")
if not loc:
problems.append(f"{lid}: missing loc (evidence pointer)")
if not how:
problems.append(f"{lid}: missing how")
+
+ # The evidence gate proper. Deliberately not a check on Leaf.source: that
+ # string is a rendering, and "bullshit x bullshit2" satisfies any shape
+ # test you can write about it. Independence is counted over the Evidence
+ # nodes: >=2 observations, >=2 kinds, >=2 origins.
+ for row in facts_lacking_independence(conn):
+ problems.append(
+ f"{row['id']}: not independently sourced "
+ f"(evidence={row['count']}, kinds={row['kinds']}, origins={row['origins']}) "
+ f"- {row['text'][:60]!r}"
+ )
+ if problems:
+ problems.append(f"note: root=facts requires >={MIN_EVIDENCE} independent observations")
+
conn.close()
db.close()
return problems
diff --git a/bin/facts/crm b/bin/facts/crm
index 2a0ddbf..06d46ab 100755
--- a/bin/facts/crm
+++ b/bin/facts/crm
@@ -28,6 +28,7 @@ sys.path.insert(0, str(ROOT / "bin" / "tools"))
from kblib import upsert_leaf, connect, leaf_id # noqa: E402
CORPUS_MESH = Path("/mnt/8TB/projects/eslider/cv/projects/knowledge-mesh-seed.yaml")
+CRM_GRAPH = Path("/tmp/opencode/crm/graph.json")
def corpus_orgs(raw: str) -> dict[str, dict]:
@@ -44,13 +45,15 @@ def main() -> int:
orgs = corpus_orgs(mesh)
# CRM graph (produced by /tmp/opencode/crm/graph.py -> /tmp/opencode/crm/graph.json)
- graph = json.load(open("/tmp/opencode/crm/graph.json"))
+ graph = json.load(open(CRM_GRAPH))
crm_person_company = graph["companies_with_persons"] # company -> [persons]
crm_project_companies = {} # pid -> title, companies
for pid, v in graph["projects_contacts"].items():
crm_project_companies[pid] = {"title": v["title"], "companies": v["companies"]}
- facts: list[str] = []
+ # Each entry keeps the identifiers of *both* sides, so the evidence can
+ # name where it was seen instead of pointing back at this script.
+ facts: list[dict] = []
mismatches: list[str] = []
# ---- person->company proven by CRM + corpus org ---- #
@@ -64,8 +67,12 @@ def main() -> int:
persons = crm_person_company.get(key, []) if key else []
if persons and org:
for p in persons:
- facts.append(f"{p} is associated with {org.get('label')} "
- f"(role: {org.get('kind', '?')}, {org.get('period', '')})")
+ facts.append({
+ "text": f"{p} is associated with {org.get('label')} "
+ f"(role: {org.get('kind', '?')}, {org.get('period', '')})",
+ "crm_key": key,
+ "org": org_name,
+ })
elif org and key and not persons:
mismatches.append(f"corpus org '{org_name}' ({org.get('label')}) has no CRM persons")
elif org and not key:
@@ -82,7 +89,7 @@ def main() -> int:
print(f"# CRM association facts proven (corpus x CRM): {len(facts)}")
for f in facts:
- print(" -", f)
+ print(" -", f["text"])
print(f"# mismatches / one-sided associations: {len(mismatches)}")
for f in mismatches:
print(" !", f)
@@ -103,14 +110,25 @@ def main() -> int:
stats_before = 0
rev = time.strftime("%Y%m%d-%H%M%S")
written = 0
- for f in facts:
- src = f"ooCRM x {CORPUS_MESH.name}"
+ import factsrules
+ for item in facts:
+ # external (the CRM system of record) x doc (the corpus SoT yaml):
+ # different kind, different origin, both with a locator to look at.
+ fact = factsrules.make_fact(item["text"], [
+ factsrules.Source(
+ "external", "ooCRM",
+ f"{CRM_GRAPH}:companies_with_persons:{item['crm_key']}", "oo-crm"),
+ factsrules.Source(
+ "doc", CORPUS_MESH.name,
+ f"{CORPUS_MESH}:orgs:{item['org']}", f"file:{CORPUS_MESH}"),
+ ], how="crm-crosscheck")
lid = upsert_leaf(
conn,
- text=f, root="facts", confidence="confirmed",
- source=src, source_rev=rev,
- how="crm-crosscheck", loc="bin/facts/crm", type_="association",
- embedding=model.encode(f).tolist(),
+ text=fact.text, root="facts", confidence="confirmed",
+ source=fact.source, source_rev=rev,
+ how=fact.how, loc=fact.loc, type_="association",
+ embedding=model.encode(fact.text).tolist(),
+ evidence=fact.evidence(),
)
written += 1
conn.close()
diff --git a/bin/facts/extract b/bin/facts/extract
index 8d30d04..8143f8c 100755
--- a/bin/facts/extract
+++ b/bin/facts/extract
@@ -165,7 +165,8 @@ def write_facts(facts: list[dict]) -> None:
emb = model.encode([f["text"]])[0].astype(float).tolist()
upsert_leaf(conn, text=f["text"], root="facts", confidence="confirmed",
source=f["source"], source_rev=REPO, how=f["how"],
- loc=f["loc"], type_="fact", embedding=emb)
+ loc=f["loc"], type_="fact", embedding=emb,
+ evidence=f["evidence"])
conn.close()
db.close()
diff --git a/bin/tools/kblib.py b/bin/tools/kblib.py
index 4d40dd4..93f3b8d 100644
--- a/bin/tools/kblib.py
+++ b/bin/tools/kblib.py
@@ -21,6 +21,7 @@
ROOT_FACTS = "facts"
ROOT_INFO = "info"
CONF_CONFIRMED = "confirmed"
+MIN_EVIDENCE = 2 # independent observations required for root=facts
def _repo_root() -> Path:
p = Path(__file__).resolve().parent
@@ -69,6 +70,17 @@ def init_schema(conn: ladybug.Connection) -> None:
"CREATE NODE TABLE IF NOT EXISTS File ("
" id STRING, path STRING, repo STRING, mtime STRING, PRIMARY KEY(id))"
)
+ # Evidence is a node, not a substring of Leaf.source: the audit has to be
+ # able to count *kinds* and *origins*, which a flattened string cannot
+ # express. value_hash is reserved for re-verification (PLAN OQ6).
+ conn.execute(
+ "CREATE NODE TABLE IF NOT EXISTS Evidence ("
+ " id STRING, kind STRING, method STRING, locator STRING, "
+ " origin STRING, value_hash STRING, observed_at STRING, PRIMARY KEY(id))"
+ )
+ conn.execute(
+ "CREATE REL TABLE IF NOT EXISTS SUPPORTS (FROM Evidence TO Leaf)"
+ )
conn.execute(
"CREATE REL TABLE IF NOT EXISTS FROM_FILE (FROM Leaf TO File)"
)
@@ -99,7 +111,8 @@ def leaf_id(text: str, source: str) -> str:
def upsert_leaf(conn: ladybug.Connection, *, text: str, root: str, confidence: str,
source: str, source_rev: str, how: str, loc: str, type_: str,
- embedding: list[float] | None) -> str:
+ embedding: list[float] | None,
+ evidence: list[dict] | None = None) -> str:
lid = leaf_id(text, source)
obs = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
conn.execute(
@@ -115,9 +128,70 @@ def upsert_leaf(conn: ladybug.Connection, *, text: str, root: str, confidence: s
"emb": (embedding if embedding else None),
},
)
+ for item in evidence or []:
+ upsert_evidence(conn, item, lid, obs)
return lid
+def upsert_evidence(conn: ladybug.Connection, item: dict, leaf: str, observed_at: str) -> None:
+ """Store one Evidence node and link it to the leaf it supports.
+
+ Evidence is shared: the same observation backing two assertions is one
+ node with two SUPPORTS edges, so 'how many independent things did we
+ actually look at' stays answerable.
+ """
+ conn.execute(
+ "MERGE (e:Evidence {id:$id}) "
+ "SET e.kind=$kind, e.method=$method, e.locator=$locator, "
+ " e.origin=$origin, e.value_hash=$vhash, e.observed_at=$obs",
+ parameters={
+ "id": item["id"], "kind": item["kind"], "method": item.get("method", ""),
+ "locator": item.get("locator", ""), "origin": item.get("origin", ""),
+ "vhash": item.get("value_hash", ""), "obs": observed_at,
+ },
+ )
+ conn.execute(
+ "MATCH (e:Evidence {id:$eid}), (l:Leaf {id:$lid}) MERGE (e)-[:SUPPORTS]->(l)",
+ parameters={"eid": item["id"], "lid": leaf},
+ )
+
+
+def evidence_for(conn: ladybug.Connection, leaf: str) -> list[dict]:
+ r = conn.execute(
+ "MATCH (e:Evidence)-[:SUPPORTS]->(l:Leaf {id:$lid}) "
+ "RETURN e.id, e.kind, e.method, e.locator, e.origin, e.observed_at",
+ parameters={"lid": leaf},
+ )
+ return [
+ {"id": row[0], "kind": row[1], "method": row[2], "locator": row[3],
+ "origin": row[4], "observed_at": row[5]}
+ for row in r.get_all()
+ ]
+
+
+def facts_lacking_independence(conn: ladybug.Connection) -> list[dict]:
+ """Every facts leaf that is not backed by >=2 independent observations.
+
+ Independent = different kind AND different origin. This is the check that
+ `" x " in source` pretended to be: a fact claiming two sources while both
+ are compose files, or claiming them with no Evidence at all, shows up here.
+ """
+ facts = conn.execute(
+ "MATCH (l:Leaf) WHERE l.root=$root RETURN l.id, l.text",
+ parameters={"root": ROOT_FACTS},
+ ).get_all()
+ problems: list[dict] = []
+ for lid, text in facts:
+ found = evidence_for(conn, lid)
+ kinds = sorted({e["kind"] for e in found if e["kind"]})
+ origins = sorted({e["origin"] for e in found if e["origin"]})
+ if len(found) >= MIN_EVIDENCE and len(kinds) >= MIN_EVIDENCE and len(origins) >= MIN_EVIDENCE:
+ continue
+ problems.append({"id": lid, "text": text, "count": len(found),
+ "kinds": kinds, "origins": origins})
+ return problems
+
+
def create_fts_and_vector(conn: ladybug.Connection, force: bool = False) -> None:
if force:
conn.execute("DROP INDEX IF EXISTS Leaf.Leaf_fts")
diff --git a/bin/tools/test_kblib.py b/bin/tools/test_kblib.py
index 6dc65f9..fc1b34a 100644
--- a/bin/tools/test_kblib.py
+++ b/bin/tools/test_kblib.py
@@ -55,6 +55,71 @@ def test_hybrid_ranks_vector_match(self):
self.assertIn("rrf", result[0])
self.assertEqual(result[0]["text"], "the quick brown fox")
+ def add_fact(self, text, evidence):
+ return kblib.upsert_leaf(
+ self.conn, text=text, root="facts", confidence="confirmed",
+ source="rendered", source_rev="r1", how="facts/extract", loc="loc",
+ type_="fact", embedding=make_emb(0.5), evidence=evidence,
+ )
+
+ def test_evidence_is_stored_as_nodes_and_linked_to_the_leaf(self):
+ lid = self.add_fact("chat runs", [
+ {"id": "e1", "kind": "runtime", "method": "docker ps",
+ "locator": "docker ps:chat", "origin": "docker-daemon"},
+ {"id": "e2", "kind": "declared", "method": "compose",
+ "locator": "/a.yaml:chat", "origin": "file:/a.yaml"},
+ ])
+ stored = kblib.evidence_for(self.conn, lid)
+ self.assertEqual({e["kind"] for e in stored}, {"runtime", "declared"})
+ self.assertEqual({e["origin"] for e in stored}, {"docker-daemon", "file:/a.yaml"})
+ self.assertTrue(all(e["locator"] for e in stored))
+
+ def test_evidence_is_shared_between_leafs_not_duplicated(self):
+ shared = {"id": "e1", "kind": "runtime", "method": "docker ps",
+ "locator": "docker ps:chat", "origin": "docker-daemon"}
+ other = {"id": "e2", "kind": "declared", "method": "compose",
+ "locator": "/a.yaml:chat", "origin": "file:/a.yaml"}
+ third = {"id": "e3", "kind": "doc", "method": "readme",
+ "locator": "README.md:1", "origin": "file:README.md"}
+ self.add_fact("chat runs", [shared, other])
+ self.add_fact("chat is documented", [shared, third])
+ total = self.conn.execute("MATCH (e:Evidence) RETURN count(*)").get_all()[0][0]
+ self.assertEqual(total, 3)
+
+ def test_audit_query_flags_a_fact_with_one_kind_of_evidence(self):
+ weak = self.add_fact("two compose files agree", [
+ {"id": "c1", "kind": "declared", "method": "compose",
+ "locator": "/a.yaml:chat", "origin": "file:/a.yaml"},
+ {"id": "c2", "kind": "declared", "method": "compose",
+ "locator": "/b.yaml:chat", "origin": "file:/b.yaml"},
+ ])
+ strong = self.add_fact("chat runs", [
+ {"id": "e1", "kind": "runtime", "method": "docker ps",
+ "locator": "docker ps:chat", "origin": "docker-daemon"},
+ {"id": "e2", "kind": "declared", "method": "compose",
+ "locator": "/a.yaml:chat", "origin": "file:/a.yaml"},
+ ])
+ flagged = {row["id"]: row for row in kblib.facts_lacking_independence(self.conn)}
+ self.assertIn(weak, flagged)
+ self.assertNotIn(strong, flagged)
+ self.assertEqual(flagged[weak]["kinds"], ["declared"])
+
+ def test_audit_query_flags_a_fact_with_no_evidence_at_all(self):
+ bare = kblib.upsert_leaf(
+ self.conn, text="trust me", root="facts", confidence="confirmed",
+ source="bullshit x bullshit2", source_rev="r1", how="h", loc="l",
+ type_="fact", embedding=make_emb(0.5),
+ )
+ flagged = {row["id"] for row in kblib.facts_lacking_independence(self.conn)}
+ self.assertIn(bare, flagged)
+
+ def test_info_leafs_are_not_subject_to_the_evidence_rule(self):
+ kblib.upsert_leaf(self.conn, text="just a note", root="info",
+ confidence="confirmed", source="s", source_rev="r1",
+ how="test", loc="/tmp", type_="reference",
+ embedding=make_emb(0.5))
+ self.assertEqual(kblib.facts_lacking_independence(self.conn), [])
+
def test_stats_counts_roots(self):
kblib.upsert_leaf(self.conn, text="a fact leaf", root="facts",
confidence="confirmed", source="s", source_rev="r1",
diff --git a/docs/design.md b/docs/design.md
index c277d42..18d6886 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -54,9 +54,28 @@ corpus HEAD. Not implemented — `bin/facts/audit` has `self` and `db` today
## Sources (auto-pairing)
-- A: runtime state — `docker ps` (container running), ports actually bound
-- B: declared config — `docker-compose.yml`, `~/.ssh/config`, `homeserver.yaml`
-- C: narrative — READMEs, AGENTS.md, docs
-
-Confirmed = A×B or B×C agreement. Single source = hypothesis + `(not confirmed)`.
-Conflicting pairings (≥2 yes vs ≥2 no) = hypothesis (OQ1 → v2 resolution).
\ No newline at end of file
+Evidence is stored as `Evidence` nodes linked by `(Evidence)-[:SUPPORTS]->(Leaf)`,
+never as a substring of `Leaf.source` — a flattened string cannot be counted, and
+any shape test over it (`" x " in source`) is satisfied by `"bullshit x bullshit2"`.
+
+Each observation carries a **kind**, a **method**, a **locator** (where to look
+again) and an **origin** (the system of record it came from):
+
+| kind | examples |
+|------|----------|
+| `runtime` | `docker ps`, systemctl, a live query |
+| `declared` | compose files, manifests, IaC |
+| `netconfig` | `~/.ssh/config`, DNS, firewall |
+| `doc` | markdown written by a human |
+| `vcs` | commits, authors, tags |
+| `external` | CRM, API, web — outside this machine and repo |
+
+**Confirmed = ≥2 observations differing in both `kind` and `origin`.** Differing
+in only one is not independence: two compose files are two paths but one kind of
+claim, and one file read two ways is still one system of record. A source
+without a locator is refused outright — evidence you cannot go back and look at
+is not evidence. Single source = hypothesis + `(not confirmed)`. Conflicting
+pairings (≥2 yes vs ≥2 no) = hypothesis (OQ1 → v2 resolution).
+
+`bin/tools/factsrules.py` enforces this at write time (`make_fact`),
+`bin/facts/audit db` re-checks it over the graph.
\ No newline at end of file
From dde4ec65ec9b4fed7556dd0b35778d7cc058be7c Mon Sep 17 00:00:00 2001
From: Jochen Schultz
Date: Thu, 13 Aug 2026 04:57:19 +0200
Subject: [PATCH 17/19] feat(kb): link leafs to their file so the graph has
edges to walk
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The graph was a bag of leafs. init_schema declared File, FROM_FILE,
HAS_VERSION and RUNS_ON, kblib's docstring advertised "Cypher graph hops",
and a fresh index produced 47 Leaf nodes, 0 File nodes and 0 edges — every
rel table empty. Nothing that reads the graph could have returned anything.
upsert_leaf now takes file_path/repo and MERGEs a File node plus a
FROM_FILE edge; kb/index and mail/index_mail pass them. neighbours_of()
does one hop: the other leafs of the same file, which is the step `--hop N`
repeats from each new frontier.
After --rebuild: 47 leafs, 12 files, 47 edges. A leaf found by FTS in
README.md now walks to its 6 siblings.
Co-Authored-By: Claude Opus 5
---
bin/kb/index | 2 +-
bin/mail/index_mail | 2 +-
bin/tools/kblib.py | 42 ++++++++++++++++++++++++++++++++++++-
bin/tools/test_kblib.py | 46 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 89 insertions(+), 3 deletions(-)
diff --git a/bin/kb/index b/bin/kb/index
index 6bf7cf5..f50c85b 100755
--- a/bin/kb/index
+++ b/bin/kb/index
@@ -83,7 +83,7 @@ def index_leafs(conn, leafs: list[dict], embed_fn, limit: int) -> tuple[int, int
upsert_leaf(conn, text=query, root="info", confidence="confirmed",
source=lf["source"], source_rev="working-tree",
how="kb/index", loc=lf["source"], type_=lf.get("type", "reference"),
- embedding=emb)
+ embedding=emb, file_path=lf["source"], repo=lf.get("repo", ""))
count += 1
return count, len(leafs)
diff --git a/bin/mail/index_mail b/bin/mail/index_mail
index 4d44eec..d4ca28a 100755
--- a/bin/mail/index_mail
+++ b/bin/mail/index_mail
@@ -121,7 +121,7 @@ def _index_leafs(conn, leafs: list[dict], embed_fn) -> tuple[int, int]:
upsert_leaf(conn, text=query, root="info", confidence="confirmed",
source=lf["source"], source_rev="mail" if lf.get("how") == "mail/import" else "working-tree",
how=lf.get("how", "kb/index"), loc=lf["source"], type_=lf.get("type", "reference"),
- embedding=emb)
+ embedding=emb, file_path=lf["source"], repo=lf.get("repo", ""))
count += 1
return count, len(leafs)
diff --git a/bin/tools/kblib.py b/bin/tools/kblib.py
index 93f3b8d..9d6f5af 100644
--- a/bin/tools/kblib.py
+++ b/bin/tools/kblib.py
@@ -112,7 +112,8 @@ def leaf_id(text: str, source: str) -> str:
def upsert_leaf(conn: ladybug.Connection, *, text: str, root: str, confidence: str,
source: str, source_rev: str, how: str, loc: str, type_: str,
embedding: list[float] | None,
- evidence: list[dict] | None = None) -> str:
+ evidence: list[dict] | None = None,
+ file_path: str | None = None, repo: str = "") -> str:
lid = leaf_id(text, source)
obs = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
conn.execute(
@@ -130,9 +131,48 @@ def upsert_leaf(conn: ladybug.Connection, *, text: str, root: str, confidence: s
)
for item in evidence or []:
upsert_evidence(conn, item, lid, obs)
+ if file_path:
+ link_to_file(conn, lid, file_path, repo, obs)
return lid
+def link_to_file(conn: ladybug.Connection, leaf: str, path: str, repo: str,
+ mtime: str) -> str:
+ """Attach a leaf to the file it came from.
+
+ Without these edges the graph is a bag of leafs: `--hop` has nothing to
+ walk and the File->Commit->Person history has nothing to hang off.
+ """
+ fid = sha256_b64(path)[:24]
+ conn.execute(
+ "MERGE (f:File {id:$id}) SET f.path=$path, f.repo=$repo, f.mtime=$mtime",
+ parameters={"id": fid, "path": path, "repo": repo, "mtime": mtime},
+ )
+ conn.execute(
+ "MATCH (l:Leaf {id:$lid}), (f:File {id:$fid}) MERGE (l)-[:FROM_FILE]->(f)",
+ parameters={"lid": leaf, "fid": fid},
+ )
+ return fid
+
+
+def neighbours_of(conn: ladybug.Connection, leaf_ids: list[str]) -> list[dict]:
+ """One hop: the other leafs of the same file, excluding the input set.
+
+ This is the walk `--hop N` repeats; N hops = N rounds from the new
+ frontier.
+ """
+ if not leaf_ids:
+ return []
+ r = conn.execute(
+ "MATCH (l:Leaf)-[:FROM_FILE]->(f:File)<-[:FROM_FILE]-(n:Leaf) "
+ "WHERE list_contains($ids, l.id) AND NOT list_contains($ids, n.id) "
+ "RETURN DISTINCT n.id, n.text, n.root, n.source",
+ parameters={"ids": leaf_ids},
+ )
+ return [{"id": row[0], "text": row[1], "root": row[2], "source": row[3]}
+ for row in r.get_all()]
+
+
def upsert_evidence(conn: ladybug.Connection, item: dict, leaf: str, observed_at: str) -> None:
"""Store one Evidence node and link it to the leaf it supports.
diff --git a/bin/tools/test_kblib.py b/bin/tools/test_kblib.py
index fc1b34a..a2b9299 100644
--- a/bin/tools/test_kblib.py
+++ b/bin/tools/test_kblib.py
@@ -120,6 +120,52 @@ def test_info_leafs_are_not_subject_to_the_evidence_rule(self):
embedding=make_emb(0.5))
self.assertEqual(kblib.facts_lacking_independence(self.conn), [])
+ def test_leaf_is_linked_to_its_file(self):
+ lid = kblib.upsert_leaf(self.conn, text="a heading", root="info",
+ confidence="confirmed", source="docs/design.md",
+ source_rev="r1", how="kb/index", loc="docs/design.md",
+ type_="reference", embedding=make_emb(0.5),
+ file_path="docs/design.md", repo="eSlider/2dph")
+ files = self.conn.execute(
+ "MATCH (l:Leaf {id:$id})-[:FROM_FILE]->(f:File) RETURN f.path, f.repo",
+ parameters={"id": lid}).get_all()
+ self.assertEqual(files, [["docs/design.md", "eSlider/2dph"]])
+
+ def test_leafs_of_one_file_share_a_single_file_node(self):
+ for text in ("first heading", "second heading"):
+ kblib.upsert_leaf(self.conn, text=text, root="info", confidence="confirmed",
+ source="docs/design.md", source_rev="r1", how="kb/index",
+ loc="docs/design.md", type_="reference",
+ embedding=make_emb(0.5), file_path="docs/design.md",
+ repo="eSlider/2dph")
+ count = self.conn.execute("MATCH (f:File) RETURN count(*)").get_all()[0][0]
+ self.assertEqual(count, 1)
+
+ def test_neighbours_are_the_other_leafs_of_the_same_file(self):
+ first = kblib.upsert_leaf(self.conn, text="first heading", root="info",
+ confidence="confirmed", source="docs/design.md",
+ source_rev="r1", how="kb/index", loc="d", type_="reference",
+ embedding=make_emb(0.5), file_path="docs/design.md",
+ repo="eSlider/2dph")
+ second = kblib.upsert_leaf(self.conn, text="second heading", root="info",
+ confidence="confirmed", source="docs/design.md",
+ source_rev="r1", how="kb/index", loc="d", type_="reference",
+ embedding=make_emb(0.5), file_path="docs/design.md",
+ repo="eSlider/2dph")
+ kblib.upsert_leaf(self.conn, text="elsewhere", root="info", confidence="confirmed",
+ source="README.md", source_rev="r1", how="kb/index", loc="r",
+ type_="reference", embedding=make_emb(0.5),
+ file_path="README.md", repo="eSlider/2dph")
+ neighbours = kblib.neighbours_of(self.conn, [first])
+ self.assertEqual([n["id"] for n in neighbours], [second])
+
+ def test_neighbours_of_an_unlinked_leaf_are_empty(self):
+ lid = kblib.upsert_leaf(self.conn, text="orphan", root="info",
+ confidence="confirmed", source="s", source_rev="r1",
+ how="test", loc="/tmp", type_="reference",
+ embedding=make_emb(0.5))
+ self.assertEqual(kblib.neighbours_of(self.conn, [lid]), [])
+
def test_stats_counts_roots(self):
kblib.upsert_leaf(self.conn, text="a fact leaf", root="facts",
confidence="confirmed", source="s", source_rev="r1",
From ec6aa60f33ea2fb2a2cd0112526126367d40bd7e Mon Sep 17 00:00:00 2001
From: Jochen Schultz
Date: Thu, 13 Aug 2026 05:15:35 +0200
Subject: [PATCH 18/19] feat(kbsearch): implement --hop, and stop swallowing
unknown flags
--hop was documented in six files and implemented in none. Worse, it failed
silently: the parser dropped any unrecognised `-flag` and kept its argument,
so the documented example `bin/kb/search "what runs on arc-2" --hop 1`
searched for "what runs on arc-2 1". Proven by running the old parse loop
verbatim.
Parsing moved to args.go, free of cgo so it can be tested at all. Unknown
flags, non-numeric or out-of-range values and an empty query are now errors
with exit 2 and a usage line, instead of being absorbed into the query.
expandHops() walks the FROM_FILE edge: each round asks for the neighbours of
the previous frontier, skips leafs already seen (so a<->b cannot loop), tags
new ones with their depth and adds at most `-n` per round. Hits carry
`hop: N` in YAML and JSON; ranked hits stay unmarked.
Docs corrected to what is actually walked: design.md and kb-search/SKILL.md
claimed `related:` links and vector-neighbours, and no such edges are written.
Verification: the package now type-checks as a whole. bin/kbsearch cannot be
compiled here (no native ladybug), so I built stub modules for the two cgo
dependencies and ran `go vet` plus the tests over every real source file --
which is how the unused `strings` import left behind by this change was
caught. The Cypher in hopStmt mirrors kblib.neighbours_of, which is tested
against a real database in test_kblib.py; the Go query itself is unrun.
Co-Authored-By: Claude Opus 5
---
bin/kbsearch/args.go | 79 ++++++++++++++++++++++++
bin/kbsearch/rank.go | 40 +++++++++++++
bin/kbsearch/rank_test.go | 122 +++++++++++++++++++++++++++++++++++++-
bin/kbsearch/search.go | 106 +++++++++++++++++++--------------
bin/kbsearch/types.go | 1 +
docs/design.md | 10 +++-
skills/kb-search/SKILL.md | 6 +-
7 files changed, 315 insertions(+), 49 deletions(-)
create mode 100644 bin/kbsearch/args.go
diff --git a/bin/kbsearch/args.go b/bin/kbsearch/args.go
new file mode 100644
index 0000000..9c3099f
--- /dev/null
+++ b/bin/kbsearch/args.go
@@ -0,0 +1,79 @@
+// Command line parsing for kbsearch. Kept free of cgo/db imports so the
+// parser is unit-testable on its own.
+package main
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+const usage = `usage: kbsearch "query" [--root facts|info] [--repo REPO] [-n N] [--hop N] [--json]
+ kbsearch serve [port]
+ kbsearch --list-model`
+
+type options struct {
+ query string
+ root string
+ repo string
+ limit int
+ hops int
+ jsonOut bool
+ listModel bool
+}
+
+// parseArgs reads the flags. Unknown flags are an error: silently dropping
+// them meant `--hop 1` vanished and its argument `1` was appended to the
+// query, so the search quietly answered a different question.
+func parseArgs(args []string) (options, error) {
+ opt := options{limit: 20}
+ var queryArgs []string
+
+ for i := 0; i < len(args); i++ {
+ arg := args[i]
+ wantsValue := arg == "--root" || arg == "--repo" || arg == "-n" || arg == "--hop"
+ if wantsValue && i+1 >= len(args) {
+ return opt, fmt.Errorf("%s needs a value", arg)
+ }
+ switch arg {
+ case "--root":
+ i++
+ opt.root = args[i]
+ if opt.root != "facts" && opt.root != "info" {
+ return opt, fmt.Errorf("--root must be facts or info, got %q", opt.root)
+ }
+ case "--repo":
+ i++
+ opt.repo = args[i]
+ case "-n":
+ i++
+ n, err := strconv.Atoi(args[i])
+ if err != nil || n < 1 {
+ return opt, fmt.Errorf("-n must be a positive integer, got %q", args[i])
+ }
+ opt.limit = n
+ case "--hop":
+ i++
+ n, err := strconv.Atoi(args[i])
+ if err != nil || n < 0 {
+ return opt, fmt.Errorf("--hop must be a non-negative integer, got %q", args[i])
+ }
+ opt.hops = n
+ case "--json":
+ opt.jsonOut = true
+ case "--list-model":
+ opt.listModel = true
+ default:
+ if strings.HasPrefix(arg, "-") {
+ return opt, fmt.Errorf("unknown flag %q", arg)
+ }
+ queryArgs = append(queryArgs, arg)
+ }
+ }
+
+ opt.query = strings.TrimSpace(strings.Join(queryArgs, " "))
+ if opt.query == "" && !opt.listModel {
+ return opt, fmt.Errorf("no query given")
+ }
+ return opt, nil
+}
diff --git a/bin/kbsearch/rank.go b/bin/kbsearch/rank.go
index cf7be33..69c0d26 100644
--- a/bin/kbsearch/rank.go
+++ b/bin/kbsearch/rank.go
@@ -71,6 +71,46 @@ func hybrid(fts, vec []Hit, limit int) []Hit {
return out
}
+// expandHops walks the graph from the ranked hits: each round takes the leafs
+// reached in the previous one and asks fetch() for their neighbours (the other
+// leafs of the same file). Already-seen leafs are never re-emitted, so a round
+// that finds nothing new ends the walk. Each round adds at most `perRound`
+// leafs, tagged with the depth they were found at.
+func expandHops(seed []Hit, hops, perRound int, fetch func([]string) ([]Hit, error)) ([]Hit, error) {
+ if hops <= 0 || len(seed) == 0 {
+ return seed, nil
+ }
+ out := append([]Hit(nil), seed...)
+ seen := make(map[string]bool, len(seed))
+ frontier := make([]string, 0, len(seed))
+ for _, h := range seed {
+ seen[h.ID] = true
+ frontier = append(frontier, h.ID)
+ }
+
+ for depth := 1; depth <= hops; depth++ {
+ found, err := fetch(frontier)
+ if err != nil {
+ return out, err
+ }
+ var next []string
+ for _, h := range found {
+ if seen[h.ID] || (perRound > 0 && len(next) >= perRound) {
+ continue
+ }
+ seen[h.ID] = true
+ h.Hop = depth
+ out = append(out, h)
+ next = append(next, h.ID)
+ }
+ if len(next) == 0 {
+ break
+ }
+ frontier = next
+ }
+ return out, nil
+}
+
func filterRoot(hits []Hit, root string) []Hit {
var out []Hit
for _, h := range hits {
diff --git a/bin/kbsearch/rank_test.go b/bin/kbsearch/rank_test.go
index de2fb20..3fbe319 100644
--- a/bin/kbsearch/rank_test.go
+++ b/bin/kbsearch/rank_test.go
@@ -1,7 +1,10 @@
// Unit tests for the pure ranking/filtering stage (no db, no model, offline).
package main
-import "testing"
+import (
+ "errors"
+ "testing"
+)
func h(id, root, source string) Hit {
return Hit{ID: id, Text: id, Root: root, Source: source}
@@ -91,6 +94,123 @@ func TestHybridKeepsVectorScoreForSharedHit(t *testing.T) {
}
}
+// --- flag parsing ---
+
+// The bug this replaced: --hop was dropped as an unknown flag and its
+// argument "1" was appended to the query, so the search silently answered a
+// different question.
+func TestParseHopIsNotSwallowedIntoTheQuery(t *testing.T) {
+ opt, err := parseArgs([]string{"what runs on arc-2", "--hop", "1"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if opt.query != "what runs on arc-2" {
+ t.Fatalf("query = %q, want %q", opt.query, "what runs on arc-2")
+ }
+ if opt.hops != 1 {
+ t.Fatalf("hops = %d, want 1", opt.hops)
+ }
+}
+
+func TestParseRejectsUnknownFlags(t *testing.T) {
+ if _, err := parseArgs([]string{"query", "--nope"}); err == nil {
+ t.Fatal("unknown flag accepted")
+ }
+}
+
+func TestParseRejectsBadValues(t *testing.T) {
+ for _, args := range [][]string{
+ {"q", "-n", "zero"},
+ {"q", "-n", "0"},
+ {"q", "--hop", "-1"},
+ {"q", "--root", "nonsense"},
+ {"q", "--hop"},
+ {"--json"},
+ } {
+ if _, err := parseArgs(args); err == nil {
+ t.Errorf("accepted %v", args)
+ }
+ }
+}
+
+func TestParseDefaults(t *testing.T) {
+ opt, err := parseArgs([]string{"two", "words", "--json"})
+ if err != nil || opt.query != "two words" || opt.limit != 20 || opt.hops != 0 || !opt.jsonOut {
+ t.Fatalf("got %+v err=%v", opt, err)
+ }
+}
+
+func TestListModelNeedsNoQuery(t *testing.T) {
+ if _, err := parseArgs([]string{"--list-model"}); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+}
+
+// --- graph walk ---
+
+func fakeGraph(edges map[string][]string) func([]string) ([]Hit, error) {
+ return func(ids []string) ([]Hit, error) {
+ var out []Hit
+ for _, id := range ids {
+ for _, n := range edges[id] {
+ out = append(out, h(n, "info", "x"))
+ }
+ }
+ return out, nil
+ }
+}
+
+func TestExpandHopsAddsNeighboursTaggedWithDepth(t *testing.T) {
+ got, err := expandHops([]Hit{h("a", "info", "x")}, 1, 0,
+ fakeGraph(map[string][]string{"a": {"b", "c"}}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ eq(t, got, "a", "b", "c")
+ if got[1].Hop != 1 || got[2].Hop != 1 {
+ t.Fatalf("neighbours not tagged with depth: %+v", got)
+ }
+}
+
+func TestExpandHopsWalksFurtherOnHigherN(t *testing.T) {
+ edges := map[string][]string{"a": {"b"}, "b": {"c"}, "c": {"d"}}
+ one, _ := expandHops([]Hit{h("a", "info", "x")}, 1, 0, fakeGraph(edges))
+ eq(t, one, "a", "b")
+ two, _ := expandHops([]Hit{h("a", "info", "x")}, 2, 0, fakeGraph(edges))
+ eq(t, two, "a", "b", "c")
+ if two[2].Hop != 2 {
+ t.Fatalf("depth not counted per round: %+v", two[2])
+ }
+}
+
+func TestExpandHopsNeverRevisitsALeaf(t *testing.T) {
+ // a<->b would loop forever without the seen set.
+ got, _ := expandHops([]Hit{h("a", "info", "x")}, 5, 0,
+ fakeGraph(map[string][]string{"a": {"b"}, "b": {"a"}}))
+ eq(t, got, "a", "b")
+}
+
+func TestExpandHopsIsANoOpWithoutHops(t *testing.T) {
+ seed := []Hit{h("a", "info", "x")}
+ got, _ := expandHops(seed, 0, 0, fakeGraph(map[string][]string{"a": {"b"}}))
+ eq(t, got, "a")
+}
+
+func TestExpandHopsCapsEachRound(t *testing.T) {
+ got, _ := expandHops([]Hit{h("a", "info", "x")}, 1, 2,
+ fakeGraph(map[string][]string{"a": {"b", "c", "d", "e"}}))
+ eq(t, got, "a", "b", "c")
+}
+
+func TestExpandHopsReturnsWhatItHasOnError(t *testing.T) {
+ boom := func([]string) ([]Hit, error) { return nil, errors.New("db gone") }
+ got, err := expandHops([]Hit{h("a", "info", "x")}, 1, 0, boom)
+ if err == nil {
+ t.Fatal("error swallowed")
+ }
+ eq(t, got, "a")
+}
+
// Regression guard for the FTS statement: BM25 scores rank best-first.
func TestFTSQueryOrdersByScoreDescending(t *testing.T) {
if !contains(ftsStmt, "ORDER BY score DESC") {
diff --git a/bin/kbsearch/search.go b/bin/kbsearch/search.go
index 29a20e5..2c2fd50 100644
--- a/bin/kbsearch/search.go
+++ b/bin/kbsearch/search.go
@@ -14,7 +14,6 @@ import (
"os/exec"
"path/filepath"
"strconv"
- "strings"
"syscall"
"time"
@@ -33,46 +32,23 @@ const ftsStmt = "CALL QUERY_FTS_INDEX('Leaf', 'id', $q) " +
const vecStmt = "CALL QUERY_VECTOR_INDEX('Leaf', 'Leaf_vec', $q, $n) " +
"RETURN node.id, node.text, node.root, node.source, distance ORDER BY distance LIMIT $n"
+// One hop: the other leafs of the file a leaf came from. Mirrors
+// kblib.neighbours_of; the edges are written by kb/index.
+const hopStmt = "MATCH (l:Leaf)-[:FROM_FILE]->(f:File)<-[:FROM_FILE]-(n:Leaf) " +
+ "WHERE list_contains($ids, l.id) AND NOT list_contains($ids, n.id) " +
+ "RETURN DISTINCT n.id, n.text, n.root, n.source"
+
func runSearch(args []string) int {
- // Manual flag parsing to allow flags after query (like Python argparse)
- root := ""
- repo := ""
- limit := 20
- jsonOut := false
- listModel := false
-
- var queryArgs []string
- for i := 0; i < len(args); i++ {
- switch args[i] {
- case "--root":
- if i+1 < len(args) {
- root = args[i+1]
- i++
- }
- case "--repo":
- if i+1 < len(args) {
- repo = args[i+1]
- i++
- }
- case "-n":
- if i+1 < len(args) {
- if n, err := strconv.Atoi(args[i+1]); err == nil {
- limit = n
- }
- i++
- }
- case "--json":
- jsonOut = true
- case "--list-model":
- listModel = true
- default:
- if !strings.HasPrefix(args[i], "-") {
- queryArgs = append(queryArgs, args[i])
- }
- }
+ // Flags may follow the query, like the python argparse version allowed.
+ opt, err := parseArgs(args)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "kbsearch: %v\n%s\n", err, usage)
+ return 2
}
+ root, repo, limit, query := opt.root, opt.repo, opt.limit, opt.query
+ jsonOut := opt.jsonOut
- if listModel {
+ if opt.listModel {
dir, err := modelDir()
if err != nil {
fmt.Fprintln(os.Stderr, err)
@@ -82,12 +58,6 @@ func runSearch(args []string) int {
return 0
}
- query := strings.TrimSpace(strings.Join(queryArgs, " "))
- if query == "" {
- fmt.Fprintln(os.Stderr, "usage: kbsearch \"query\" [--root facts|info] [--repo REPO] [-n N] [--json]")
- return 1
- }
-
if err := openBrain(); err != nil {
fmt.Fprintf(os.Stderr, "open brain: %v\n", err)
return 1
@@ -113,6 +83,14 @@ func runSearch(args []string) int {
results := rankAndFilter(fts, vec, root, repo, limit)
+ if opt.hops > 0 {
+ walked, err := expandHops(results, opt.hops, limit, neighbours)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "hop: %v\n", err)
+ }
+ results = walked
+ }
+
for i := range results {
if results[i].Text != "" {
runes := []rune(results[i].Text)
@@ -184,6 +162,41 @@ func queryVector(emb []float64, limit int) ([]Hit, error) {
return hits, nil
}
+// neighbours runs one hop for every leaf id in the frontier.
+func neighbours(ids []string) ([]Hit, error) {
+ anyIDs := make([]any, len(ids))
+ for i, v := range ids {
+ anyIDs[i] = v
+ }
+ stmt, err := conn.Prepare(hopStmt)
+ if err != nil {
+ return nil, err
+ }
+ defer stmt.Close()
+ res, err := conn.Execute(stmt, map[string]any{"ids": anyIDs})
+ if err != nil {
+ return nil, err
+ }
+ var hits []Hit
+ for res.HasNext() {
+ row, err := res.Next()
+ if err != nil {
+ return nil, err
+ }
+ vals, err := row.GetAsSlice()
+ if err != nil || len(vals) < 4 {
+ continue
+ }
+ hits = append(hits, Hit{
+ ID: fmt.Sprint(vals[0]),
+ Text: fmt.Sprint(vals[1]),
+ Root: fmt.Sprint(vals[2]),
+ Source: fmt.Sprint(vals[3]),
+ })
+ }
+ return hits, nil
+}
+
func rowsToHits(res *lbug.QueryResult) ([]Hit, error) {
var hits []Hit
for res.HasNext() {
@@ -218,6 +231,7 @@ type jsonHit struct {
Text string `json:"text"`
Root string `json:"root"`
Score float64 `json:"score"`
+ Hop int `json:"hop,omitempty"`
Snippet string `json:"snippet,omitempty"`
}
@@ -229,6 +243,7 @@ func toJSONOut(hits []Hit, query, rootFilter string) *jsonOut {
Text: h.Text,
Root: h.Root,
Score: h.Score,
+ Hop: h.Hop,
Snippet: h.Snippet,
}
}
@@ -249,6 +264,9 @@ func resultsToDicts(hits []Hit) []any {
{"root", h.Root},
{"score", h.Score},
}
+ if h.Hop > 0 {
+ d = append(d, KV{"hop", h.Hop})
+ }
if h.Snippet != "" {
d = append(d, KV{"snippet", h.Snippet})
}
diff --git a/bin/kbsearch/types.go b/bin/kbsearch/types.go
index 2a403bd..4c4c54e 100644
--- a/bin/kbsearch/types.go
+++ b/bin/kbsearch/types.go
@@ -13,4 +13,5 @@ type Hit struct {
Source string `json:"-"` // for repo filtering, not in output
Score float64 `json:"score"`
Snippet string `json:"snippet,omitempty"`
+ Hop int `json:"hop,omitempty"` // 0 = ranked hit, N = reached in N graph hops
}
\ No newline at end of file
diff --git a/docs/design.md b/docs/design.md
index 18d6886..f9f6a2e 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -23,8 +23,14 @@ bin/kb/search "question"
3. web-search — second independent source → upgrade hypothesis to confirmed
```
-`--hop N` follows graph edges (sibling leaves under a heading, owning file,
-`related:` files, vector-neighbour leaves) — the deduction walk.
+`--hop N` follows graph edges — the deduction walk. Implemented today: the
+`FROM_FILE` edge, so one hop reaches the other leafs of the same file, and N
+hops repeat that from each new frontier (already-seen leafs are never
+re-emitted). Results carry `hop: N`; ranked hits have no `hop` key.
+
+`related:` links and vector-neighbour edges are *not* walked — nothing writes
+those edges yet. `HAS_VERSION`/`AUTHORED` exist in the schema and are written
+only by `bin/git/import`.
## Who / What / How / Where / When + evidence
diff --git a/skills/kb-search/SKILL.md b/skills/kb-search/SKILL.md
index 3800da3..09c02a2 100644
--- a/skills/kb-search/SKILL.md
+++ b/skills/kb-search/SKILL.md
@@ -38,8 +38,10 @@ bin/kb/eval # recall@5 >= 0.95 gate
facts first, then info leafs clearly marked `(not confirmed)`.
- If recall looks wrong, run `bin/kb/eval`; it gates control questions and
should stay at or above 95% recall@5.
-- `--hop N` follows sibling leaves, owning files, `related:` links and
- vector-neighbours — that is the deduction walk, not random expansion.
+- `--hop N` walks the `FROM_FILE` edge: one hop adds the other leafs of the
+ same file, N hops repeat from each new frontier. Hop results are marked
+ `hop: N`; ranked hits are unmarked. That is the deduction walk, not random
+ expansion. (`related:` links and vector-neighbour edges are not written yet.)
- Escalate to `web-search` (the `web-search` skill) as the independent second
source when both local roots cannot confirm; never report an unconfirmed
single-source local answer as fact.
\ No newline at end of file
From 7ecde15f6546fa43a90939d20774be9c4ce2efc5 Mon Sep 17 00:00:00 2001
From: Jochen Schultz
Date: Thu, 13 Aug 2026 05:21:11 +0200
Subject: [PATCH 19/19] feat(facts): audit every documented command and flag,
not just audit's modes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The mode check I added only covered bin/facts/audit, so it would not have
caught --hop, and did not. check_documented_commands() now scans AGENTS.md,
PLAN.md, README.md, docs/*.md and skills/*/SKILL.md: every bin/... shown
inside code must exist, and every flag it is shown with must be declared in
its source.
Static by design — running each tool with --help would execute tools like
facts/crm that have no argparse and no dry-run. Both declaration spellings
count, since argparse writes "--rebuild" while Go's flag package writes
"out" and still accepts --out.
Three false positives were found while triaging and fixed in the checker,
not by loosening the rule: `-s bin/tools` is a directory argument, `-n` in
`bash -n bin/db/psql-yq` sits before the command, and `///usr/bin/env go run`
is not an invocation of `bin/env`.
Four real findings, now fixed:
- AGENTS.md advertised bin/md/tables and bin/brain/deduce; neither exists.
Replaced with bin/kb/stats, and PLAN's layout marks the rest as planned
- skills/diataxis-docs documented `--hop` following `related:` links and a
`--type` filter; neither exists. Corrected to what the tools do
- skills/agent-cost documents bin/agents/cost, which was never vendored.
Rather than delete someone's skill, it is declared in EXTERNAL_TOOLS with
a reason and printed under `exceptions` on every audit run — an invisible
suppression is how --hop survived in six files
96 python tests, audit self and db clean, recall@5 1.0.
Co-Authored-By: Claude Opus 5
---
AGENTS.md | 3 +-
PLAN.md | 4 +-
bin/facts/audit | 93 ++++++++++++++++++++++++++++++++++-
bin/tools/test_facts_audit.py | 69 ++++++++++++++++++++++++++
skills/diataxis-docs/SKILL.md | 9 ++--
5 files changed, 169 insertions(+), 9 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index eae389f..ce5d099 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -81,8 +81,7 @@ bin/mail/index_mail # rebuil
bin/facts/audit ["self"|"db"] # repo invariants | evidence over kb.lbug
bin/facts/crm [--dry-run] # proof person↔company/company↔project (ooCRM × corpus SoT)
bin/kb/search "query" [--hop N] [--repo X] # deduction search → YAML
-bin/md/tables # what the graph holds → YAML
-bin/brain/deduce "question" # thinking wrapper
+bin/kb/stats # what the graph holds → YAML
```
Never start a shell command with `cd` — use the tool working-directory
diff --git a/PLAN.md b/PLAN.md
index f952637..bda5aab 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -54,8 +54,8 @@ detective method: **a fact needs ≥2 independent sources or it is
kb/index build FTS + HNSW from corpus
kb/search deduction: facts → info → web-search; --hop N
kb/get kb/stats kb/eval
- md/import md/select md/tables md/gaps (mistune)
- brain/extract brain/audit brain/deduce (thinking wrapper)
+ md/import (mistune; select/tables/gaps planned)
+ (brain/* thinking wrappers planned)
web/search (vendored)
db/psql-yq (vendored)
ssh-tunnel onlyoffice pg tunnel 5433
diff --git a/bin/facts/audit b/bin/facts/audit
index f8b36c9..5df76f5 100755
--- a/bin/facts/audit
+++ b/bin/facts/audit
@@ -165,6 +165,95 @@ def code_snippets(text: str) -> list[str]:
return spans
+DOC_FILES = ["AGENTS.md", "PLAN.md", "README.md"]
+
+# Where a tool's flags are really declared, when not in the tool file itself:
+# bin/kb/search is a shell wrapper around the Go binary, bin/mail/sync.go is a
+# shebang entry whose flags live in its package.
+FLAG_SOURCES = {
+ "bin/kb/search": ["bin/kbsearch"],
+ "bin/mail/sync.go": ["bin/mail/sync"],
+}
+
+# Tools documented here but deliberately not vendored. Each needs a reason,
+# and `audit self` prints them under `exceptions` on every run — a suppression
+# you cannot see is how `--hop` survived in six files for as long as it did.
+EXTERNAL_TOOLS = {
+ "bin/agents/cost": "skills/agent-cost documents the upstream tool; "
+ "not vendored into this repo (PLAN D2). Vendor it or drop the skill.",
+}
+
+
+def doc_paths() -> list[Path]:
+ paths = [ROOT / n for n in DOC_FILES]
+ paths += sorted((ROOT / "docs").glob("*.md"))
+ paths += sorted((ROOT / "skills").glob("*/SKILL.md"))
+ return [p for p in paths if p.exists()]
+
+
+def documented_invocations(text: str) -> list[tuple[str, list[str]]]:
+ """(tool, flags) for every bin/... invocation inside code in a doc."""
+ out: list[tuple[str, list[str]]] = []
+ for snippet in code_snippets(text):
+ # (? bool:
+ """Search the tool's own source (and its package, for wrappers) for the
+ flag. Static on purpose: running a tool to ask for --help would execute
+ tools that have no argparse and no dry-run.
+
+ Both spellings count: argparse writes `add_argument("--rebuild")`, Go's
+ flag package writes `fs.String("out", ...)` and still accepts `--out`.
+ """
+ bare = flag.lstrip("-")
+ wanted = (flag, f'"{bare}"', f"'{bare}'")
+ haystacks: list[Path] = [ROOT / tool]
+ for extra in FLAG_SOURCES.get(tool, []):
+ haystacks.extend(sorted((ROOT / extra).rglob("*")))
+ for path in haystacks:
+ if not path.is_file():
+ continue
+ try:
+ body = path.read_text(encoding="utf-8", errors="replace")
+ except OSError:
+ continue
+ if any(w in body for w in wanted):
+ return True
+ return False
+
+
+def check_documented_commands() -> list[str]:
+ """Every bin/... command shown in the docs must exist and accept the flags
+ it is shown with. `--hop` was documented in six files and implemented in
+ none, and silently swallowed its argument into the query."""
+ problems: list[str] = []
+ for path in doc_paths():
+ name = path.relative_to(ROOT)
+ for tool, flags in documented_invocations(path.read_text()):
+ target = ROOT / tool
+ if target.is_dir():
+ continue # a path argument, not a command (`-s bin/tools`)
+ if tool in EXTERNAL_TOOLS:
+ continue
+ if not target.exists():
+ problems.append(f"{name} documents a tool that does not exist: {tool}")
+ continue
+ for flag in flags:
+ if not flag_is_declared(tool, flag):
+ problems.append(f"{name}: {tool} does not accept {flag}")
+ return sorted(set(problems))
+
+
def check_documented_modes() -> list[str]:
"""Docs must name the modes this tool actually has (AGENTS rule 6)."""
problems: list[str] = []
@@ -206,7 +295,7 @@ def check_docs() -> list[str]:
def audit_self() -> list[str]:
problems: list[str] = []
for check in (check_evidence_rule, check_tool_convention,
- check_documented_modes, check_docs):
+ check_documented_commands, check_documented_modes, check_docs):
problems.extend(check())
return problems
@@ -220,6 +309,8 @@ def main(argv: list[str]) -> int:
problems = audit_self() if a.mode == "self" else audit_db()
out = {"mode": a.mode, "ok": not problems, "problems": problems}
+ if a.mode == "self" and EXTERNAL_TOOLS:
+ out["exceptions"] = [f"{tool}: {why}" for tool, why in sorted(EXTERNAL_TOOLS.items())]
if a.json:
print(json.dumps(out, indent=2))
else:
diff --git a/bin/tools/test_facts_audit.py b/bin/tools/test_facts_audit.py
index f004969..bd02848 100644
--- a/bin/tools/test_facts_audit.py
+++ b/bin/tools/test_facts_audit.py
@@ -128,6 +128,75 @@ def test_prose_mentioning_the_tool_is_not_a_mode_claim(self):
self.assertEqual(audit.check_documented_modes(), [])
+class DocumentedCommandsTest(unittest.TestCase):
+ def setUp(self):
+ tmp = tempfile.TemporaryDirectory()
+ self.addCleanup(tmp.cleanup)
+ self.tmp = Path(tmp.name)
+ (self.tmp / "docs").mkdir()
+ (self.tmp / "skills").mkdir()
+ self.original_root = audit.ROOT
+ audit.ROOT = self.tmp
+
+ def tearDown(self):
+ audit.ROOT = self.original_root
+
+ def readme(self, body: str) -> None:
+ (self.tmp / "README.md").write_text(f"```bash\n{body}\n```\n")
+
+ def test_missing_tool_is_flagged(self):
+ self.readme("bin/brain/deduce 'question'")
+ self.assertTrue(any("does not exist" in p for p in audit.check_documented_commands()))
+
+ def test_existing_tool_passes(self):
+ write_tool(self.tmp / "bin" / "kb" / "stats", "#!/usr/bin/env python3\n")
+ self.readme("bin/kb/stats")
+ self.assertEqual(audit.check_documented_commands(), [])
+
+ def test_undeclared_flag_is_flagged(self):
+ write_tool(self.tmp / "bin" / "kb" / "search", '#!/bin/sh\n# --root and --json\n')
+ self.readme("bin/kb/search 'deploy' --type howto")
+ problems = audit.check_documented_commands()
+ self.assertTrue(any("--type" in p for p in problems), problems)
+
+ def test_go_style_flag_declaration_counts(self):
+ # fs.String("out", ...) accepts --out even though "--out" never appears.
+ write_tool(self.tmp / "bin" / "mail" / "sync", '#!/bin/sh\nfs.String("out", "")\n')
+ self.readme("bin/mail/sync --out var/mail")
+ self.assertEqual(audit.check_documented_commands(), [])
+
+ def test_flags_before_the_command_are_not_attributed_to_it(self):
+ write_tool(self.tmp / "bin" / "db" / "psql-yq", "#!/bin/sh\n")
+ self.readme("bash -n bin/db/psql-yq")
+ self.assertEqual(audit.check_documented_commands(), [])
+
+ def test_a_directory_argument_is_not_a_command(self):
+ (self.tmp / "bin" / "tools").mkdir(parents=True)
+ self.readme("uv run python -m unittest discover -s bin/tools -t .")
+ self.assertEqual(audit.check_documented_commands(), [])
+
+ def test_the_go_shebang_is_not_read_as_a_command(self):
+ (self.tmp / "README.md").write_text('`///usr/bin/env go run "$0" "$@"; exit`\n')
+ self.assertEqual(audit.check_documented_commands(), [])
+
+ def test_prose_is_not_scanned(self):
+ (self.tmp / "README.md").write_text("Someday bin/brain/deduce will answer questions.\n")
+ self.assertEqual(audit.check_documented_commands(), [])
+
+ def test_skills_are_scanned_too(self):
+ skill = self.tmp / "skills" / "some-skill"
+ skill.mkdir()
+ (skill / "SKILL.md").write_text("```bash\nbin/nope/tool\n```\n")
+ self.assertTrue(any("bin/nope/tool" in p for p in audit.check_documented_commands()))
+
+ def test_declared_external_tools_are_exempt_but_listed(self):
+ self.readme("bin/agents/cost --json")
+ self.assertEqual(audit.check_documented_commands(), [])
+ # The exemption has to stay visible, with a reason, or it is a rug.
+ self.assertIn("bin/agents/cost", audit.EXTERNAL_TOOLS)
+ self.assertTrue(audit.EXTERNAL_TOOLS["bin/agents/cost"].strip())
+
+
class EvidenceRuleTest(unittest.TestCase):
def test_the_shipped_rule_satisfies_the_gate(self):
# Runs against the real factsrules, not a fixture.
diff --git a/skills/diataxis-docs/SKILL.md b/skills/diataxis-docs/SKILL.md
index 16bc675..d42441f 100644
--- a/skills/diataxis-docs/SKILL.md
+++ b/skills/diataxis-docs/SKILL.md
@@ -30,12 +30,13 @@ related:
---
```
-`bin/kb/index` reads this. `type` becomes a searchable column and `related`
-becomes a graph edge:
+`bin/kb/index` reads this. `type` is stored on the leaf; filtering on it from
+the CLI is not implemented, so use the root filter. `related` is not turned
+into a graph edge yet — the walk follows `FROM_FILE`.
```bash
-bin/kb/search "deploy" --type howto
-bin/kb/search "Stecktafel" --hop 1 # follow links and related
+bin/kb/search "deploy" --root info
+bin/kb/search "Stecktafel" --hop 1 # walks FROM_FILE to the file's other leafs
```
## Audit checklist