From d10aaf0928e62166491aac101f66124fe7db0d1e Mon Sep 17 00:00:00 2001 From: cvetty Date: Thu, 11 Jun 2026 23:55:10 +0300 Subject: [PATCH 1/5] build(docs): add docs dependency group, make targets, ignore site/ --- .gitignore | 3 + Makefile | 8 +- pyproject.toml | 4 + uv.lock | 630 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 644 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index de6d83a..266a584 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ whygraph.toml # Local scratch space for in-progress plan markdown (not version-controlled). plans/ + +# MkDocs build output (the site is built and deployed by CI, never committed). +site/ diff --git a/Makefile b/Makefile index 13e503a..6f07862 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ IMAGE ?= whygraph:dev # Name of the long-running container started by `make image-debug`. DEBUG_NAME ?= whygraph-debug -.PHONY: help sync test scan db db-down inspect image image-test image-inspect image-debug image-debug-down +.PHONY: help sync test scan docs docs-build db db-down inspect image image-test image-inspect image-debug image-debug-down help: ## List available targets @grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | sort | awk 'BEGIN{FS=":.*?## "}{printf " %-10s %s\n", $$1, $$2}' @@ -27,6 +27,12 @@ test: ## Run the test suite scan: ## Re-scan this repo so WhyGraph is tested against itself uv run whygraph scan +docs: ## Serve the docs site locally with live reload (social cards skipped — no Cairo needed) + uv run mkdocs serve + +docs-build: ## Build the static docs site into ./site (strict; cards skipped unless CI=true) + uv run mkdocs build --strict + db: ## Start the DBGate database viewer (http://localhost:8081) @test -f docker-compose.yml || { echo "error: docker-compose.yml missing - run: cp docker-compose.example.yml docker-compose.yml"; exit 1; } @test -f .whygraph/whygraph.db || echo "warning: .whygraph/whygraph.db missing - run 'make scan' first" diff --git a/pyproject.toml b/pyproject.toml index 69ff2d2..a9e4a57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,3 +36,7 @@ line-length = 88 [dependency-groups] dev = ["pytest>=8", "ruff>=0.8"] +docs = [ + "mkdocs-material[imaging]>=9.5", # [imaging] → cairosvg + pillow for social cards + "mkdocs-git-revision-date-localized-plugin>=1.2", # per-page "last updated" dates +] diff --git a/uv.lock b/uv.lock index a6d7ba1..d91154f 100644 --- a/uv.lock +++ b/uv.lock @@ -75,6 +75,56 @@ 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 = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, + { url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, +] + +[[package]] +name = "cairocffi" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" }, +] + +[[package]] +name = "cairosvg" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cairocffi" }, + { name = "cssselect2" }, + { name = "defusedxml" }, + { name = "pillow" }, + { name = "tinycss2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/07/e8412a13019b3f737972dea23a2c61ca42becafc16c9338f4ca7a0caa993/cairosvg-2.9.0.tar.gz", hash = "sha256:1debb00cd2da11350d8b6f5ceb739f1b539196d71d5cf5eb7363dbd1bfbc8dc5", size = 40877, upload-time = "2026-03-13T15:42:00.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/e0/5011747466414c12cac8a8df77aa235068669a6a5a5df301a96209db6054/cairosvg-2.9.0-py3-none-any.whl", hash = "sha256:4b82d07d145377dffdfc19d9791bd5fb65539bb4da0adecf0bdbd9cd4ffd7c68", size = 45962, upload-time = "2026-03-14T13:56:33.512Z" }, +] + [[package]] name = "certifi" version = "2026.4.22" @@ -154,6 +204,95 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + [[package]] name = "click" version = "8.3.3" @@ -234,6 +373,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/9c/51f28c3550276bcf35660703ba0ab829a90b88be8cd98a71ef23c2413913/cryptography-47.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", size = 3698916, upload-time = "2026-04-24T19:54:49.782Z" }, ] +[[package]] +name = "cssselect2" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tinycss2" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" }, +] + +[[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 = "distro" version = "1.9.0" @@ -252,6 +413,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, +] + [[package]] name = "greenlet" version = "3.5.0" @@ -363,6 +560,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "jiter" version = "0.15.0" @@ -501,6 +710,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, ] +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -627,6 +845,105 @@ 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 = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-git-revision-date-localized-plugin" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "gitpython" }, + { name = "mkdocs" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/99/8067eb7d1652767ee8e5474010647dd5a8e464e0ca8c783b5cac135a2043/mkdocs_git_revision_date_localized_plugin-1.5.3.tar.gz", hash = "sha256:873444b54cab4d47c69bd6e85da05ef5fbe81fee27e64508114c46a0e4f81e37", size = 451961, upload-time = "2026-06-01T08:32:09.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/d0/cbe85158dc091219fd5134bf6d724d30b1f2005ee1d0dabaaa41416bee78/mkdocs_git_revision_date_localized_plugin-1.5.3-py3-none-any.whl", hash = "sha256:cd96e432de6a7e59b31c7041574b22f84179c8636835419ff458877ecfaaaf05", size = 26156, upload-time = "2026-06-01T08:32:07.765Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + +[package.optional-dependencies] +imaging = [ + { name = "cairosvg" }, + { name = "pillow" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + [[package]] name = "numpy" version = "2.4.4" @@ -747,6 +1064,120 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -919,6 +1350,19 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pymdown-extensions" +version = "10.21.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, +] + [[package]] name = "pytest" version = "9.0.3" @@ -935,6 +1379,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] +[[package]] +name = "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-dotenv" version = "1.2.2" @@ -972,6 +1428,73 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, ] +[[package]] +name = "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/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { 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 = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + [[package]] name = "referencing" version = "0.37.0" @@ -986,6 +1509,21 @@ 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 = "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" @@ -1262,6 +1800,24 @@ 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 = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -1373,6 +1929,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] +[[package]] +name = "tinycss2" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, +] + [[package]] name = "tomli-w" version = "1.2.0" @@ -1430,6 +1998,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[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 = "uvicorn" version = "0.46.0" @@ -1443,6 +2029,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, ] +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + [[package]] name = "whygraph" version = "0.1.0" @@ -1465,6 +2087,10 @@ dev = [ { name = "pytest" }, { name = "ruff" }, ] +docs = [ + { name = "mkdocs-git-revision-date-localized-plugin" }, + { name = "mkdocs-material", extra = ["imaging"] }, +] [package.metadata] requires-dist = [ @@ -1485,3 +2111,7 @@ dev = [ { name = "pytest", specifier = ">=8" }, { name = "ruff", specifier = ">=0.8" }, ] +docs = [ + { name = "mkdocs-git-revision-date-localized-plugin", specifier = ">=1.2" }, + { name = "mkdocs-material", extras = ["imaging"], specifier = ">=9.5" }, +] From 8f3bad1d1fa233a30faa0a803a6a526816a8785c Mon Sep 17 00:00:00 2001 From: cvetty Date: Fri, 12 Jun 2026 00:02:17 +0300 Subject: [PATCH 2/5] docs: add Material for MkDocs site (config + 18 pages) --- docs/deploy/docker.md | 68 +++++++++++++++++ docs/deploy/index.md | 30 ++++++++ docs/deploy/service.md | 89 ++++++++++++++++++++++ docs/getting-started/index.md | 61 +++++++++++++++ docs/getting-started/installation.md | 69 +++++++++++++++++ docs/getting-started/quickstart.md | 82 ++++++++++++++++++++ docs/guide/concepts.md | 59 ++++++++++++++ docs/guide/editors.md | 58 ++++++++++++++ docs/guide/index.md | 46 +++++++++++ docs/guide/mcp-usage.md | 50 ++++++++++++ docs/guide/scanning.md | 75 ++++++++++++++++++ docs/index.md | 87 +++++++++++++++++++++ docs/reference/cli.md | 95 +++++++++++++++++++++++ docs/reference/configuration.md | 110 +++++++++++++++++++++++++++ docs/reference/index.md | 40 ++++++++++ docs/reference/mcp.md | 109 ++++++++++++++++++++++++++ docs/reference/providers.md | 48 ++++++++++++ docs/roadmap.md | 34 +++++++++ docs/stylesheets/extra.css | 4 + mkdocs.yml | 99 ++++++++++++++++++++++++ 20 files changed, 1313 insertions(+) create mode 100644 docs/deploy/docker.md create mode 100644 docs/deploy/index.md create mode 100644 docs/deploy/service.md create mode 100644 docs/getting-started/index.md create mode 100644 docs/getting-started/installation.md create mode 100644 docs/getting-started/quickstart.md create mode 100644 docs/guide/concepts.md create mode 100644 docs/guide/editors.md create mode 100644 docs/guide/index.md create mode 100644 docs/guide/mcp-usage.md create mode 100644 docs/guide/scanning.md create mode 100644 docs/index.md create mode 100644 docs/reference/cli.md create mode 100644 docs/reference/configuration.md create mode 100644 docs/reference/index.md create mode 100644 docs/reference/mcp.md create mode 100644 docs/reference/providers.md create mode 100644 docs/roadmap.md create mode 100644 docs/stylesheets/extra.css create mode 100644 mkdocs.yml diff --git a/docs/deploy/docker.md b/docs/deploy/docker.md new file mode 100644 index 0000000..ac6d46c --- /dev/null +++ b/docs/deploy/docker.md @@ -0,0 +1,68 @@ +# Run with Docker + +Don't want Python, Node, `gh`, and CodeGraph on your machine? WhyGraph ships as a self-contained +image. Your host needs **only Docker**. Install a tiny shim, then it's the same `init` and `scan` as a +native install. + +```bash +curl -fsSL https://raw.githubusercontent.com/mtrdesign/whygraph/main/scripts/install.sh | sh + +cd your-repo +whygraph init # bootstrap the WhyGraph DB + write config +whygraph scan # crawl history + refresh CodeGraph + LLM descriptions +``` + +## How the shim works + +`install.sh` drops `whygraph` and `whygraph-mcp` shims on your `PATH`. Each one runs the published +image against the current directory: + +```bash +docker run --rm -v "$PWD:/workspace" -w /workspace ghcr.io/mtrdesign/whygraph whygraph "$@" +``` + +The container is **ephemeral per command** — no compose, no `docker exec`, nothing to start or stop. +Each invocation is a fresh process against the repo you're standing in. + +- **Everything's in the image** — Python and WhyGraph, `git`, the GitHub CLI, and Node with the + CodeGraph CLI. CodeGraph indexes from the in-image binary, so there's no docker-in-docker. +- **Per-project config just works.** Each command reads the current repo's own `whygraph.toml`, + `.whygraph/`, and `.codegraph/`. +- **Files come back as yours.** The shim runs as your host user, so generated files aren't + root-owned and git sees matching ownership. + +## Credentials + +The shim passes your environment through. A GitHub token goes in `[scan].token` of the repo's +`whygraph.toml` (gitignored), and the shim also forwards `GH_TOKEN` / `GITHUB_TOKEN` plus +`ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `DEEPSEEK_API_KEY` from your environment. + +!!! warning "Never bake a token into the image" + Pass credentials at run time, never at build time. The repo's gitignored `whygraph.toml` is the + right home for a pinned token. + +## Wire your editor, still only Docker + +The MCP server is containerized too. `install.sh` drops a `whygraph-mcp` shim alongside `whygraph`, so +there's nothing extra to install. Wire your editor from inside the repo: + +```bash +whygraph init --agent claude # writes .mcp.json (also: --agent cursor / vscode / codex) +``` + +The generated config launches `whygraph-mcp` by bare command name. Your editor resolves it to the +shim, which starts a per-session container speaking MCP over stdio. It reads the repo's `.whygraph/` +and `.codegraph/` over the same `/workspace` mount the scan writes to — so the editor and the scan +share one source of truth on disk. + +## Build the image yourself + +Building locally instead of pulling — say, while developing: + +```bash +docker build -f docker/whygraph/Dockerfile -t whygraph:latest . +WHYGRAPH_IMAGE=whygraph:latest whygraph scan +``` + +`WHYGRAPH_IMAGE` overrides the image the shim runs, so you can test a local build without touching the +install. diff --git a/docs/deploy/index.md b/docs/deploy/index.md new file mode 100644 index 0000000..905450c --- /dev/null +++ b/docs/deploy/index.md @@ -0,0 +1,30 @@ +# Docker & Self-Hosting + +WhyGraph ships as a self-contained image, so there are two ways to run it. Both use the same image; +they differ in who's driving. + +
+ +- :material-laptop:{ .lg .middle } __As a local dev tool__ + + --- + + Install the Docker shim, then `init` and `scan` your repos and wire your editor — no Python or + Node on the host. This is the default install. + + [:octicons-arrow-right-24: Run with Docker](docker.md) + +- :material-server-network:{ .lg .middle } __As a service__ + + --- + + A containerized `whygraph-mcp` endpoint that real applications — not just editors — connect to + for git-based analysis of a target repo. + + [:octicons-arrow-right-24: WhyGraph as a service](service.md) + +
+ +Most people start with the local tool. Reach for the service model when you're building an +application that needs the *why* behind code — a review bot, an onboarding assistant, an internal +portal. diff --git a/docs/deploy/service.md b/docs/deploy/service.md new file mode 100644 index 0000000..4d84043 --- /dev/null +++ b/docs/deploy/service.md @@ -0,0 +1,89 @@ +# WhyGraph as a service + +Editors aren't the only thing that can talk to WhyGraph. The MCP server is a plain stdio program, so +any application that speaks MCP can connect to it for git-based analysis of a target repo. Think of a +review bot, an onboarding assistant, or an internal dev portal that needs the *why* behind a chunk of +code — not just the code. + +This page covers that model: a containerized `whygraph-mcp` endpoint a third-party app drives over +MCP, reading the same on-disk data your scan produces. + +## The shape of it + +A consuming app launches the WhyGraph image, mounts the target repo at `/workspace`, and speaks MCP +over stdio. The scan **writes** the databases; the MCP server **reads** them. One repo on disk is the +single source of truth. + +```mermaid +flowchart LR + app["Consuming app
(bot · portal · assistant)"] + mcp["whygraph-mcp
(container)"] + repo[("/workspace mount
.whygraph + .codegraph")] + + app -- "MCP over stdio" --> mcp + mcp -- "reads cached evidence + rationale" --> repo + scan["whygraph scan"] -- "writes" --> repo +``` + +The consuming app gets the full read surface over MCP: + +- **Evidence** — `whygraph_evidence_for` and `whygraph_area_history` for the commits, PRs, and issues + behind a path or symbol. +- **Rationale** — `whygraph_rationale_brief` for a structured why-this-exists card. +- **Resources** — `whygraph://commit/{sha}`, `whygraph://pr/{number}`, `whygraph://issue/{number}`, + and `whygraph://repo/overview`. + +See the [MCP surface reference](../reference/mcp.md) for exact signatures. + +## Scan first + +The server reads cached data — it doesn't crawl on demand. So the target repo must be scanned before +an app connects, or the tools have nothing to return. + +```bash +cd /path/to/target-repo +whygraph scan +``` + +!!! warning "An unscanned repo returns nothing" + `whygraph_rationale_brief` raises an error when a target maps to no scanned commit, and the + evidence tools come back empty. Run `whygraph scan` (and keep it fresh with + [git hooks](../guide/scanning.md#keep-it-fresh)) before pointing an app at the server. + +## Launch the endpoint + +The MCP server runs from the same image as everything else. Mount the target repo and run +`whygraph-mcp`: + +```bash +docker run --rm -i \ + -v "/path/to/target-repo:/workspace" -w /workspace \ + ghcr.io/mtrdesign/whygraph whygraph-mcp +``` + +The `-i` flag keeps stdin open for the MCP stdio transport. Your app spawns this command and talks +JSON-RPC to it, exactly as an editor would. The [`install.sh` shim](docker.md) wraps the same call as +a bare `whygraph-mcp` on `PATH`. + +## Credentials + +What the app needs depends on what it asks for: + +- **Reading cached evidence and rationale needs no credentials.** It's all in the mounted databases. +- **Generating a *new* rationale card needs an LLM key.** `whygraph_rationale_brief` calls the + configured provider on a cache miss. Supply the key through the environment + (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `DEEPSEEK_API_KEY`) or the repo's `whygraph.toml` + `[llm.*]` table. + +!!! info "Tokens never live in the image" + Pass credentials at run time via env or the gitignored `whygraph.toml` — never bake them into a + built image. This matches WhyGraph's own invariant for the Docker shim. + +## Current scope + +Today the server speaks MCP over **stdio, one session per process**. There's no long-running HTTP +endpoint yet — each connection is its own `docker run`. A persistent server mode with an HTTP MCP +transport is on the [roadmap](../roadmap.md), not built. + +For now, model your integration as "spawn a session, run the tools you need, let it exit" — the same +lifecycle an editor uses, driven by your app instead. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md new file mode 100644 index 0000000..ba78869 --- /dev/null +++ b/docs/getting-started/index.md @@ -0,0 +1,61 @@ +# Getting Started + +WhyGraph is a rationale layer over [CodeGraph](https://github.com/colbymchenry/codegraph). CodeGraph +maps what your code *is* — symbols, callers, callees. WhyGraph adds *why* it exists, drawn from the +history around it. + +For each chunk of code, it collects evidence from git and GitHub — commits, blame, pull requests, the +issues those PRs closed — and links it together. Then it exposes that evidence to your AI editor over +MCP, plus an on-demand rationale card (purpose, why, constraints, tradeoffs, risks) that it caches. + +You install WhyGraph once, then wire it into each repo you want it to analyze. It speaks MCP, so any +editor that does too can use it. + +## CodeGraph vs WhyGraph + +The two tools stay in their lanes: + +| Layer | Answers | Examples | +|---|---|---| +| **CodeGraph** | "What's connected to what?" | callers, callees, symbol resolution, type hierarchy | +| **WhyGraph** | "Why does this exist, and when did it change?" | evidence, rationale cards, area history | + +Run both. WhyGraph reads CodeGraph's index to resolve a symbol to a file and line range, then layers +its own history on top. + +## Prerequisites + +You don't need all of these — most are optional, and the phases that depend on them skip cleanly when +they're missing. + +- **[uv](https://docs.astral.sh/uv/)** *or* **Docker** — uv for a native install, or Docker alone for + the [container install](installation.md). With Docker, you need nothing else on your host. +- **git** — your repo history is the primary evidence source. +- **Docker** *(native installs only)* — when no `codegraph` binary is on `PATH`, `whygraph scan` runs + CodeGraph inside the WhyGraph image to index the repo. +- **[`gh` CLI](https://cli.github.com/)**, authenticated — only for GitHub repos, and only if you + enable the remote crawl. Without it, the GitHub phase is skipped. +- **`claude` CLI** *or* an LLM API key — for per-commit descriptions and rationale cards. Both phases + skip cleanly if neither is available. + +Ready to install? + +
+ +- :material-download:{ .lg .middle } __Installation__ + + --- + + Docker, PyPI, GitHub, or a local checkout — pick the path that fits. + + [:octicons-arrow-right-24: Install WhyGraph](installation.md) + +- :material-rocket-launch:{ .lg .middle } __Quickstart__ + + --- + + Init, scan, wire an editor — the happy path in four commands. + + [:octicons-arrow-right-24: Quickstart](quickstart.md) + +
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 0000000..b1ab936 --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,69 @@ +# Installation + +WhyGraph follows a one-global-install, use-anywhere model — like `npx`, but for Python. You install +the package once; that puts `whygraph` and `whygraph-mcp` on your `PATH`. Then +`whygraph init --agent ` wires each project so its editor can launch the MCP server. + +Pick the path that fits where you are. + +=== "Docker (recommended)" + + The host needs **only Docker** — no Python, Node, `gh`, or CodeGraph. A tiny shim runs everything + inside one published image. + + ```bash + curl -fsSL https://raw.githubusercontent.com/mtrdesign/whygraph/main/scripts/install.sh | sh + ``` + + This drops `whygraph` and `whygraph-mcp` shims on your `PATH`. Each wraps a + `docker run --rm -v "$PWD:/workspace" … ghcr.io/mtrdesign/whygraph` against the current repo. The + container is ephemeral per command. See [Run with Docker](../deploy/docker.md) for the full story. + +=== "PyPI" + + ```bash + uv tool install whygraph # or: pipx install whygraph + ``` + + !!! warning "Not yet published" + WhyGraph isn't on PyPI yet. Use the GitHub or local-checkout paths until v1 ships. + +=== "GitHub" + + Install straight from the repo — latest `main`, a feature branch, or a tag: + + ```bash + # Latest from main: + uv tool install "git+https://github.com/mtrdesign/whygraph.git" + + # A specific branch: + uv tool install "git+https://github.com/mtrdesign/whygraph.git@feature/some-branch" + + # A specific tag (once tagged): + uv tool install "git+https://github.com/mtrdesign/whygraph.git@v1.0.0" + ``` + + Re-running upgrades in place. To switch refs, add `--force`. `pipx` accepts the same URLs. + +=== "Local checkout" + + For contributors who want their edits to show up immediately: + + ```bash + git clone https://github.com/mtrdesign/whygraph.git + uv tool install --editable ./whygraph + ``` + + `--editable` skips the reinstall on every change. + +## Verify + +```bash +whygraph version +which whygraph-mcp +``` + +Both should resolve to your global tool install. With the Docker shim, `which whygraph-mcp` points at +the shim script on your `PATH`. + +Next: [scan a repo and wire your editor.](quickstart.md) diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md new file mode 100644 index 0000000..e41f6e0 --- /dev/null +++ b/docs/getting-started/quickstart.md @@ -0,0 +1,82 @@ +# Quickstart + +You've [installed WhyGraph](installation.md). Now point it at a repo. This is the happy path: init, +scan, wire an editor, sanity-check. + +## 1. Initialize + +From the repo you want to analyze: + +```bash +whygraph init +``` + +This creates `.whygraph/whygraph.db`, writes a commented `whygraph.example.toml`, and adds the right +`.gitignore` entries. It's idempotent — run it again any time. It does *not* index CodeGraph yet; +that's the next step. + +## 2. Scan + +```bash +whygraph scan +``` + +`scan` walks your git history, optionally crawls the remote for PRs and issues, refreshes the +CodeGraph index, and writes a per-commit LLM description. That fills `.whygraph/whygraph.db` with the +evidence WhyGraph serves. + +!!! note "The remote crawl is off by default" + A fresh scan stays git-only and needs no token, because `[scan].provider` defaults to `"off"`. To + pull PRs and issues, set `provider = "github"` (or `"auto"`) in `whygraph.toml`. + +For a fast, offline pass — no remote calls, no LLM — skip both phases: + +```bash +whygraph scan --no-remote --no-llm-descriptions +``` + +Descriptions backfill lazily later, so this is a fine way to get started quickly. See +[Scanning your repo](../guide/scanning.md) for what each phase does. + +## 3. Wire your editor + +Register the MCP server with your agent. For Claude Code: + +```bash +whygraph init --agent claude +``` + +That writes `.mcp.json` at the repo root and copies the bundled assets into `.claude/`. Other agents +work the same way — `--agent cursor`, `--agent vscode`, `--agent codex`. See +[Wiring your editor](../guide/editors.md) for each one's config path. + +## 4. Sanity-check the server + +```bash +whygraph-mcp # Ctrl-C to exit +``` + +If it launches without error, your editor can launch it too. That's it — ask your assistant why a +function exists, and WhyGraph answers from history. + +## Where to next + +
+ +- :material-lightbulb-on:{ .lg .middle } __Concepts__ + + --- + + Evidence, rationale cards, and the CodeGraph split. + + [:octicons-arrow-right-24: Concepts](../guide/concepts.md) + +- :material-connection:{ .lg .middle } __Using WhyGraph__ + + --- + + How an agent calls the tools mid-task. + + [:octicons-arrow-right-24: MCP usage](../guide/mcp-usage.md) + +
diff --git a/docs/guide/concepts.md b/docs/guide/concepts.md new file mode 100644 index 0000000..b5b8cfa --- /dev/null +++ b/docs/guide/concepts.md @@ -0,0 +1,59 @@ +# Concepts + +WhyGraph has two core ideas: **evidence** (the raw history behind code) and **rationale** (an +LLM-synthesized explanation built from that evidence). Understand these two, and the rest of the tool +follows. + +## Evidence + +Evidence is the factual record: the commits that touched a chunk of code, the blame behind each line, +the pull requests that merged it, and the issues those PRs closed. WhyGraph collects it during +[`scan`](scanning.md) and links it together, so one lookup returns the whole chain. + +You reach evidence two ways, because there are two questions to ask: + +- **`whygraph_evidence_for`** — "which commits authored *these specific lines*?" Line-blame-driven and + anchored to HEAD. Best when you have a precise range or a symbol. +- **`whygraph_area_history`** — "which commits ever touched *this file*, or anything that became this + file?" It walks the rename chain, so it reaches code that's since been deleted, moved, or fully + rewritten — commits that blame physically can't surface. + +The two reinforce each other. Evidence keeps line-level precision; area history reaches further back. + +## Rationale cards + +A rationale card is WhyGraph's answer to "why does this exist?" It takes the evidence bundle, hands it +to the configured LLM, and gets back a structured card with exactly five fields: + +- **purpose** — what this code is for. +- **why** — why it was written this way. +- **constraints** — what it must preserve. +- **tradeoffs** — what was given up, and for what. +- **risks** — what could break if you change it. + +The card comes back with provenance too — `model`, `provider`, `cached_at` — and an `evidence_count` +summarizing how many commits, PRs, and issues fed it. + +!!! note "Five fields, no score" + A card carries those five narrative fields and nothing more. There's no confidence score — the + rationale is the evidence-grounded explanation, full stop. + +### Caching + +Cards are **persistently cached**, keyed by content — the target, the evidence bundle, the provider, +and the model. Generate a card once and the next identical lookup is a sub-second database read. Change +the underlying code (so the evidence shifts) and the next call regenerates. You pay the LLM cost only +when something actually changed. + +## The CodeGraph split + +WhyGraph sits on top of CodeGraph and stays out of its lane: + +| Layer | Owns | +|---|---| +| **CodeGraph** | "what's connected to what" — callers, callees, `find_symbols`, type hierarchy | +| **WhyGraph** | "why it exists and when it changed" — evidence, rationale, area history | + +When you target a symbol by `qualified_name`, WhyGraph asks CodeGraph to resolve it to a file and line +range, then layers its own history on top. It exposes no graph-traversal tools of its own — for that, +call CodeGraph directly. diff --git a/docs/guide/editors.md b/docs/guide/editors.md new file mode 100644 index 0000000..5db2ea5 --- /dev/null +++ b/docs/guide/editors.md @@ -0,0 +1,58 @@ +# Wiring your editor + +`whygraph-mcp` is a standalone MCP server, so any agent that speaks MCP can use it. +`whygraph init --agent X` writes the right config to the right place for each one. + +Run it from the repo you want WhyGraph to analyze: + +```bash +whygraph init --agent claude +``` + +## Supported agents + +Four agents are supported. **All of them are project-scoped** — the config file is written or merged +inside the repo, so you can commit it and every teammate's editor picks it up. + +| `--agent` | Editor | Config file | +|---|---|---| +| `claude` | Claude Code | `.mcp.json` (repo root) | +| `cursor` | Cursor | `.cursor/mcp.json` | +| `vscode` (alias `copilot`) | VS Code / GitHub Copilot | `.vscode/mcp.json` | +| `codex` | OpenAI Codex | `.codex/config.toml` | + +Run `whygraph init --list-agents` to print these paths for your own checkout. + +The generated config launches `whygraph-mcp` by bare command name, so the same checked-in file works +for everyone who has WhyGraph installed — no absolute paths to scrub. + +## Claude Code assets + +`--agent claude` does one extra thing: it copies a bundled asset tree into `.claude/`. Re-running +leaves your existing files alone; pass `--force` to overwrite them. + +```bash +whygraph init --agent claude --no-install-assets # MCP wiring only, skip the .claude/ copy +whygraph init --agent claude --force # overwrite existing .claude/ files +``` + +## Useful flags + +| Flag | What it does | +|---|---| +| `--print` | Print the MCP snippet to stdout instead of writing any file. Good for pasting by hand. | +| `--list-agents` | List supported agents and their config paths, then exit. | +| `--install-assets / --no-install-assets` | Copy (or skip) the agent's bundled assets. Default: copy. No-op for agents with no asset tree. | +| `--skip-preflight` | Skip the host-tool diagnostics. For known-good scripted environments. | +| `--force` | Overwrite existing asset files in the destination directory. | + +## Verify + +After wiring, confirm the server launches: + +```bash +whygraph-mcp # Ctrl-C to exit +``` + +If it starts cleanly, your editor can start it too. Next, see how an agent +[actually calls the tools](mcp-usage.md). diff --git a/docs/guide/index.md b/docs/guide/index.md new file mode 100644 index 0000000..b251d67 --- /dev/null +++ b/docs/guide/index.md @@ -0,0 +1,46 @@ +# User Guide + +You've scanned a repo and wired an editor. This guide explains how the pieces fit together — and how +to get the most out of each. + +The flow is simple. You **scan** a repo to build the evidence database and refresh the CodeGraph +index. Your editor launches the **MCP server**, which reads that database. As you work, the server's +**tools, resources, and prompts** answer "why does this code exist?" from history. + +Start with the concepts, then dig into whichever piece you need. + +
+ +- :material-lightbulb-on:{ .lg .middle } __Concepts__ + + --- + + Evidence vs rationale, and how WhyGraph splits work with CodeGraph. + + [:octicons-arrow-right-24: Concepts](concepts.md) + +- :material-radar:{ .lg .middle } __Scanning your repo__ + + --- + + The scan phases, every flag, and the keep-fresh git hooks. + + [:octicons-arrow-right-24: Scanning](scanning.md) + +- :material-application-edit:{ .lg .middle } __Wiring your editor__ + + --- + + Per-agent setup for Claude Code, Cursor, VS Code, and Codex. + + [:octicons-arrow-right-24: Editors](editors.md) + +- :material-connection:{ .lg .middle } __Using WhyGraph (MCP)__ + + --- + + How an agent calls the tools, resources, and prompts mid-task. + + [:octicons-arrow-right-24: MCP usage](mcp-usage.md) + +
diff --git a/docs/guide/mcp-usage.md b/docs/guide/mcp-usage.md new file mode 100644 index 0000000..ae28dde --- /dev/null +++ b/docs/guide/mcp-usage.md @@ -0,0 +1,50 @@ +# Using WhyGraph (MCP) + +Once your editor is wired, WhyGraph works through MCP — your assistant calls its tools mid-task, the +way it would any other tool. This page shows what's available and when to reach for each. For exact +signatures, see the [MCP reference](../reference/mcp.md). + +## The tools + +Three tools cover the two questions — evidence and rationale. + +**Reaching for history?** Use the evidence tools: + +- `whygraph_evidence_for(path, line_start, line_end | qualified_name, limit=20)` — the commits, PRs, + and issues behind a specific range or symbol. Line-blame-driven, anchored to HEAD. +- `whygraph_area_history(path, limit=20, include_renames=True)` — every commit that touched a file or + its rename predecessors. Use it when the code moved, got deleted, or was rewritten and blame comes + up short. + +**Reaching for the *why*?** Use the rationale tool: + +- `whygraph_rationale_brief(path, line_start, line_end | qualified_name)` — a structured card: + purpose, why, constraints, tradeoffs, risks. Cached, so a repeat call is instant. + +A typical flow: before editing a function, your assistant calls `whygraph_rationale_brief` to ground +itself in intent. If the card comes back thin, it falls back to `whygraph_evidence_for` for the raw +history. + +## The resources + +Read-only JSON, addressed by URI — handy when you already know the commit, PR, or issue: + +- `whygraph://commit/{sha}` — a commit and the PRs that contain it. +- `whygraph://pr/{number}` — a PR and the issues it closes. +- `whygraph://issue/{number}` — an issue and the PRs that close it. +- `whygraph://repo/overview` — repo-level summary: counts, scan freshness, coverage, top contributors. + +## The prompts + +Prompts are ready-made recipes that wire the tools into a workflow: + +- `whygraph_pre_edit_brief` — gather rationale and history before you edit, so the change respects + constraints and avoids known risks. +- `whygraph_why_was_this_written` — recover the original intent behind a chunk of code. +- `whygraph_triage_commit` — summarize what one commit did and why, from its PR and closing issues. + +## What WhyGraph won't do + +WhyGraph has **no graph-traversal tools** — no callers, no callees, no symbol search. That's +CodeGraph's job, on purpose. When your assistant needs "who calls this?", it asks CodeGraph directly. +WhyGraph stays focused on why the code exists and how it got here. diff --git a/docs/guide/scanning.md b/docs/guide/scanning.md new file mode 100644 index 0000000..ab05390 --- /dev/null +++ b/docs/guide/scanning.md @@ -0,0 +1,75 @@ +# Scanning your repo + +`whygraph scan` builds the evidence database. It's the command you run after `init`, and again +whenever you want WhyGraph current. It's idempotent — each run picks up new commits and backfills +what's missing. + +```bash +whygraph scan +``` + +## What a scan does + +A scan runs several phases: + +1. **Git crawl** — walks first-parent history and records commits, authors, and blame. +2. **Remote crawl** *(optional)* — pulls PRs and issues per `[scan].provider`, and links them to + commits. Off unless you enable a provider. +3. **CodeGraph index refresh** — `codegraph init -i` on the first run, `codegraph sync` after. Runs + concurrently with the crawl. A failure here warns rather than aborting, since only the rationale + and evidence *tools* need CodeGraph. +4. **LLM descriptions** — writes a short description of each commit's diff with the configured + provider. + +It also handles **squash-merge recovery**: when a PR was squash-merged, `--pr-origins` does one +targeted `git fetch` of the PR's original head, so its feature-branch commits enrich the evidence +without polluting area history. + +## Flags + +| Flag | Default | What it does | +|---|---|---| +| `--no-llm-descriptions` | off | Skip the per-commit LLM phase. Git and GitHub crawlers still run; descriptions backfill lazily and on a later full scan. | +| `--codegraph / --no-codegraph` | on | Refresh the CodeGraph index concurrently with the crawl. | +| `--codegraph-image TEXT` | pinned tag | Override the Docker image for the CodeGraph fallback. Ignored when a local `codegraph` binary is found. | +| `--remote / --no-remote` | on | Crawl the remote for PRs and issues per `[scan].provider`. `--no-remote` is a fast, offline, token-free scan. | +| `--pr-origins / --no-pr-origins` | on | Recover a squash-merged PR's original commits. Needs the network, so it's skipped under `--no-remote`. | + +A common fast pass while iterating: + +```bash +whygraph scan --no-remote --no-llm-descriptions +``` + +!!! tip "Lazy backfill" + Skipping descriptions doesn't lose them. The MCP tools backfill a commit's description on demand + when they need it, and a later full `whygraph scan` fills in the rest. Start fast, enrich later. + +## Keep it fresh + +Don't want to re-scan by hand? Install git hooks once, and new commits refresh WhyGraph and CodeGraph +on the fly: + +```bash +whygraph hooks install +``` + +This wires `post-commit`, `post-merge`, and `post-rewrite` to run +`whygraph scan --no-remote --no-llm-descriptions` **in the background**. Git history and a CodeGraph +`sync` only — no LLM, no remote calls — so commits stay instant and the scan is offline and +token-free. + +The hooks are detached and single-flight: rapid commits coalesce instead of stacking, and the latest +`HEAD` always wins. An existing hook of your own is appended to behind a sentinel guard, never +overwritten. + +Check or remove them any time: + +```bash +whygraph hooks status +whygraph hooks uninstall +``` + +!!! note "Hooks stay fast on purpose" + The hooks deliberately skip the remote and LLM phases so they never slow a commit. For PRs, + issues, and fresh descriptions, run a full `whygraph scan` now and then. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..beb4859 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,87 @@ +--- +hide: + - navigation + - toc +--- + +# WhyGraph + +

+Explains why code exists, not just what it does. +

+ +A rationale layer over [CodeGraph](https://github.com/colbymchenry/codegraph). It mines your git +history and GitHub for the story behind each line — the commits, pull requests, and issues that put +it there — and serves that story to any AI editor over MCP. + +[Get started](getting-started/quickstart.md){ .md-button .md-button--primary } +[View on GitHub](https://github.com/mtrdesign/whygraph){ .md-button } + +--- + +
+ +- :material-history:{ .lg .middle } __Evidence from your history__ + + --- + + For any chunk of code, WhyGraph pulls the commits that touched it, the blame behind each line, + the PRs that merged it, and the issues those PRs closed — already linked together. + + [:octicons-arrow-right-24: Concepts](guide/concepts.md) + +- :material-card-text-outline:{ .lg .middle } __Rationale cards__ + + --- + + Ask why a symbol exists and get a structured card: purpose, why, constraints, tradeoffs, and + risks. Each card is cached, so the second lookup is instant. + + [:octicons-arrow-right-24: Using WhyGraph](guide/mcp-usage.md) + +- :material-connection:{ .lg .middle } __MCP-native__ + + --- + + `whygraph-mcp` is a standard MCP server over stdio. Claude Code, Cursor, VS Code, Codex — any + editor that speaks MCP can call it. One command wires each project. + + [:octicons-arrow-right-24: Wire your editor](guide/editors.md) + +- :material-docker:{ .lg .middle } __Only Docker required__ + + --- + + No Python, Node, `gh`, or CodeGraph on your host. A tiny shim runs everything inside one image, + ephemeral per command. Install, init, scan — done. + + [:octicons-arrow-right-24: Run with Docker](deploy/docker.md) + +- :material-graph-outline:{ .lg .middle } __Composes with CodeGraph__ + + --- + + CodeGraph answers "what's connected to what". WhyGraph answers "why it exists and when it + changed". Run both; each stays focused on its own job. + + [:octicons-arrow-right-24: Getting started](getting-started/index.md) + +- :material-server-network:{ .lg .middle } __Git analysis as a service__ + + --- + + The MCP server isn't just for editors. Real applications can connect to it for git-based + analysis of a target repo, reading the same cached data your scan writes. + + [:octicons-arrow-right-24: WhyGraph as a service](deploy/service.md) + +
+ +## Who it's for + +You're dropping into an unfamiliar codebase, or editing code you wrote months ago and no longer +remember. The *what* is in front of you; the *why* is buried in history. WhyGraph surfaces that why +right where your AI assistant works, so an edit respects the original intent instead of rediscovering +it the hard way. + +Ready? [Start with the Quickstart.](getting-started/quickstart.md) diff --git a/docs/reference/cli.md b/docs/reference/cli.md new file mode 100644 index 0000000..d047eaf --- /dev/null +++ b/docs/reference/cli.md @@ -0,0 +1,95 @@ +# CLI reference + +Every WhyGraph command and its flags. Run `whygraph --help` to see the same text from your +own install. There are five commands. + +```console +$ whygraph --help +Commands: + analyze Describe a commit's diff with the configured LLM. + hooks Manage opt-in git hooks that auto-rescan on new commits. + init Initialize the WhyGraph database under .whygraph/whygraph.db. + scan Run the source crawlers, then describe each commit with the LLM. + version Print installed whygraph version. +``` + +## `whygraph version` + +Print the installed package version. No options. + +```bash +whygraph version +``` + +## `whygraph init` + +Bootstrap the WhyGraph database under `.whygraph/whygraph.db`, write a committable +`whygraph.example.toml` documenting every tunable, and add the right `.gitignore` entries. It's +idempotent — re-running on an initialized project just confirms both databases are present. + +`init` does **not** index CodeGraph. That happens on [`scan`](#whygraph-scan). + +With `--agent X`, it also wires the WhyGraph MCP server into that agent's config. All supported +agents are project-scoped, so the config file is written inside the repo. + +| Option | Description | +|---|---| +| `--agent [claude\|codex\|copilot\|cursor\|vscode]` | Wire the MCP server into the named agent's config. | +| `--print` | Print the MCP snippet to stdout instead of writing any config file. | +| `--list-agents` | List supported agents (with config-file paths) and exit. | +| `--install-assets / --no-install-assets` | Copy the chosen agent's bundled assets into the project. Default: enabled. No-op for agents that ship no asset tree. | +| `--skip-preflight` | Skip the host-tool diagnostics that normally run first. For known-good scripted environments. | +| `--force` | When installing assets, overwrite existing files in the agent's destination directory. | + +See [Wiring your editor](../guide/editors.md) for the per-agent paths. + +## `whygraph scan` + +Run the source crawlers, then describe each commit with the configured LLM. This is the command that +populates `.whygraph/whygraph.db` and refreshes the CodeGraph index. It's idempotent — re-running +picks up new commits and backfills what's missing. + +| Option | Default | Description | +|---|---|---| +| `--no-llm-descriptions` | off | Skip the per-commit LLM description phase. The git and GitHub crawlers still run; descriptions backfill lazily on demand and on a later full scan. | +| `--codegraph / --no-codegraph` | on | Refresh the CodeGraph index concurrently with the crawl — `codegraph sync` when an index exists, `codegraph init -i` on first run. A failure here warns rather than aborting. | +| `--codegraph-image TEXT` | pinned tag | Override the Docker image used for the CodeGraph refresh fallback. Ignored when a local `codegraph` binary is found. | +| `--remote / --no-remote` | on | Crawl the source-control remote (GitHub PRs / issues) per `[scan].provider`. `--no-remote` skips it for a fast, offline, token-free scan. | +| `--pr-origins / --no-pr-origins` | on | Recover a squash-merged PR's original feature-branch commits via one targeted `git fetch`. Needs the network, so it's skipped under `--no-remote`. | + +See [Scanning your repo](../guide/scanning.md) for what each phase does. + +## `whygraph analyze` + +Describe a single commit's diff with the configured LLM and **print** the result. Unlike `scan`, it +doesn't persist anything. + +```bash +whygraph analyze [BASELINE] +``` + +`TARGET` is the commit being analyzed. With no `BASELINE`, it's compared to its parent; with a +`BASELINE`, the diff analyzed is `git diff BASELINE..TARGET`. + +!!! note "Scan first" + Every commit named on the command line must already exist in the WhyGraph database. Run + `whygraph scan` before `whygraph analyze`. + +## `whygraph hooks` + +Manage opt-in git hooks that auto-rescan on new commits. There's no daemon — the hooks run a fast, +background, offline scan as you commit. + +| Subcommand | Description | +|---|---| +| `install` | Install the auto-rescan hooks into the current repository. Idempotent and non-clobbering — it appends to a foreign hook behind a sentinel guard. | +| `status` | Report whether the auto-rescan hooks are installed. | +| `uninstall` | Remove the auto-rescan hooks, leaving any foreign hook content intact. | + +```bash +whygraph hooks install +``` + +The hooks wire `post-commit`, `post-merge`, and `post-rewrite` to run +`whygraph scan --no-remote --no-llm-descriptions` in the background. See +[Keep it fresh](../guide/scanning.md#keep-it-fresh) for the details. diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md new file mode 100644 index 0000000..e160dc1 --- /dev/null +++ b/docs/reference/configuration.md @@ -0,0 +1,110 @@ +# Configuration + +WhyGraph reads an optional `whygraph.toml` at your repo root. Every field has a built-in default, so +an unedited file behaves exactly as if none were present. `whygraph init` scaffolds a fully-commented +`whygraph.example.toml` for you — copy it to `whygraph.toml` and edit what you need. + +!!! warning "`whygraph.toml` is gitignored — never commit a token" + `init` adds `whygraph.toml` to `.gitignore` precisely because it can hold API keys. Keep it that + way. Use `whygraph.example.toml` (committed) for documentation, `whygraph.toml` (ignored) for + secrets. + +## The full tree + +The values shown are the defaults. + +```toml +log_level = "INFO" # DEBUG | INFO | WARN | ERROR | CRITICAL + +[scan] +max_workers = 2 # parallel LLM calls in the diff-analyzer crawler +provider = "off" # source-control backend for the PR/issue crawl: + # "off" — skip the remote crawl (default) + # "github" — pull PRs/issues from the GitHub remote + # "auto" — detect from the remote URL (github only, for now) +remote = "origin" # git remote whose URL is inspected for provider="auto" +# token = "ghp_..." # GitHub token for the gh CLI. Default: read GH_TOKEN / + # GITHUB_TOKEN from env (or an existing `gh auth login`). + +[analyze] +provider = "anthropic" # which [llm.*] adapter writes per-commit descriptions +# model = "claude-haiku-4-5" # override the provider's model for analysis only +# max_diff_chars = 50000 # diff truncated past this length before prompting +# large_commit_file_count = 30 # commits touching more files are described per-file on demand +# pr_origin_min_commits = 5 # recover a squash-merged PR's original commits past this size +# timeout_sec = 60 # per-call timeout; default: the adapter's own + +[rationale] +provider = "anthropic" # which [llm.*] adapter writes the rationale card +# model = "claude-haiku-4-5" # override the provider's model for rationale only +# timeout_sec = 60 # per-call timeout; default: the adapter's own +# pr_roster_max_commits = 30 # squashed-commit headlines shown per PR in the prompt +# pr_discussion_max_comments = 20 # PR comments shown per PR in the prompt +# pr_comment_max_chars = 500 # each PR comment clipped to this length + +# Override default DB locations (resolved relative to this file): +# whygraph_db = ".whygraph/whygraph.db" +# codegraph_db = ".codegraph/codegraph.db" + +# Optional rotating file log. Console (stderr) logging is always on. +# [logging] +# file = ".whygraph/logs/whygraph.log" +# level = "DEBUG" # default: inherit top-level log_level +# max_bytes = 5_000_000 +# backup_count = 3 + +[llm.anthropic] +model = "claude-opus-4-7" +# api_key = "sk-ant-..." # default: read ANTHROPIC_API_KEY from env +timeout_sec = 60 + +[llm.openai] +model = "gpt-4o" +# api_key = "sk-..." # default: read OPENAI_API_KEY from env +# base_url = "..." # default: https://api.openai.com/v1 +timeout_sec = 60 + +[llm.deepseek] +model = "deepseek-chat" +# api_key = "sk-..." # default: read DEEPSEEK_API_KEY from env +timeout_sec = 60 + +[llm.ollama] +model = "llama3" +# host = "http://localhost:11434" +timeout_sec = 120 + +# `claude_cli` (Python attribute) and `claude-cli` (TOML idiom) both parse. +[llm.claude_cli] +model = "claude-opus-4-7" +# api_key = "sk-ant-..." # default: subscription billing (strips the env var) +timeout_sec = 120 +``` + +## Section by section + +| Section | What it controls | +|---|---| +| top-level `log_level` | Console log verbosity. | +| `[scan]` | The crawl: parallelism, which remote provider to use, the git remote name, and an optional pinned GitHub token. | +| `[analyze]` | The per-commit LLM diff descriptions written during `scan` — provider, model, and the truncation / per-file thresholds. | +| `[rationale]` | The `whygraph_rationale_brief` card — provider, model, and how much of a squash-merged PR is rendered into the prompt. | +| `whygraph_db` / `codegraph_db` | Override either database path. | +| `[logging]` | An optional rotating file log, in addition to the always-on stderr log. | +| `[llm.*]` | Per-provider client settings — model, key, timeout, and `base_url` / `host` where relevant. | + +## Environment variables + +Omit an `api_key` from an `[llm.*]` table and WhyGraph reads the standard environment variable +instead. + +| Variable | Used for | +|---|---| +| `ANTHROPIC_API_KEY` | The `anthropic` LLM adapter. | +| `OPENAI_API_KEY` | The `openai` LLM adapter. | +| `DEEPSEEK_API_KEY` | The `deepseek` LLM adapter. | +| `GH_TOKEN` / `GITHUB_TOKEN` | The `gh` CLI during the remote crawl, when `[scan].token` is unset. | + +!!! tip "Provider keys degrade gracefully" + Missing a key for the analysis or rationale phase isn't fatal — that phase skips, and the rest of + the scan still runs. Descriptions and cards backfill once a credential is available. diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 0000000..f38ea0b --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,40 @@ +# Reference + +The exact surface — every command, tool, resource, and config key. This section is hand-written +against the running code, so it stays in step with what `whygraph` actually does. Pick a page: + +
+ +- :material-console:{ .lg .middle } __CLI__ + + --- + + Every command and flag, straight from `--help`: `init`, `scan`, `analyze`, `hooks`, `version`. + + [:octicons-arrow-right-24: CLI reference](cli.md) + +- :material-connection:{ .lg .middle } __MCP surface__ + + --- + + The three tools, four resources, and three prompts that `whygraph-mcp` registers. + + [:octicons-arrow-right-24: MCP reference](mcp.md) + +- :material-cog:{ .lg .middle } __Configuration__ + + --- + + The full `whygraph.toml` tree and the environment variables WhyGraph reads. + + [:octicons-arrow-right-24: Configuration](configuration.md) + +- :material-source-branch:{ .lg .middle } __Providers__ + + --- + + Which source-control hosts the remote crawl supports today, and what's coming. + + [:octicons-arrow-right-24: Providers](providers.md) + +
diff --git a/docs/reference/mcp.md b/docs/reference/mcp.md new file mode 100644 index 0000000..900a9d5 --- /dev/null +++ b/docs/reference/mcp.md @@ -0,0 +1,109 @@ +# MCP surface + +`whygraph-mcp` registers three tools, four resources, and three prompts. That's the whole surface — +deliberately narrow. WhyGraph owns "why this exists and when it changed"; graph traversal +("what's connected to what") stays with CodeGraph. + +For a usage-first walkthrough of how an agent calls these mid-task, see +[Using WhyGraph](../guide/mcp-usage.md). + +## Tools + +### `whygraph_evidence_for` + +Historical evidence — commits, PRs, and closing issues — for a chunk of code. Line-blame-driven and +anchored to HEAD. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `path` | str | — | Source file path, relative to the repo root. | +| `line_start` | int | — | First line of the chunk (1-indexed, inclusive). | +| `line_end` | int | — | Last line of the chunk (1-indexed, inclusive). | +| `qualified_name` | str | — | Fully-qualified symbol name. Use instead of `path`/lines when you know the symbol. CodeGraph resolves it to a file/line range. | +| `limit` | int | `20` | Cap on the number of commits returned. | + +Returns `{ "target": {...}, "evidence": [ { "commit", "pull_requests", "issues", "source" }, ... ] }`. + +### `whygraph_area_history` + +Every commit that touched a file path — or any path it was renamed from. Where `evidence_for` is +line-blame-driven, `area_history` reaches commits for code that's since been deleted, moved, or +fully rewritten. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `path` | str | *required* | The file path, as it appears at HEAD (or any commit — the rename chain is bidirectional). | +| `limit` | int | `20` | Cap on commits returned, newest first. | +| `include_renames` | bool | `true` | Walk the `renamed_from` chain to include commits that touched historical names. | + +Returns `{ "path", "include_renames", "evidence": [...] }`, using the same evidence shape as +`whygraph_evidence_for`. + +### `whygraph_rationale_brief` + +Generate a structured rationale card explaining why a chunk of code exists. It gathers the evidence, +optionally enriches it with CodeGraph symbol context, and asks the configured LLM to synthesize the +card. Cards are cached, so a repeat call on unchanged code is a fast database read. + +| Parameter | Type | Description | +|---|---|---| +| `path` | str | Source file path, relative to the repo root. | +| `line_start` | int | First line of the chunk. | +| `line_end` | int | Last line of the chunk. | +| `qualified_name` | str | Fully-qualified symbol name, instead of `path`/lines. | + +Returns the card: + +```json +{ + "target": { "...": "..." }, + "purpose": "...", + "why": "...", + "constraints": ["..."], + "tradeoffs": ["..."], + "risks": ["..."], + "model": "claude-opus-4-7", + "provider": "anthropic", + "cached_at": "2026-06-11T12:00:00Z", + "evidence_count": { "commits": 8, "prs": 3, "issues": 2 } +} +``` + +The card carries exactly five narrative fields — **purpose, why, constraints, tradeoffs, risks** — +plus provenance (`model`, `provider`, `cached_at`) and an `evidence_count` summary. + +!!! note "Calls the LLM on a cache miss" + On a miss, this hits the configured provider and may take several seconds. A hit is sub-second. + Generation needs a credential — see [Configuration](configuration.md). + +## Resources + +Read-only, JSON, addressed by URI. + +| URI | Name | Description | +|---|---|---| +| `whygraph://commit/{sha}` | `whygraph_commit` | A scanned commit and the pull requests that contain it (closing issues not inlined). | +| `whygraph://pr/{number}` | `whygraph_pull_request` | A pull request and the issues it closes. Includes full `commit_titles` and `comments`. | +| `whygraph://issue/{number}` | `whygraph_issue` | An issue and the pull requests that close it. | +| `whygraph://repo/overview` | `whygraph_repo_overview` | Repo-level summary: row counts, commit date range, scan freshness, LLM-description coverage, top contributors. | + +## Prompts + +Orchestration recipes that wire the tools into a workflow. + +| Name | Title | Arguments | What it does | +|---|---|---|---| +| `whygraph_pre_edit_brief` | Pre-edit brief | `path` / `line_start` / `line_end` / `qualified_name` | Before you edit, gather rationale and history so the edit respects constraints and avoids known risks. | +| `whygraph_why_was_this_written` | Why was this written? | `path` / `line_start` / `line_end` / `qualified_name` | Recover the original intent behind a chunk of code from its commits, PRs, and closing issues. | +| `whygraph_triage_commit` | Triage a commit | `sha` | Summarize what one commit did and why, using its linked PR and closing issues. | + +## Composition with CodeGraph + +WhyGraph exposes no graph-traversal tools on purpose. The split: + +| Layer | Owns | +|---|---| +| **CodeGraph** | "what is connected to what" — callers, callees, symbol resolution, type hierarchy. | +| **WhyGraph** | "why does this exist and when did it change" — evidence, rationale, history. | + +For traversal mid-conversation, call CodeGraph's own tools directly. diff --git a/docs/reference/providers.md b/docs/reference/providers.md new file mode 100644 index 0000000..9220dde --- /dev/null +++ b/docs/reference/providers.md @@ -0,0 +1,48 @@ +# Source control providers + +WhyGraph's remote crawl enriches your scan with pull requests and issues. You choose the backend with +`[scan].provider` in `whygraph.toml`. Today there's one supported provider — GitHub — with others on +the way. + +## GitHub — supported + +Set the provider to `github` to pull PRs and issues from the GitHub remote: + +```toml +[scan] +provider = "github" +``` + +Or let WhyGraph detect it from your remote URL: + +```toml +[scan] +provider = "auto" # detect from the remote (github only, for now) +remote = "origin" # the git remote whose URL is inspected +``` + +The crawl uses the `gh` CLI, so you need it authenticated. Provide a token one of three ways: + +- An existing `gh auth login`. +- `GH_TOKEN` or `GITHUB_TOKEN` in your environment. +- `[scan].token` in `whygraph.toml` — handy when one container scans repos across different orgs. + +!!! note "Off by default" + `provider` defaults to `"off"`, so a fresh scan stays git-only and needs no token. Opt into the + remote crawl by setting `github` or `auto`. You can also skip it per-run with + `whygraph scan --no-remote`. + +## Upcoming + +Other hosts are planned, not yet built. Until they land, point WhyGraph at a GitHub remote or run +git-only with `provider = "off"`. + +| Provider | Status | +|---|---| +| GitHub | Supported | +| Azure DevOps | Upcoming | +| GitLab | Upcoming | +| Forgejo | Upcoming | +| Others | Under consideration | + +See the [Roadmap](../roadmap.md) for the broader picture. diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..cbd4ed0 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,34 @@ +# Roadmap + +WhyGraph works today for the core loop — scan a repo, serve evidence and rationale over MCP. Here's +what's planned but not yet built. Treat everything below as direction, not a promise of dates. + +## More source-control providers + +GitHub is the only supported remote today. These are on the way: + +| Provider | Status | +|---|---| +| GitHub | Supported | +| Azure DevOps | Upcoming | +| GitLab | Upcoming | +| Forgejo | Upcoming | +| Others | Under consideration | + +Until then, run against a GitHub remote or stay git-only with `[scan].provider = "off"`. See +[Providers](reference/providers.md). + +## Deferred capabilities + +Larger, net-new pieces that aren't built yet: + +- **Cross-repo project registry** — orchestrate WhyGraph across several repos from one place. +- **Persistent / server mode** — a long-running endpoint with an HTTP MCP transport, so an app + doesn't spawn a fresh stdio session per connection. This is what unlocks the full + [service model](deploy/service.md). +- **Per-branch databases** — separate CodeGraph and WhyGraph databases per branch, so a scan on one + branch doesn't clobber another's. + +!!! info "Want to weigh in?" + These are shaped by what people actually need. Open an issue on + [GitHub](https://github.com/mtrdesign/whygraph) if one of these matters to you. diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..8d19c89 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,4 @@ +/* WhyGraph docs — brand-tuning hook. + * + * Intentionally near-empty. Material's indigo defaults carry the site for now; + * drop palette / typography overrides here when a brand identity lands. */ diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..f492822 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,99 @@ +site_name: WhyGraph +site_description: Rationale layer over CodeGraph — explains why code exists, not just what it does. +site_url: https://mtrdesign.github.io/whygraph/ # project-pages subpath (also drives social cards) +repo_url: https://github.com/mtrdesign/whygraph +repo_name: mtrdesign/whygraph +edit_uri: edit/main/docs/ +copyright: Copyright © MTR Design + +theme: + name: material + language: en + icon: + repo: fontawesome/brands/github + logo: material/graph-outline # built-in icon as logo until a brand asset exists + features: + - navigation.tabs # top-level sections as tabs + - navigation.tabs.sticky # tabs stay pinned on scroll + - navigation.sections # group pages in the sidebar + - navigation.indexes # section landing pages (the section title is itself a page) + - navigation.top # back-to-top button + - navigation.instant # SPA-like instant nav + - navigation.instant.progress # top loading bar on navigation + - navigation.tracking # URL tracks the scrolled-to anchor + - navigation.footer # prev/next footer links + - toc.follow + - search.suggest + - search.highlight + - content.code.copy # copy button on code blocks + - content.code.annotate # annotated code blocks + - content.tabs.link # linked content tabs (install variants, agents) + - content.tooltips # rich hover tooltips + - announce.dismiss # dismissible announcement bar + palette: # light/dark toggle + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: indigo + toggle: { icon: material/brightness-7, name: Switch to dark mode } + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + accent: indigo + toggle: { icon: material/brightness-4, name: Switch to light mode } + +extra_css: + - stylesheets/extra.css # near-empty hook for future brand tuning + +markdown_extensions: + - admonition # note/warning/tip callouts + - attr_list # required for grid-card homepage + - md_in_html # required for grid-card homepage + - tables + - toc: { permalink: true } + - pymdownx.highlight: { anchor_linenums: true } + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences: # required for mermaid diagrams + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: { alternate_style: true } # tabbed install/agent variants + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +plugins: + - search + - social: # Open Graph link-preview cards + cards: !ENV [CI, false] # generate only in CI (needs Cairo/Pango); skip locally + - git-revision-date-localized: # per-page "last updated" dates + enable_creation_date: true + type: timeago + fallback_to_build_date: true # don't fail --strict on uncommitted pages + +nav: + - Home: index.md + - Getting Started: + - getting-started/index.md + - Installation: getting-started/installation.md + - Quickstart: getting-started/quickstart.md + - User Guide: + - guide/index.md + - Concepts: guide/concepts.md + - Scanning your repo: guide/scanning.md + - Wiring your editor: guide/editors.md + - Using WhyGraph (MCP): guide/mcp-usage.md + - Docker & Self-Hosting: + - deploy/index.md + - Run with Docker: deploy/docker.md + - WhyGraph as a service: deploy/service.md + - Reference: + - reference/index.md + - CLI: reference/cli.md + - MCP surface: reference/mcp.md + - Configuration: reference/configuration.md + - Source control providers: reference/providers.md + - Roadmap: roadmap.md From bb36a39c4ca59a6fa648e558abe51777bdb0eae2 Mon Sep 17 00:00:00 2001 From: cvetty Date: Fri, 12 Jun 2026 00:04:15 +0300 Subject: [PATCH 3/5] docs: trim README to pointer + add Pages deploy workflow --- .github/workflows/docs.yml | 51 ++++++ README.md | 328 ++----------------------------------- docs/guide/concepts.md | 4 +- 3 files changed, 66 insertions(+), 317 deletions(-) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..6aebb43 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,51 @@ +name: docs + +# Builds the Material for MkDocs site and deploys it to GitHub Pages. +# Pages "Source" must be set to "GitHub Actions" once in repo Settings. +on: + push: + branches: [main] + paths: # only rebuild when docs inputs change + - docs/** + - mkdocs.yml + - .github/workflows/docs.yml + - pyproject.toml + workflow_dispatch: # allow manual rebuilds + +permissions: + contents: read + pages: write + id-token: write + +concurrency: # never run two deploys at once + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history → correct "last updated" dates + - name: Install Cairo/Pango for social cards + run: | + sudo apt-get update + sudo apt-get install -y libcairo2-dev libfreetype6-dev libffi-dev \ + libjpeg-dev libpng-dev libz-dev libpango-1.0-0 libpangocairo-1.0-0 + - uses: astral-sh/setup-uv@v5 + with: { enable-cache: true } + - run: uv sync --group docs + - run: uv run mkdocs build --strict # CI=true ⇒ social cards generate; --strict fails on broken links + - uses: actions/upload-pages-artifact@v3 + with: { path: site } + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index e91fc83..c0591f0 100644 --- a/README.md +++ b/README.md @@ -2,341 +2,39 @@ Rationale layer over [CodeGraph](https://github.com/colbymchenry/codegraph): explains *why* code exists, not just what it does. -For each symbol, WhyGraph collects evidence from git history and GitHub (commits, blame, PRs, closing issues, callers/callees from CodeGraph), then exposes it to AI assistants over MCP plus an on-demand rationale (purpose, why, constraints, tradeoffs, risks) with a persistent cache. +For each chunk of code, WhyGraph collects evidence from git history and GitHub — commits, blame, PRs, the issues those PRs closed — then serves it to AI editors over MCP, plus an on-demand rationale card (purpose, why, constraints, tradeoffs, risks) with a persistent cache. -> **Status:** v1.x in progress on the `feature/scan-and-scoring` branch. MCP surface (resources, tools, prompts) is functional. The `/whygraph-plan` slash command + fan-out/fan-in planner subagents shipped in v1.3. - -## Prerequisites - -- **[uv](https://docs.astral.sh/uv/)** — Python toolchain. Installs Python 3.11+ automatically. -- **git** — repo history is the primary evidence source. -- **Docker** *(native installs only)* — when no `codegraph` binary is on PATH, `whygraph scan` runs CodeGraph inside the WhyGraph image to index the repo. No host Node install needed. The pure-Docker install (below) already runs everything in that image. -- **[`gh` CLI](https://cli.github.com/)**, authenticated (`gh auth login`) — required only if your repo is on GitHub. Without it, the GitHub crawl phase is skipped silently. -- **`claude` CLI** *or* `ANTHROPIC_API_KEY` — needed for the LLM diff-description phase of `whygraph scan` and for `whygraph_rationale_brief`. Both phases skip cleanly if neither is available. The `claude` CLI defaults to your Claude.ai subscription billing. +> **📖 Full documentation → ** +> +> Installation, configuration, the CLI and MCP reference, the Docker delivery, and the service model all live there. This README is just the elevator pitch. ## Quickstart -Install WhyGraph once (see [Installation](#installation) below), then in the repository you want to analyse: - -```bash -# 1. Bootstrap WhyGraph + CodeGraph (via Docker; idempotent — re-runs are no-ops). -whygraph init - -# 2. Scan: walks git history, fetches PRs/issues, runs TF-IDF scoring, -# generates an LLM diff description per commit. Writes to -# .whygraph/whygraph.db in the current repo. -whygraph scan - -# 3. Verify the MCP server can launch. -whygraph-mcp # Ctrl-C to exit -``` - -The full scan touches every commit on the default branch. On large or remote-heavy repos you may want to bound the LLM phase only: - -```bash -# Run scan + LLM only on the 50 most recent commits. Other phases -# (git crawl, GitHub fetch, TF-IDF scoring) still cover full history. -whygraph scan --llm-recent 50 -``` - -## Installation - -WhyGraph follows a **one-global-install / use-anywhere** model — like `npx`, but for Python. You install the package once on your machine; that puts the `whygraph` and `whygraph-mcp` console scripts on your `PATH`. Then `whygraph init --agent ` wires up each individual project so its agent can launch the MCP server. - -Pick whichever install path fits where the project is in its lifecycle: - -### From PyPI (stable releases) +Install WhyGraph once, then from the repo you want to analyze: ```bash -uv tool install whygraph # or: pipx install whygraph +whygraph init # bootstrap the WhyGraph DB + write config +whygraph scan # crawl history + refresh CodeGraph + LLM descriptions +whygraph init --agent claude # wire the MCP server into your editor +whygraph-mcp # sanity-check the server (Ctrl-C to exit) ``` -> **Status:** WhyGraph is not yet published to PyPI. Use one of the GitHub or local-checkout paths below until v1 ships. - -### From GitHub (latest / pre-release) - -For unreleased features on `main`, a specific feature branch, or a tag: - -```bash -# Latest from main: -uv tool install "git+https://github.com/mtrdesign/whygraph.git" - -# A specific branch (e.g. an in-flight feature): -uv tool install "git+https://github.com/mtrdesign/whygraph.git@feature/scan-and-scoring" - -# A specific tag (once tagged): -uv tool install "git+https://github.com/mtrdesign/whygraph.git@v1.3.0" -``` - -Re-running upgrades in place. To switch refs, add `--force` (or `uv tool uninstall whygraph` first). `pipx` accepts the same `git+https://…` URLs. - -### From a local checkout (contributors) - -```bash -git clone https://github.com/mtrdesign/whygraph.git -uv tool install --editable ./whygraph -``` - -`--editable` lets your local edits show up immediately, without reinstalling. - -### Verify - -```bash -whygraph version -which whygraph-mcp -``` - -Both should resolve to the global tool install (under `~/.local/bin/` or `uv`'s shim directory). - -### Wire each project - -```bash -cd /path/to/your-project -whygraph init --agent claude # Claude Code: writes .mcp.json + drops bundled assets in .claude/ -``` - -`whygraph init --agent claude` writes `.mcp.json` at the repo root and copies the bundled agent / command / skill markdown into `/.claude/agents`, `/.claude/commands`, `/.claude/skills`. The `.mcp.json` references `whygraph-mcp` by bare command name, so the same checked-in config works for every teammate who has WhyGraph installed globally — no absolute paths to scrub. Re-running is safe — pre-existing files are left alone; pass `--force` to overwrite, `--no-install-assets` to skip the asset copy entirely. - -### Migration from the Claude Code plugin - -Earlier versions shipped via the Claude Code plugin marketplace. That path is gone — there is no more `plugins/whygraph/` or `.claude-plugin/marketplace.json`, and an old `/plugin install whygraph@whygraph` will fail. To migrate: - -``` -# In your Claude Code session: -/plugin uninstall whygraph@whygraph -/plugin marketplace remove whygraph - -# Then in each project where you want WhyGraph: -whygraph init --agent claude -``` - -## Wire WhyGraph into your editor - -WhyGraph's MCP server (`whygraph-mcp`) is a standalone console script, so any LLM agent that speaks MCP can use it. `whygraph init --agent X` writes the right snippet to the right file for each supported agent. - -Run from the repo you want WhyGraph to analyse: - -```bash -whygraph init # preflight + WhyGraph DB + example config (no agent wiring) -whygraph init --skip-preflight # skip the host-tool diagnostics (CI escape hatch) -whygraph init --agent claude # all of the above + writes .mcp.json + populates .claude/ -whygraph init --agent cursor # writes .cursor/mcp.json -whygraph init --agent vscode # writes .vscode/mcp.json (alias: copilot) -whygraph init --agent codex # prints snippet for ~/.codex/config.toml -whygraph init --agent X --print # prints the MCP snippet, never writes it -whygraph init --agent claude --no-install-assets # MCP only, skip .claude/ copy -whygraph init --agent claude --force # overwrite existing .claude/* files -whygraph init --list-agents # show all supported agents + paths (no preflight, no bootstrap) -``` - -**Project-scoped agents** (Claude Code, Cursor, VS Code / Copilot) get a config file written inside the repo so you can commit it — every contributor's editor picks it up automatically. **User-scoped agents** (Codex, Claude Desktop) are print-only: the command emits the snippet and tells you where to paste it, so WhyGraph never silently edits files outside the repo. - -`--agent claude` is the only path that also installs the `/whygraph-plan` slash command, the `plan-change` skill, and the planner / researcher / synthesizer subagents — these only make sense for Claude Code, so they ship as `.claude/*` markdown rather than as part of the MCP surface. - -## Run with Docker (only Docker required) - -Don't want Python, Node, `gh`, and CodeGraph on your machine? WhyGraph ships as a self-contained image. The host needs **only Docker** — install a tiny shim, then it's the same `init` / `scan` as a native install: +The only-Docker install needs nothing but Docker on the host: ```bash curl -fsSL https://raw.githubusercontent.com/mtrdesign/whygraph/main/scripts/install.sh | sh - -cd your-repo -whygraph init # bootstrap WhyGraph DB + write config (+ optional --agent wiring) -whygraph scan # crawl history + build/refresh CodeGraph index + LLM descriptions -``` - -`install.sh` drops a `whygraph` (and `whygraph-mcp`) shim on your `PATH` that runs the published image (`ghcr.io/mtrdesign/whygraph`) against the current directory — `docker run --rm -v "$PWD:/workspace" … whygraph "$@"`. The container is ephemeral per command: no compose, no `docker exec`, nothing to start or stop. - -- **Everything is in the image** — Python + WhyGraph, `git`, the GitHub CLI, and Node + the CodeGraph CLI. CodeGraph indexes from the in-image binary, so there's no docker-in-docker. -- **Per-project config just works.** Each command runs against the current repo, reading that repo's own `whygraph.toml`, `.whygraph/`, and `.codegraph/`. Generated files are written back owned by your user. -- **GitHub token** goes in `[scan].token` of the repo's `whygraph.toml` (gitignored). The shim also passes through `GH_TOKEN` / `GITHUB_TOKEN` and `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `DEEPSEEK_API_KEY` from your environment. - -Build the image yourself instead of pulling (e.g. while developing) with `docker build -f docker/whygraph/Dockerfile -t whygraph:latest .`, then `WHYGRAPH_IMAGE=whygraph:latest whygraph scan`. - -### Use it in your editor (MCP), still only Docker - -The MCP server is containerized too — `install.sh` drops a `whygraph-mcp` shim alongside `whygraph`, so there's nothing extra to install on the host. Wire your editor from inside the repo: - -```bash -whygraph init --agent claude # writes .mcp.json (also: --agent cursor / vscode) ``` -The generated `.mcp.json` launches `whygraph-mcp` by bare command name; your editor resolves it to the shim, which starts a per-session container (`docker run -i … whygraph-mcp`) speaking MCP over stdio. It reads the repo's `.whygraph/` + `.codegraph/` over the same `/workspace` mount the scan writes to — so the editor and the scan share one on-disk source of truth. - -- Reading cached rationale / evidence needs **no credentials**. -- On-demand rationale *generation* uses `ANTHROPIC_API_KEY` from the editor's environment (the shim passes it through) or the repo's `whygraph.toml [llm.*] api_key`. - -## CLI commands - -| Command | Purpose | -|---|---| -| `whygraph version` | Print installed package version. | -| `whygraph init` | Run preflight diagnostics (git / gh / LLM credential), bootstrap the WhyGraph DB, and write the example config + `.gitignore` entries. Pass `--agent ` to also wire MCP for an editor. Idempotent. Does not index CodeGraph — that happens on `scan`. | -| `whygraph scan` | Build or refresh the CodeGraph index (`.codegraph/codegraph.db`), then walk first-parent history and populate `.whygraph/whygraph.db`: commits + GitHub PRs/issues + TF-IDF scoring + per-commit LLM diff descriptions. Idempotent. `--no-codegraph` skips the index refresh; `--no-remote` skips the PR/issue crawl for a fast, offline, git-only scan. | -| `whygraph hooks install / uninstall / status` | Opt-in git hooks (`post-commit` / `post-merge` / `post-rewrite`) that keep WhyGraph current as you commit — see [Keep it fresh automatically](#keep-it-fresh-automatically). | -| `whygraph render [--out PATH] [--open] [--depth N]` | Render a self-contained HTML viewer of the CodeGraph + WhyGraph data. Single file, vendored Cytoscape, opens with double-click. Cached rationale only. `--depth N` (1–4, default 1) caps which nodes get a populated detail block — fast first paint at default 1 (modules only); pass `--depth 4` for full data. | -| `whygraph serve [--port 8765] [--open]` | Long-running localhost viewer with on-demand rationale generation. Same UI as `render`, plus a "Generate rationale" button on uncached nodes. | -| `whygraph-mcp` | Launch the FastMCP stdio server. Referenced by the `.mcp.json` files `whygraph init --agent X` writes into each project. | - -### Keep it fresh automatically - -Don't want to re-scan by hand? Install git hooks once and new commits refresh WhyGraph + CodeGraph on the fly: - -```bash -whygraph hooks install # opt-in; uninstall / status also available -``` - -This wires `post-commit`, `post-merge`, and `post-rewrite` to run an incremental scan **in the background** — git history + a CodeGraph `sync`, with **no LLM and no remote calls**, so commits stay instant and the scan is offline and token-free (LLM descriptions still backfill lazily; run a full `whygraph scan` for PRs/issues + descriptions). Rapid commits coalesce (single-flight), and an existing hook of your own is appended to, never overwritten. The hooks call whatever `whygraph` is on your `PATH` — so they work with both the Docker shim and a native install. - -### `whygraph scan` flags - -| Flag | Default | Purpose | -|---|---|---| -| `--no-score` | off | Skip TF-IDF scoring after data collection. | -| `--no-llm-description` | off | Skip the per-commit LLM diff-description phase entirely. | -| `--anthropic-key TEXT` | unset | If set, the `claude` subprocess uses API billing with this key. If omitted, the subprocess inherits a stripped env (no `ANTHROPIC_API_KEY`), forcing Claude.ai subscription billing. | -| `--llm-workers N` | `4` | Parallel `claude` subprocesses in the LLM phase. | -| `--llm-recent N` | unbounded | Limit the LLM phase to the most recent N commits on the default branch. Other phases still cover full history. | -| `--llm-model TEXT` | `claude-sonnet-4-6` | Model used by the `claude` subprocess in the LLM phase. Also persisted to `commits.llm_description_model` per row. Use Opus on small repos for higher-quality descriptions; default is Sonnet for throughput on large scans. | - -## MCP surface - -The `.mcp.json` written by `whygraph init --agent claude` (and the equivalents for other agents) launches `whygraph-mcp`, which registers: - -### Resources - -- `whygraph://repo/overview` — counts, scan freshness, scoring + LLM coverage, top contributors. -- `whygraph://commit/{sha}` — full commit row + linked PRs + closing issues. -- `whygraph://pr/{number}` — full PR row + closing issues. -- `whygraph://issue/{number}` — full issue row + closing PRs. - -### Tools - -- **`whygraph_evidence_for`** — historical evidence (commits + PRs + closing issues) for a code chunk. Pass `(path, line_start, line_end)` or `qualified_name` (CodeGraph resolves it to a file/line range — no graph traversal). Multi-narrative output: each commit ships `llm_description` + `subject` + `body` when each clears the harshness gate. For caller/callee context, query CodeGraph or Claude Code's Explore agent separately. -- **`whygraph_search`** — LIKE-match query across commits/PRs/issues, ranked by TF-IDF. -- **`whygraph_velocity_summary`** — per-author commit velocity or per-path-prefix touch counts over a window. Author resolution goes through the `authors` identity table (replaces the old email-localpart heuristic). -- **`whygraph_window`** — generic windowed query over the scan DB. Filters: `since` / `until` (ISO date or relative shorthand `30d`/`3m`/`1y`), `kinds` (`commit` / `pr` / `issue`), `author` (login | email | name → resolved via `authors`), `path_prefix`, `label`, `state` (`merged` | `open` | `closed`). Returns time-ordered rows; the data spine for the analytics prompts below. -- **`whygraph_rationale_brief`** — generates the 5-section rationale card (purpose / why / constraints / tradeoffs / risks + confidence) by feeding the evidence bundle to a `claude` subprocess. **Cached** in the scan DB by `(target + bundle content + model + prompt version)` — re-invocation on unchanged code is a sub-millisecond DB read. Pass `force_refresh=True` to bypass. - -### Prompts - -Orchestration recipes that wire the tools above into common workflows: - -- `explain_change` — pre-edit rationale for a code chunk. -- `debug_history` — find historical candidate causes for a bug symptom. -- `team_pulse` — per-author + per-path velocity over a rolling window. -- `changelog(since, until, scope?)` — themed markdown changelog of merged PRs in a date window. -- `feature_timeline(since, until)` — Mermaid `timeline` of merged PRs and issues opened in the window. -- `user_profile(identity, since, until)` — per-user contribution profile (commits, PRs, areas owned, issues closed). -- `whygraph_plan(task)` — composes search → CodeGraph (for symbol resolution) → rationale_brief into an ordered implementation plan. - -### Composition with CodeGraph - -WhyGraph deliberately does not expose graph-traversal tools. The split: - -| Layer | Owns | -|---|---| -| **CodeGraph** (its MCP server / Claude's Explore agent) | "what is connected to what" — `findUsages`, `getCallers`, `find_symbols`, type hierarchy | -| **WhyGraph** | "why does this exist + when did it change" — evidence, rationale, windowed analytics | - -The `whygraph_plan` prompt and the `/whygraph-plan` slash command both explicitly delegate symbol resolution to CodeGraph. For ad-hoc traversal mid-conversation, agents should call CodeGraph's tools directly. - -### Slash command - -`/whygraph-plan [--shallow|--deep] [--no-questions]` — produces a step-by-step implementation plan grounded in CodeGraph (impact) and WhyGraph (rationale). `whygraph init --agent claude` drops three subagents under `.claude/agents/`: - -- **`whygraph-planner`** — orchestrator. Builds the working set via CodeGraph, warms the rationale cache, then either writes a plan single-pass (small scope) or fans out. -- **`whygraph-researcher`** — fan-out worker, instantiated three times in parallel with one of three dimensions: `impact` (blast radius via CodeGraph callers), `constraints_risks` (verbatim from rationale cards), `prior_art` (similar past PRs via `whygraph_search` / `whygraph_window`). -- **`whygraph-synthesizer`** — fan-in combiner. Folds the three reports into the final plan markdown with a confidence floor = lowest researcher confidence. - -Auto-mode heuristic: single-pass when working set < 5 nodes OR > 60% of rationale cards are empty/low-confidence; fan-out otherwise. `--shallow` and `--deep` override. - -### Skill - -`plan-change` — auto-trigger description that nudges the user toward `/whygraph-plan` on planning-shaped prompts (e.g. *"plan how to refactor X"*, *"design a migration for Y"*). Suggestion-only; never auto-runs the planner — the slash command is the user's opt-in cost gate. - -## HTML viewer - -```bash -whygraph render --open # static, self-contained HTML -whygraph serve --open # localhost viewer with on-demand rationale -``` - -`render` writes `.whygraph/whygraph.html` — a single self-contained file (Cytoscape.js vendored inline) with three tabs: - -- **Graph** — Cytoscape view of CodeGraph nodes/edges. Nodes are coloured by hierarchy level (Modules / Classes / Methods / Leaves); the slider buttons double as a legend. Cached rationale is shown as a green border. A **level slider** in the top bar controls how deep the graph displays — defaults to "Modules" for fast first paint. Edges aggregate up to the nearest visible ancestor when deeper levels are hidden. Click a node → side panel with top contributors (from blame), per-month activity, recent commits + linked PRs/issues, and the cached rationale card if any. Nodes deeper than the rendered `--depth` show a "re-render with `--depth N`" placeholder when clicked. -- **Dashboard** — repo overview (commits/PRs/issues counts), top contributors over the last 90 days, hottest path-prefixes, monthly activity bars. -- **Authors** — list of identities (resolved through the `authors` table); click → recent activity over the last 180 days. - -`serve` starts a localhost-only HTTP server (default `127.0.0.1:8765`) with the same UI plus a "Generate rationale" button on uncached nodes. The button calls `whygraph_rationale_brief` server-side (~30s on first call, then cached); subsequent `whygraph render` runs include the newly-cached rationale in the static dump. - -## Environment variables - -| Variable | Default | Purpose | -|---|---|---| -| `WHYGRAPH_DB` | `/.whygraph/whygraph.db` | Where WhyGraph stores its evidence + rationale cache. Used by both the CLI and the MCP tools. | -| `CODEGRAPH_DB` | `/.codegraph/codegraph.db` | CodeGraph SQLite location. Required for `qualified_name` targeting on `whygraph_evidence_for`. | -| `ANTHROPIC_API_KEY` | unset | Honoured by the `claude` subprocess only when `--anthropic-key` is passed (or the equivalent tool argument). Not a runtime switch by itself. | - -## Layout - -``` -. -├── src/whygraph/ -│ ├── cli.py # `whygraph` CLI (init, scan, version) -│ ├── agents.py # agent registry + MCP snippet writers -│ ├── assets.py # .claude/ asset installer (skip-if-exists + --force) -│ ├── assets/claude-code/ # bundled Claude Code assets (shipped in the wheel) -│ │ ├── agents/ # planner / researcher / synthesizer / implementor -│ │ ├── commands/ # /rationale, /whygraph-plan, /whygraph-implement -│ │ └── skills/ # ask-why / plan-change / pre-edit / implement-plan -│ ├── init.py # WhyGraph DB + config + agent MCP wiring -│ ├── mcp_server.py # FastMCP stdio server (resources/tools/prompts) -│ ├── mcp_queries.py # composite SQL for the MCP layer -│ ├── backend.py # GraphBackend Protocol + SqliteCodegraphBackend -│ ├── llm_subprocess.py # `claude` CLI invocation helpers -│ └── scan/ -│ ├── runner.py # parallel crawler orchestration -│ ├── git.py / github.py # data sources -│ ├── db.py # WhyGraph SQLite schema + migrations -│ ├── scoring.py # TF-IDF + ValueGate -│ ├── authors.py # identity dedup + resolver -│ └── llm_descriptions.py # per-commit diff-description phase -├── tests/ -└── pyproject.toml # uv-managed -``` +See the [Getting Started guide](https://mtrdesign.github.io/whygraph/getting-started/) for every install path and the [Quickstart](https://mtrdesign.github.io/whygraph/getting-started/quickstart/) for the walkthrough. ## Develop ```bash uv sync # bootstrap .venv and install deps uv run pytest # full test suite -uv run pytest tests/test_smoke.py::test_imports # single test uv run whygraph version # CLI sanity check uv run whygraph-mcp # launch MCP server on stdio +make docs # serve the documentation site locally ``` -A `Makefile` wraps the common dev tasks; run `make` to list them — `make sync`, `make test`, `make scan`, `make db` / `make db-down`, `make inspect`. - -### Browse the databases - -WhyGraph is developed by running it against its own repo, so it helps to eyeball the two SQLite databases it touches — `.whygraph/whygraph.db` (its own evidence/rationale data) and `.codegraph/codegraph.db` (CodeGraph's symbol graph). `make db` brings up [DBGate](https://dbgate.org/) in Docker with both databases wired up as connections: - -```bash -cp docker-compose.example.yml docker-compose.yml # one-time; the copy is git-ignored -make db # DBGate at http://localhost:8081 -make db-down # stop the viewer -``` - -Both databases appear in the DBGate sidebar; the CodeGraph one is opened read-only since CodeGraph rewrites it on re-index. Toggle the dark theme in DBGate's Settings — it persists across restarts. - -### Debug the MCP server with MCP Inspector - -The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is the official web UI for poking at a stdio MCP server — list tools, call them with custom args, see raw responses, tail stderr. - -```bash -make inspect # against this checkout -make inspect REPO=/path/to/other/repo # against another repo's databases -``` - -`make inspect` needs Node ≥ 20 active — the same modern Node CodeGraph requires (`nvm use 22`). Open the printed `http://localhost:…` URL with the one-time auth token. Use **Reconnect** to pick up code changes. +A `Makefile` wraps the common dev tasks; run `make` to list them. See [`CLAUDE.md`](CLAUDE.md) for the architecture and conventions. diff --git a/docs/guide/concepts.md b/docs/guide/concepts.md index b5b8cfa..58bbd4a 100644 --- a/docs/guide/concepts.md +++ b/docs/guide/concepts.md @@ -34,8 +34,8 @@ to the configured LLM, and gets back a structured card with exactly five fields: The card comes back with provenance too — `model`, `provider`, `cached_at` — and an `evidence_count` summarizing how many commits, PRs, and issues fed it. -!!! note "Five fields, no score" - A card carries those five narrative fields and nothing more. There's no confidence score — the +!!! note "Five fields, nothing more" + A card carries those five narrative fields and nothing else — no extra score or rating. The rationale is the evidence-grounded explanation, full stop. ### Caching From 954188219749ec81872f044e5acb44e4a798cee8 Mon Sep 17 00:00:00 2001 From: cvetty Date: Fri, 12 Jun 2026 00:04:30 +0300 Subject: [PATCH 4/5] chore: gitignore Material social-plugin .cache/ --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 266a584..7dc0efe 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ plans/ # MkDocs build output (the site is built and deployed by CI, never committed). site/ + +# Material social-plugin font/image cache. +.cache/ From 69e23c752860ca9ce7d1e64aa00dd056f23381ca Mon Sep 17 00:00:00 2001 From: cvetty Date: Fri, 12 Jun 2026 00:10:18 +0300 Subject: [PATCH 5/5] docs: use plain hyphens instead of em dashes in prose --- README.md | 2 +- docs/deploy/docker.md | 8 ++++---- docs/deploy/index.md | 6 +++--- docs/deploy/service.md | 16 ++++++++-------- docs/getting-started/index.md | 22 +++++++++++----------- docs/getting-started/installation.md | 6 +++--- docs/getting-started/quickstart.md | 8 ++++---- docs/guide/concepts.md | 28 ++++++++++++++-------------- docs/guide/editors.md | 4 ++-- docs/guide/index.md | 2 +- docs/guide/mcp-usage.md | 28 ++++++++++++++-------------- docs/guide/scanning.md | 12 ++++++------ docs/index.md | 10 +++++----- docs/reference/cli.md | 10 +++++----- docs/reference/configuration.md | 18 +++++++++--------- docs/reference/index.md | 2 +- docs/reference/mcp.md | 24 ++++++++++++------------ docs/reference/providers.md | 6 +++--- docs/roadmap.md | 8 ++++---- docs/stylesheets/extra.css | 2 +- 20 files changed, 111 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index c0591f0..d2c917f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Rationale layer over [CodeGraph](https://github.com/colbymchenry/codegraph): explains *why* code exists, not just what it does. -For each chunk of code, WhyGraph collects evidence from git history and GitHub — commits, blame, PRs, the issues those PRs closed — then serves it to AI editors over MCP, plus an on-demand rationale card (purpose, why, constraints, tradeoffs, risks) with a persistent cache. +For each chunk of code, WhyGraph collects evidence from git history and GitHub - commits, blame, PRs, the issues those PRs closed - then serves it to AI editors over MCP, plus an on-demand rationale card (purpose, why, constraints, tradeoffs, risks) with a persistent cache. > **📖 Full documentation → ** > diff --git a/docs/deploy/docker.md b/docs/deploy/docker.md index ac6d46c..e8e2ea6 100644 --- a/docs/deploy/docker.md +++ b/docs/deploy/docker.md @@ -21,10 +21,10 @@ image against the current directory: docker run --rm -v "$PWD:/workspace" -w /workspace ghcr.io/mtrdesign/whygraph whygraph "$@" ``` -The container is **ephemeral per command** — no compose, no `docker exec`, nothing to start or stop. +The container is **ephemeral per command** - no compose, no `docker exec`, nothing to start or stop. Each invocation is a fresh process against the repo you're standing in. -- **Everything's in the image** — Python and WhyGraph, `git`, the GitHub CLI, and Node with the +- **Everything's in the image** - Python and WhyGraph, `git`, the GitHub CLI, and Node with the CodeGraph CLI. CodeGraph indexes from the in-image binary, so there's no docker-in-docker. - **Per-project config just works.** Each command reads the current repo's own `whygraph.toml`, `.whygraph/`, and `.codegraph/`. @@ -52,12 +52,12 @@ whygraph init --agent claude # writes .mcp.json (also: --agent cursor / vsco The generated config launches `whygraph-mcp` by bare command name. Your editor resolves it to the shim, which starts a per-session container speaking MCP over stdio. It reads the repo's `.whygraph/` -and `.codegraph/` over the same `/workspace` mount the scan writes to — so the editor and the scan +and `.codegraph/` over the same `/workspace` mount the scan writes to - so the editor and the scan share one source of truth on disk. ## Build the image yourself -Building locally instead of pulling — say, while developing: +Building locally instead of pulling - say, while developing: ```bash docker build -f docker/whygraph/Dockerfile -t whygraph:latest . diff --git a/docs/deploy/index.md b/docs/deploy/index.md index 905450c..24018e7 100644 --- a/docs/deploy/index.md +++ b/docs/deploy/index.md @@ -9,7 +9,7 @@ they differ in who's driving. --- - Install the Docker shim, then `init` and `scan` your repos and wire your editor — no Python or + Install the Docker shim, then `init` and `scan` your repos and wire your editor - no Python or Node on the host. This is the default install. [:octicons-arrow-right-24: Run with Docker](docker.md) @@ -18,7 +18,7 @@ they differ in who's driving. --- - A containerized `whygraph-mcp` endpoint that real applications — not just editors — connect to + A containerized `whygraph-mcp` endpoint that real applications - not just editors - connect to for git-based analysis of a target repo. [:octicons-arrow-right-24: WhyGraph as a service](service.md) @@ -26,5 +26,5 @@ they differ in who's driving. Most people start with the local tool. Reach for the service model when you're building an -application that needs the *why* behind code — a review bot, an onboarding assistant, an internal +application that needs the *why* behind code - a review bot, an onboarding assistant, an internal portal. diff --git a/docs/deploy/service.md b/docs/deploy/service.md index 4d84043..cae7107 100644 --- a/docs/deploy/service.md +++ b/docs/deploy/service.md @@ -3,7 +3,7 @@ Editors aren't the only thing that can talk to WhyGraph. The MCP server is a plain stdio program, so any application that speaks MCP can connect to it for git-based analysis of a target repo. Think of a review bot, an onboarding assistant, or an internal dev portal that needs the *why* behind a chunk of -code — not just the code. +code - not just the code. This page covers that model: a containerized `whygraph-mcp` endpoint a third-party app drives over MCP, reading the same on-disk data your scan produces. @@ -27,17 +27,17 @@ flowchart LR The consuming app gets the full read surface over MCP: -- **Evidence** — `whygraph_evidence_for` and `whygraph_area_history` for the commits, PRs, and issues +- **Evidence** - `whygraph_evidence_for` and `whygraph_area_history` for the commits, PRs, and issues behind a path or symbol. -- **Rationale** — `whygraph_rationale_brief` for a structured why-this-exists card. -- **Resources** — `whygraph://commit/{sha}`, `whygraph://pr/{number}`, `whygraph://issue/{number}`, +- **Rationale** - `whygraph_rationale_brief` for a structured why-this-exists card. +- **Resources** - `whygraph://commit/{sha}`, `whygraph://pr/{number}`, `whygraph://issue/{number}`, and `whygraph://repo/overview`. See the [MCP surface reference](../reference/mcp.md) for exact signatures. ## Scan first -The server reads cached data — it doesn't crawl on demand. So the target repo must be scanned before +The server reads cached data - it doesn't crawl on demand. So the target repo must be scanned before an app connects, or the tools have nothing to return. ```bash @@ -76,14 +76,14 @@ What the app needs depends on what it asks for: `[llm.*]` table. !!! info "Tokens never live in the image" - Pass credentials at run time via env or the gitignored `whygraph.toml` — never bake them into a + Pass credentials at run time via env or the gitignored `whygraph.toml` - never bake them into a built image. This matches WhyGraph's own invariant for the Docker shim. ## Current scope Today the server speaks MCP over **stdio, one session per process**. There's no long-running HTTP -endpoint yet — each connection is its own `docker run`. A persistent server mode with an HTTP MCP +endpoint yet - each connection is its own `docker run`. A persistent server mode with an HTTP MCP transport is on the [roadmap](../roadmap.md), not built. -For now, model your integration as "spawn a session, run the tools you need, let it exit" — the same +For now, model your integration as "spawn a session, run the tools you need, let it exit" - the same lifecycle an editor uses, driven by your app instead. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index ba78869..e25b3ab 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -1,11 +1,11 @@ # Getting Started WhyGraph is a rationale layer over [CodeGraph](https://github.com/colbymchenry/codegraph). CodeGraph -maps what your code *is* — symbols, callers, callees. WhyGraph adds *why* it exists, drawn from the +maps what your code *is* - symbols, callers, callees. WhyGraph adds *why* it exists, drawn from the history around it. -For each chunk of code, it collects evidence from git and GitHub — commits, blame, pull requests, the -issues those PRs closed — and links it together. Then it exposes that evidence to your AI editor over +For each chunk of code, it collects evidence from git and GitHub - commits, blame, pull requests, the +issues those PRs closed - and links it together. Then it exposes that evidence to your AI editor over MCP, plus an on-demand rationale card (purpose, why, constraints, tradeoffs, risks) that it caches. You install WhyGraph once, then wire it into each repo you want it to analyze. It speaks MCP, so any @@ -25,17 +25,17 @@ its own history on top. ## Prerequisites -You don't need all of these — most are optional, and the phases that depend on them skip cleanly when +You don't need all of these - most are optional, and the phases that depend on them skip cleanly when they're missing. -- **[uv](https://docs.astral.sh/uv/)** *or* **Docker** — uv for a native install, or Docker alone for +- **[uv](https://docs.astral.sh/uv/)** *or* **Docker** - uv for a native install, or Docker alone for the [container install](installation.md). With Docker, you need nothing else on your host. -- **git** — your repo history is the primary evidence source. -- **Docker** *(native installs only)* — when no `codegraph` binary is on `PATH`, `whygraph scan` runs +- **git** - your repo history is the primary evidence source. +- **Docker** *(native installs only)* - when no `codegraph` binary is on `PATH`, `whygraph scan` runs CodeGraph inside the WhyGraph image to index the repo. -- **[`gh` CLI](https://cli.github.com/)**, authenticated — only for GitHub repos, and only if you +- **[`gh` CLI](https://cli.github.com/)**, authenticated - only for GitHub repos, and only if you enable the remote crawl. Without it, the GitHub phase is skipped. -- **`claude` CLI** *or* an LLM API key — for per-commit descriptions and rationale cards. Both phases +- **`claude` CLI** *or* an LLM API key - for per-commit descriptions and rationale cards. Both phases skip cleanly if neither is available. Ready to install? @@ -46,7 +46,7 @@ Ready to install? --- - Docker, PyPI, GitHub, or a local checkout — pick the path that fits. + Docker, PyPI, GitHub, or a local checkout - pick the path that fits. [:octicons-arrow-right-24: Install WhyGraph](installation.md) @@ -54,7 +54,7 @@ Ready to install? --- - Init, scan, wire an editor — the happy path in four commands. + Init, scan, wire an editor - the happy path in four commands. [:octicons-arrow-right-24: Quickstart](quickstart.md) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index b1ab936..47e9741 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -1,6 +1,6 @@ # Installation -WhyGraph follows a one-global-install, use-anywhere model — like `npx`, but for Python. You install +WhyGraph follows a one-global-install, use-anywhere model - like `npx`, but for Python. You install the package once; that puts `whygraph` and `whygraph-mcp` on your `PATH`. Then `whygraph init --agent ` wires each project so its editor can launch the MCP server. @@ -8,7 +8,7 @@ Pick the path that fits where you are. === "Docker (recommended)" - The host needs **only Docker** — no Python, Node, `gh`, or CodeGraph. A tiny shim runs everything + The host needs **only Docker** - no Python, Node, `gh`, or CodeGraph. A tiny shim runs everything inside one published image. ```bash @@ -30,7 +30,7 @@ Pick the path that fits where you are. === "GitHub" - Install straight from the repo — latest `main`, a feature branch, or a tag: + Install straight from the repo - latest `main`, a feature branch, or a tag: ```bash # Latest from main: diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index e41f6e0..0b2257c 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -12,7 +12,7 @@ whygraph init ``` This creates `.whygraph/whygraph.db`, writes a commented `whygraph.example.toml`, and adds the right -`.gitignore` entries. It's idempotent — run it again any time. It does *not* index CodeGraph yet; +`.gitignore` entries. It's idempotent - run it again any time. It does *not* index CodeGraph yet; that's the next step. ## 2. Scan @@ -29,7 +29,7 @@ evidence WhyGraph serves. A fresh scan stays git-only and needs no token, because `[scan].provider` defaults to `"off"`. To pull PRs and issues, set `provider = "github"` (or `"auto"`) in `whygraph.toml`. -For a fast, offline pass — no remote calls, no LLM — skip both phases: +For a fast, offline pass - no remote calls, no LLM - skip both phases: ```bash whygraph scan --no-remote --no-llm-descriptions @@ -47,7 +47,7 @@ whygraph init --agent claude ``` That writes `.mcp.json` at the repo root and copies the bundled assets into `.claude/`. Other agents -work the same way — `--agent cursor`, `--agent vscode`, `--agent codex`. See +work the same way - `--agent cursor`, `--agent vscode`, `--agent codex`. See [Wiring your editor](../guide/editors.md) for each one's config path. ## 4. Sanity-check the server @@ -56,7 +56,7 @@ work the same way — `--agent cursor`, `--agent vscode`, `--agent codex`. See whygraph-mcp # Ctrl-C to exit ``` -If it launches without error, your editor can launch it too. That's it — ask your assistant why a +If it launches without error, your editor can launch it too. That's it - ask your assistant why a function exists, and WhyGraph answers from history. ## Where to next diff --git a/docs/guide/concepts.md b/docs/guide/concepts.md index 58bbd4a..288b253 100644 --- a/docs/guide/concepts.md +++ b/docs/guide/concepts.md @@ -12,11 +12,11 @@ the pull requests that merged it, and the issues those PRs closed. WhyGraph coll You reach evidence two ways, because there are two questions to ask: -- **`whygraph_evidence_for`** — "which commits authored *these specific lines*?" Line-blame-driven and +- **`whygraph_evidence_for`** - "which commits authored *these specific lines*?" Line-blame-driven and anchored to HEAD. Best when you have a precise range or a symbol. -- **`whygraph_area_history`** — "which commits ever touched *this file*, or anything that became this +- **`whygraph_area_history`** - "which commits ever touched *this file*, or anything that became this file?" It walks the rename chain, so it reaches code that's since been deleted, moved, or fully - rewritten — commits that blame physically can't surface. + rewritten - commits that blame physically can't surface. The two reinforce each other. Evidence keeps line-level precision; area history reaches further back. @@ -25,22 +25,22 @@ The two reinforce each other. Evidence keeps line-level precision; area history A rationale card is WhyGraph's answer to "why does this exist?" It takes the evidence bundle, hands it to the configured LLM, and gets back a structured card with exactly five fields: -- **purpose** — what this code is for. -- **why** — why it was written this way. -- **constraints** — what it must preserve. -- **tradeoffs** — what was given up, and for what. -- **risks** — what could break if you change it. +- **purpose** - what this code is for. +- **why** - why it was written this way. +- **constraints** - what it must preserve. +- **tradeoffs** - what was given up, and for what. +- **risks** - what could break if you change it. -The card comes back with provenance too — `model`, `provider`, `cached_at` — and an `evidence_count` +The card comes back with provenance too - `model`, `provider`, `cached_at` - and an `evidence_count` summarizing how many commits, PRs, and issues fed it. !!! note "Five fields, nothing more" - A card carries those five narrative fields and nothing else — no extra score or rating. The + A card carries those five narrative fields and nothing else - no extra score or rating. The rationale is the evidence-grounded explanation, full stop. ### Caching -Cards are **persistently cached**, keyed by content — the target, the evidence bundle, the provider, +Cards are **persistently cached**, keyed by content - the target, the evidence bundle, the provider, and the model. Generate a card once and the next identical lookup is a sub-second database read. Change the underlying code (so the evidence shifts) and the next call regenerates. You pay the LLM cost only when something actually changed. @@ -51,9 +51,9 @@ WhyGraph sits on top of CodeGraph and stays out of its lane: | Layer | Owns | |---|---| -| **CodeGraph** | "what's connected to what" — callers, callees, `find_symbols`, type hierarchy | -| **WhyGraph** | "why it exists and when it changed" — evidence, rationale, area history | +| **CodeGraph** | "what's connected to what" - callers, callees, `find_symbols`, type hierarchy | +| **WhyGraph** | "why it exists and when it changed" - evidence, rationale, area history | When you target a symbol by `qualified_name`, WhyGraph asks CodeGraph to resolve it to a file and line -range, then layers its own history on top. It exposes no graph-traversal tools of its own — for that, +range, then layers its own history on top. It exposes no graph-traversal tools of its own - for that, call CodeGraph directly. diff --git a/docs/guide/editors.md b/docs/guide/editors.md index 5db2ea5..abdb728 100644 --- a/docs/guide/editors.md +++ b/docs/guide/editors.md @@ -11,7 +11,7 @@ whygraph init --agent claude ## Supported agents -Four agents are supported. **All of them are project-scoped** — the config file is written or merged +Four agents are supported. **All of them are project-scoped** - the config file is written or merged inside the repo, so you can commit it and every teammate's editor picks it up. | `--agent` | Editor | Config file | @@ -24,7 +24,7 @@ inside the repo, so you can commit it and every teammate's editor picks it up. Run `whygraph init --list-agents` to print these paths for your own checkout. The generated config launches `whygraph-mcp` by bare command name, so the same checked-in file works -for everyone who has WhyGraph installed — no absolute paths to scrub. +for everyone who has WhyGraph installed - no absolute paths to scrub. ## Claude Code assets diff --git a/docs/guide/index.md b/docs/guide/index.md index b251d67..8a1ee6b 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -1,6 +1,6 @@ # User Guide -You've scanned a repo and wired an editor. This guide explains how the pieces fit together — and how +You've scanned a repo and wired an editor. This guide explains how the pieces fit together - and how to get the most out of each. The flow is simple. You **scan** a repo to build the evidence database and refresh the CodeGraph diff --git a/docs/guide/mcp-usage.md b/docs/guide/mcp-usage.md index ae28dde..c05692a 100644 --- a/docs/guide/mcp-usage.md +++ b/docs/guide/mcp-usage.md @@ -1,24 +1,24 @@ # Using WhyGraph (MCP) -Once your editor is wired, WhyGraph works through MCP — your assistant calls its tools mid-task, the +Once your editor is wired, WhyGraph works through MCP - your assistant calls its tools mid-task, the way it would any other tool. This page shows what's available and when to reach for each. For exact signatures, see the [MCP reference](../reference/mcp.md). ## The tools -Three tools cover the two questions — evidence and rationale. +Three tools cover the two questions - evidence and rationale. **Reaching for history?** Use the evidence tools: -- `whygraph_evidence_for(path, line_start, line_end | qualified_name, limit=20)` — the commits, PRs, +- `whygraph_evidence_for(path, line_start, line_end | qualified_name, limit=20)` - the commits, PRs, and issues behind a specific range or symbol. Line-blame-driven, anchored to HEAD. -- `whygraph_area_history(path, limit=20, include_renames=True)` — every commit that touched a file or +- `whygraph_area_history(path, limit=20, include_renames=True)` - every commit that touched a file or its rename predecessors. Use it when the code moved, got deleted, or was rewritten and blame comes up short. **Reaching for the *why*?** Use the rationale tool: -- `whygraph_rationale_brief(path, line_start, line_end | qualified_name)` — a structured card: +- `whygraph_rationale_brief(path, line_start, line_end | qualified_name)` - a structured card: purpose, why, constraints, tradeoffs, risks. Cached, so a repeat call is instant. A typical flow: before editing a function, your assistant calls `whygraph_rationale_brief` to ground @@ -27,24 +27,24 @@ history. ## The resources -Read-only JSON, addressed by URI — handy when you already know the commit, PR, or issue: +Read-only JSON, addressed by URI - handy when you already know the commit, PR, or issue: -- `whygraph://commit/{sha}` — a commit and the PRs that contain it. -- `whygraph://pr/{number}` — a PR and the issues it closes. -- `whygraph://issue/{number}` — an issue and the PRs that close it. -- `whygraph://repo/overview` — repo-level summary: counts, scan freshness, coverage, top contributors. +- `whygraph://commit/{sha}` - a commit and the PRs that contain it. +- `whygraph://pr/{number}` - a PR and the issues it closes. +- `whygraph://issue/{number}` - an issue and the PRs that close it. +- `whygraph://repo/overview` - repo-level summary: counts, scan freshness, coverage, top contributors. ## The prompts Prompts are ready-made recipes that wire the tools into a workflow: -- `whygraph_pre_edit_brief` — gather rationale and history before you edit, so the change respects +- `whygraph_pre_edit_brief` - gather rationale and history before you edit, so the change respects constraints and avoids known risks. -- `whygraph_why_was_this_written` — recover the original intent behind a chunk of code. -- `whygraph_triage_commit` — summarize what one commit did and why, from its PR and closing issues. +- `whygraph_why_was_this_written` - recover the original intent behind a chunk of code. +- `whygraph_triage_commit` - summarize what one commit did and why, from its PR and closing issues. ## What WhyGraph won't do -WhyGraph has **no graph-traversal tools** — no callers, no callees, no symbol search. That's +WhyGraph has **no graph-traversal tools** - no callers, no callees, no symbol search. That's CodeGraph's job, on purpose. When your assistant needs "who calls this?", it asks CodeGraph directly. WhyGraph stays focused on why the code exists and how it got here. diff --git a/docs/guide/scanning.md b/docs/guide/scanning.md index ab05390..962ad3e 100644 --- a/docs/guide/scanning.md +++ b/docs/guide/scanning.md @@ -1,7 +1,7 @@ # Scanning your repo `whygraph scan` builds the evidence database. It's the command you run after `init`, and again -whenever you want WhyGraph current. It's idempotent — each run picks up new commits and backfills +whenever you want WhyGraph current. It's idempotent - each run picks up new commits and backfills what's missing. ```bash @@ -12,13 +12,13 @@ whygraph scan A scan runs several phases: -1. **Git crawl** — walks first-parent history and records commits, authors, and blame. -2. **Remote crawl** *(optional)* — pulls PRs and issues per `[scan].provider`, and links them to +1. **Git crawl** - walks first-parent history and records commits, authors, and blame. +2. **Remote crawl** *(optional)* - pulls PRs and issues per `[scan].provider`, and links them to commits. Off unless you enable a provider. -3. **CodeGraph index refresh** — `codegraph init -i` on the first run, `codegraph sync` after. Runs +3. **CodeGraph index refresh** - `codegraph init -i` on the first run, `codegraph sync` after. Runs concurrently with the crawl. A failure here warns rather than aborting, since only the rationale and evidence *tools* need CodeGraph. -4. **LLM descriptions** — writes a short description of each commit's diff with the configured +4. **LLM descriptions** - writes a short description of each commit's diff with the configured provider. It also handles **squash-merge recovery**: when a PR was squash-merged, `--pr-origins` does one @@ -56,7 +56,7 @@ whygraph hooks install This wires `post-commit`, `post-merge`, and `post-rewrite` to run `whygraph scan --no-remote --no-llm-descriptions` **in the background**. Git history and a CodeGraph -`sync` only — no LLM, no remote calls — so commits stay instant and the scan is offline and +`sync` only - no LLM, no remote calls - so commits stay instant and the scan is offline and token-free. The hooks are detached and single-flight: rapid commits coalesce instead of stacking, and the latest diff --git a/docs/index.md b/docs/index.md index beb4859..0607132 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,8 +11,8 @@ Explains why code exists, not just what it does.

A rationale layer over [CodeGraph](https://github.com/colbymchenry/codegraph). It mines your git -history and GitHub for the story behind each line — the commits, pull requests, and issues that put -it there — and serves that story to any AI editor over MCP. +history and GitHub for the story behind each line - the commits, pull requests, and issues that put +it there - and serves that story to any AI editor over MCP. [Get started](getting-started/quickstart.md){ .md-button .md-button--primary } [View on GitHub](https://github.com/mtrdesign/whygraph){ .md-button } @@ -26,7 +26,7 @@ it there — and serves that story to any AI editor over MCP. --- For any chunk of code, WhyGraph pulls the commits that touched it, the blame behind each line, - the PRs that merged it, and the issues those PRs closed — already linked together. + the PRs that merged it, and the issues those PRs closed - already linked together. [:octicons-arrow-right-24: Concepts](guide/concepts.md) @@ -43,7 +43,7 @@ it there — and serves that story to any AI editor over MCP. --- - `whygraph-mcp` is a standard MCP server over stdio. Claude Code, Cursor, VS Code, Codex — any + `whygraph-mcp` is a standard MCP server over stdio. Claude Code, Cursor, VS Code, Codex - any editor that speaks MCP can call it. One command wires each project. [:octicons-arrow-right-24: Wire your editor](guide/editors.md) @@ -53,7 +53,7 @@ it there — and serves that story to any AI editor over MCP. --- No Python, Node, `gh`, or CodeGraph on your host. A tiny shim runs everything inside one image, - ephemeral per command. Install, init, scan — done. + ephemeral per command. Install, init, scan - done. [:octicons-arrow-right-24: Run with Docker](deploy/docker.md) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index d047eaf..add9123 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -25,7 +25,7 @@ whygraph version Bootstrap the WhyGraph database under `.whygraph/whygraph.db`, write a committable `whygraph.example.toml` documenting every tunable, and add the right `.gitignore` entries. It's -idempotent — re-running on an initialized project just confirms both databases are present. +idempotent - re-running on an initialized project just confirms both databases are present. `init` does **not** index CodeGraph. That happens on [`scan`](#whygraph-scan). @@ -46,13 +46,13 @@ See [Wiring your editor](../guide/editors.md) for the per-agent paths. ## `whygraph scan` Run the source crawlers, then describe each commit with the configured LLM. This is the command that -populates `.whygraph/whygraph.db` and refreshes the CodeGraph index. It's idempotent — re-running +populates `.whygraph/whygraph.db` and refreshes the CodeGraph index. It's idempotent - re-running picks up new commits and backfills what's missing. | Option | Default | Description | |---|---|---| | `--no-llm-descriptions` | off | Skip the per-commit LLM description phase. The git and GitHub crawlers still run; descriptions backfill lazily on demand and on a later full scan. | -| `--codegraph / --no-codegraph` | on | Refresh the CodeGraph index concurrently with the crawl — `codegraph sync` when an index exists, `codegraph init -i` on first run. A failure here warns rather than aborting. | +| `--codegraph / --no-codegraph` | on | Refresh the CodeGraph index concurrently with the crawl - `codegraph sync` when an index exists, `codegraph init -i` on first run. A failure here warns rather than aborting. | | `--codegraph-image TEXT` | pinned tag | Override the Docker image used for the CodeGraph refresh fallback. Ignored when a local `codegraph` binary is found. | | `--remote / --no-remote` | on | Crawl the source-control remote (GitHub PRs / issues) per `[scan].provider`. `--no-remote` skips it for a fast, offline, token-free scan. | | `--pr-origins / --no-pr-origins` | on | Recover a squash-merged PR's original feature-branch commits via one targeted `git fetch`. Needs the network, so it's skipped under `--no-remote`. | @@ -77,12 +77,12 @@ whygraph analyze [BASELINE] ## `whygraph hooks` -Manage opt-in git hooks that auto-rescan on new commits. There's no daemon — the hooks run a fast, +Manage opt-in git hooks that auto-rescan on new commits. There's no daemon - the hooks run a fast, background, offline scan as you commit. | Subcommand | Description | |---|---| -| `install` | Install the auto-rescan hooks into the current repository. Idempotent and non-clobbering — it appends to a foreign hook behind a sentinel guard. | +| `install` | Install the auto-rescan hooks into the current repository. Idempotent and non-clobbering - it appends to a foreign hook behind a sentinel guard. | | `status` | Report whether the auto-rescan hooks are installed. | | `uninstall` | Remove the auto-rescan hooks, leaving any foreign hook content intact. | diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index e160dc1..51abbc7 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -2,9 +2,9 @@ WhyGraph reads an optional `whygraph.toml` at your repo root. Every field has a built-in default, so an unedited file behaves exactly as if none were present. `whygraph init` scaffolds a fully-commented -`whygraph.example.toml` for you — copy it to `whygraph.toml` and edit what you need. +`whygraph.example.toml` for you - copy it to `whygraph.toml` and edit what you need. -!!! warning "`whygraph.toml` is gitignored — never commit a token" +!!! warning "`whygraph.toml` is gitignored - never commit a token" `init` adds `whygraph.toml` to `.gitignore` precisely because it can hold API keys. Keep it that way. Use `whygraph.example.toml` (committed) for documentation, `whygraph.toml` (ignored) for secrets. @@ -19,9 +19,9 @@ log_level = "INFO" # DEBUG | INFO | WARN | ERROR | CRITICAL [scan] max_workers = 2 # parallel LLM calls in the diff-analyzer crawler provider = "off" # source-control backend for the PR/issue crawl: - # "off" — skip the remote crawl (default) - # "github" — pull PRs/issues from the GitHub remote - # "auto" — detect from the remote URL (github only, for now) + # "off" - skip the remote crawl (default) + # "github" - pull PRs/issues from the GitHub remote + # "auto" - detect from the remote URL (github only, for now) remote = "origin" # git remote whose URL is inspected for provider="auto" # token = "ghp_..." # GitHub token for the gh CLI. Default: read GH_TOKEN / # GITHUB_TOKEN from env (or an existing `gh auth login`). @@ -87,11 +87,11 @@ timeout_sec = 120 |---|---| | top-level `log_level` | Console log verbosity. | | `[scan]` | The crawl: parallelism, which remote provider to use, the git remote name, and an optional pinned GitHub token. | -| `[analyze]` | The per-commit LLM diff descriptions written during `scan` — provider, model, and the truncation / per-file thresholds. | -| `[rationale]` | The `whygraph_rationale_brief` card — provider, model, and how much of a squash-merged PR is rendered into the prompt. | +| `[analyze]` | The per-commit LLM diff descriptions written during `scan` - provider, model, and the truncation / per-file thresholds. | +| `[rationale]` | The `whygraph_rationale_brief` card - provider, model, and how much of a squash-merged PR is rendered into the prompt. | | `whygraph_db` / `codegraph_db` | Override either database path. | | `[logging]` | An optional rotating file log, in addition to the always-on stderr log. | -| `[llm.*]` | Per-provider client settings — model, key, timeout, and `base_url` / `host` where relevant. | +| `[llm.*]` | Per-provider client settings - model, key, timeout, and `base_url` / `host` where relevant. | ## Environment variables @@ -106,5 +106,5 @@ instead. | `GH_TOKEN` / `GITHUB_TOKEN` | The `gh` CLI during the remote crawl, when `[scan].token` is unset. | !!! tip "Provider keys degrade gracefully" - Missing a key for the analysis or rationale phase isn't fatal — that phase skips, and the rest of + Missing a key for the analysis or rationale phase isn't fatal - that phase skips, and the rest of the scan still runs. Descriptions and cards backfill once a credential is available. diff --git a/docs/reference/index.md b/docs/reference/index.md index f38ea0b..c47ccae 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -1,6 +1,6 @@ # Reference -The exact surface — every command, tool, resource, and config key. This section is hand-written +The exact surface - every command, tool, resource, and config key. This section is hand-written against the running code, so it stays in step with what `whygraph` actually does. Pick a page:
diff --git a/docs/reference/mcp.md b/docs/reference/mcp.md index 900a9d5..27815eb 100644 --- a/docs/reference/mcp.md +++ b/docs/reference/mcp.md @@ -1,6 +1,6 @@ # MCP surface -`whygraph-mcp` registers three tools, four resources, and three prompts. That's the whole surface — +`whygraph-mcp` registers three tools, four resources, and three prompts. That's the whole surface - deliberately narrow. WhyGraph owns "why this exists and when it changed"; graph traversal ("what's connected to what") stays with CodeGraph. @@ -11,28 +11,28 @@ For a usage-first walkthrough of how an agent calls these mid-task, see ### `whygraph_evidence_for` -Historical evidence — commits, PRs, and closing issues — for a chunk of code. Line-blame-driven and +Historical evidence - commits, PRs, and closing issues - for a chunk of code. Line-blame-driven and anchored to HEAD. | Parameter | Type | Default | Description | |---|---|---|---| -| `path` | str | — | Source file path, relative to the repo root. | -| `line_start` | int | — | First line of the chunk (1-indexed, inclusive). | -| `line_end` | int | — | Last line of the chunk (1-indexed, inclusive). | -| `qualified_name` | str | — | Fully-qualified symbol name. Use instead of `path`/lines when you know the symbol. CodeGraph resolves it to a file/line range. | +| `path` | str | - | Source file path, relative to the repo root. | +| `line_start` | int | - | First line of the chunk (1-indexed, inclusive). | +| `line_end` | int | - | Last line of the chunk (1-indexed, inclusive). | +| `qualified_name` | str | - | Fully-qualified symbol name. Use instead of `path`/lines when you know the symbol. CodeGraph resolves it to a file/line range. | | `limit` | int | `20` | Cap on the number of commits returned. | Returns `{ "target": {...}, "evidence": [ { "commit", "pull_requests", "issues", "source" }, ... ] }`. ### `whygraph_area_history` -Every commit that touched a file path — or any path it was renamed from. Where `evidence_for` is +Every commit that touched a file path - or any path it was renamed from. Where `evidence_for` is line-blame-driven, `area_history` reaches commits for code that's since been deleted, moved, or fully rewritten. | Parameter | Type | Default | Description | |---|---|---|---| -| `path` | str | *required* | The file path, as it appears at HEAD (or any commit — the rename chain is bidirectional). | +| `path` | str | *required* | The file path, as it appears at HEAD (or any commit - the rename chain is bidirectional). | | `limit` | int | `20` | Cap on commits returned, newest first. | | `include_renames` | bool | `true` | Walk the `renamed_from` chain to include commits that touched historical names. | @@ -69,12 +69,12 @@ Returns the card: } ``` -The card carries exactly five narrative fields — **purpose, why, constraints, tradeoffs, risks** — +The card carries exactly five narrative fields - **purpose, why, constraints, tradeoffs, risks** - plus provenance (`model`, `provider`, `cached_at`) and an `evidence_count` summary. !!! note "Calls the LLM on a cache miss" On a miss, this hits the configured provider and may take several seconds. A hit is sub-second. - Generation needs a credential — see [Configuration](configuration.md). + Generation needs a credential - see [Configuration](configuration.md). ## Resources @@ -103,7 +103,7 @@ WhyGraph exposes no graph-traversal tools on purpose. The split: | Layer | Owns | |---|---| -| **CodeGraph** | "what is connected to what" — callers, callees, symbol resolution, type hierarchy. | -| **WhyGraph** | "why does this exist and when did it change" — evidence, rationale, history. | +| **CodeGraph** | "what is connected to what" - callers, callees, symbol resolution, type hierarchy. | +| **WhyGraph** | "why does this exist and when did it change" - evidence, rationale, history. | For traversal mid-conversation, call CodeGraph's own tools directly. diff --git a/docs/reference/providers.md b/docs/reference/providers.md index 9220dde..1f9dd81 100644 --- a/docs/reference/providers.md +++ b/docs/reference/providers.md @@ -1,10 +1,10 @@ # Source control providers WhyGraph's remote crawl enriches your scan with pull requests and issues. You choose the backend with -`[scan].provider` in `whygraph.toml`. Today there's one supported provider — GitHub — with others on +`[scan].provider` in `whygraph.toml`. Today there's one supported provider - GitHub - with others on the way. -## GitHub — supported +## GitHub - supported Set the provider to `github` to pull PRs and issues from the GitHub remote: @@ -25,7 +25,7 @@ The crawl uses the `gh` CLI, so you need it authenticated. Provide a token one o - An existing `gh auth login`. - `GH_TOKEN` or `GITHUB_TOKEN` in your environment. -- `[scan].token` in `whygraph.toml` — handy when one container scans repos across different orgs. +- `[scan].token` in `whygraph.toml` - handy when one container scans repos across different orgs. !!! note "Off by default" `provider` defaults to `"off"`, so a fresh scan stays git-only and needs no token. Opt into the diff --git a/docs/roadmap.md b/docs/roadmap.md index cbd4ed0..2162e63 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap -WhyGraph works today for the core loop — scan a repo, serve evidence and rationale over MCP. Here's +WhyGraph works today for the core loop - scan a repo, serve evidence and rationale over MCP. Here's what's planned but not yet built. Treat everything below as direction, not a promise of dates. ## More source-control providers @@ -22,11 +22,11 @@ Until then, run against a GitHub remote or stay git-only with `[scan].provider = Larger, net-new pieces that aren't built yet: -- **Cross-repo project registry** — orchestrate WhyGraph across several repos from one place. -- **Persistent / server mode** — a long-running endpoint with an HTTP MCP transport, so an app +- **Cross-repo project registry** - orchestrate WhyGraph across several repos from one place. +- **Persistent / server mode** - a long-running endpoint with an HTTP MCP transport, so an app doesn't spawn a fresh stdio session per connection. This is what unlocks the full [service model](deploy/service.md). -- **Per-branch databases** — separate CodeGraph and WhyGraph databases per branch, so a scan on one +- **Per-branch databases** - separate CodeGraph and WhyGraph databases per branch, so a scan on one branch doesn't clobber another's. !!! info "Want to weigh in?" diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 8d19c89..ab309d3 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,4 +1,4 @@ -/* WhyGraph docs — brand-tuning hook. +/* WhyGraph docs - brand-tuning hook. * * Intentionally near-empty. Material's indigo defaults carry the site for now; * drop palette / typography overrides here when a brand identity lands. */