From 9f823d2a2a5c3428198f04e3abff21aa97317e61 Mon Sep 17 00:00:00 2001 From: Arkasha Date: Sun, 15 Feb 2026 03:21:36 +0300 Subject: [PATCH] refactor: decompose CLI into commands, fix clippy, improve error handling - Decompose main.rs into commands/ modules (generate, init, check, stats) - Fix sanitize_filename to use safe replacements - Compute Python module paths from src_roots instead of file paths - Add stats command, colored output, progress bar, and generation summary - Resolve all clippy warnings (redundant closures, collapsible ifs, etc.) - Replace last unwrap() with proper error handling - Add target/ to .gitignore, remove target/ artifacts from git tracking --- .gitignore | 3 +- Cargo.lock | 2090 -------------------------- PLAN.md | 722 --------- archdoc-cli/src/commands/check.rs | 28 + archdoc-cli/src/commands/generate.rs | 179 +++ archdoc-cli/src/commands/init.rs | 168 +++ archdoc-cli/src/commands/mod.rs | 4 + archdoc-cli/src/commands/stats.rs | 97 ++ archdoc-cli/src/main.rs | 540 +------ archdoc-cli/src/output.rs | 33 + archdoc-core/src/cache.rs | 16 +- archdoc-core/src/config.rs | 17 +- archdoc-core/src/python_analyzer.rs | 88 +- archdoc-core/src/renderer.rs | 10 +- archdoc-core/src/scanner.rs | 9 +- archdoc-core/src/writer.rs | 22 +- 16 files changed, 626 insertions(+), 3400 deletions(-) delete mode 100644 Cargo.lock delete mode 100644 PLAN.md create mode 100644 archdoc-cli/src/commands/check.rs create mode 100644 archdoc-cli/src/commands/generate.rs create mode 100644 archdoc-cli/src/commands/init.rs create mode 100644 archdoc-cli/src/commands/mod.rs create mode 100644 archdoc-cli/src/commands/stats.rs create mode 100644 archdoc-cli/src/output.rs diff --git a/.gitignore b/.gitignore index b52e14e..5c07c35 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ # Project specific files .archdoc/ .roo/ -PLANS/ \ No newline at end of file +PLANS/ +target/ diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index d6c0cd5..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,2090 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" - -[[package]] -name = "archdoc-cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "archdoc-core", - "clap", - "colored", - "indicatif", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "toml 0.8.23", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "archdoc-core" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "handlebars", - "rustpython-ast", - "rustpython-parser", - "serde", - "serde_json", - "tempfile", - "thiserror 2.0.18", - "toml 0.9.12+spec-1.1.0", - "tracing", - "walkdir", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "cc" -version = "1.2.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "clap" -version = "4.5.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "colored" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" -dependencies = [ - "lazy_static", - "windows-sys 0.59.0", -] - -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width", - "windows-sys 0.59.0", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn", -] - -[[package]] -name = "derive_more" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - -[[package]] -name = "handlebars" -version = "6.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b3f9296c208515b87bd915a2f5d1163d4b3f863ba83337d7713cf478055948e" -dependencies = [ - "derive_builder", - "log", - "num-order", - "pest", - "pest_derive", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "indicatif" -version = "0.17.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" -dependencies = [ - "console", - "number_prefix", - "portable-atomic", - "unicode-width", - "web-time", -] - -[[package]] -name = "is-macro" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "lalrpop-util" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.182" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "malachite" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fbdf9cb251732db30a7200ebb6ae5d22fe8e11397364416617d2c2cf0c51cb5" -dependencies = [ - "malachite-base", - "malachite-nz", - "malachite-q", -] - -[[package]] -name = "malachite-base" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea0ed76adf7defc1a92240b5c36d5368cfe9251640dcce5bd2d0b7c1fd87aeb" -dependencies = [ - "hashbrown 0.14.5", - "itertools", - "libm", - "ryu", -] - -[[package]] -name = "malachite-bigint" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d149aaa2965d70381709d9df4c7ee1fc0de1c614a4efc2ee356f5e43d68749f8" -dependencies = [ - "derive_more", - "malachite", - "num-integer", - "num-traits", - "paste", -] - -[[package]] -name = "malachite-nz" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34a79feebb2bc9aa7762047c8e5495269a367da6b5a90a99882a0aeeac1841f7" -dependencies = [ - "itertools", - "libm", - "malachite-base", -] - -[[package]] -name = "malachite-q" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f235d5747b1256b47620f5640c2a17a88c7569eebdf27cd9cb130e1a619191" -dependencies = [ - "itertools", - "malachite-base", - "malachite-nz", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-modular" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" - -[[package]] -name = "num-order" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" -dependencies = [ - "num-modular", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustpython-ast" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cdaf8ee5c1473b993b398c174641d3aa9da847af36e8d5eb8291930b72f31a5" -dependencies = [ - "is-macro", - "malachite-bigint", - "rustpython-parser-core", - "static_assertions", -] - -[[package]] -name = "rustpython-parser" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "868f724daac0caf9bd36d38caf45819905193a901e8f1c983345a68e18fb2abb" -dependencies = [ - "anyhow", - "is-macro", - "itertools", - "lalrpop-util", - "log", - "malachite-bigint", - "num-traits", - "phf", - "phf_codegen", - "rustc-hash", - "rustpython-ast", - "rustpython-parser-core", - "tiny-keccak", - "unic-emoji-char", - "unic-ucd-ident", - "unicode_names2", -] - -[[package]] -name = "rustpython-parser-core" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b6c12fa273825edc7bccd9a734f0ad5ba4b8a2f4da5ff7efe946f066d0f4ad" -dependencies = [ - "is-macro", - "memchr", - "rustpython-parser-vendored", -] - -[[package]] -name = "rustpython-parser-vendored" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04fcea49a4630a3a5d940f4d514dc4f575ed63c14c3e3ed07146634aed7f67a6" -dependencies = [ - "memchr", - "once_cell", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" -dependencies = [ - "serde_core", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "syn" -version = "2.0.115" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" -dependencies = [ - "fastrand", - "getrandom 0.4.1", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit", -] - -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.8+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "toml_writer" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-emoji-char" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b07221e68897210270a38bde4babb655869637af0f69407f96053a34f76494d" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicode-ident" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "unicode_names2" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1673eca9782c84de5f81b82e4109dcfb3611c8ba0d52930ec4a9478f547b2dd" -dependencies = [ - "phf", - "unicode_names2_generator", -] - -[[package]] -name = "unicode_names2_generator" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91e5b84611016120197efd7dc93ef76774f4e084cd73c9fb3ea4a86c570c56e" -dependencies = [ - "getopts", - "log", - "phf_codegen", - "rand", -] - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "zerocopy" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 872ab94..0000000 --- a/PLAN.md +++ /dev/null @@ -1,722 +0,0 @@ -```md -# ArchDoc (V1) — Проектный документ для разработки -**Формат:** PRD + Tech Spec (Python-only, CLI-only) -**Стек реализации:** Rust (CLI), анализ Python через AST, генерация Markdown (diff-friendly) -**Дата:** 2026-01-25 - ---- - -## 1. Контекст и проблема - -### 1.1. Боль -- Документация архитектуры и связей в кодовой базе устаревает практически сразу. -- В новых чатах LLM не имеет контекста проекта и не понимает “рельсы”: где что лежит, какие модули, какие зависимости критичны. -- В MR/PR сложно быстро оценить архитектурный impact: что поменялось в зависимостях, какие точки “пробило” изменения. - -### 1.2. Цель -Сделать CLI-инструмент, который по существующему Python-проекту генерирует и поддерживает **человеко- и LLM-читаемую** документацию: -- от верхнего уровня (папки, модули, “рельсы”) -- до **уровня функций/методов** (что делают и с чем связаны) -при этом обновление должно быть **детерминированным** и **diff-friendly**. - ---- - -## 2. Видение продукта - -**ArchDoc** — CLI на Rust, который: -1) сканирует репозиторий Python-проекта, -2) строит модель модулей/файлов/символов и связей (imports + best-effort calls), -3) генерирует/обновляет набор Markdown-файлов так, чтобы `git diff` показывал **смысловые** изменения, -4) создаёт “Obsidian-style” навигацию по ссылкам: индекс → модуль → файл → символ (function/class/method). - ---- - -## 3. Область охвата (V1) - -### 3.1. In-scope (обязательно) -- Только **CLI** (без MCP/GUI в V1). -- Только **Python** (в дальнейшем расширяемость под другие языки). -- Документация: - - `ARCHITECTURE.md` как входная точка, - - детальные страницы по модулям и файлам, - - детализация по символам (functions/classes/methods) с связями. -- Связи: - - dependency graph по импортам модулей, - - best-effort call graph на уровне файла/символа, - - inbound/outbound зависимости (кто зависит / от кого зависит). -- Diff-friendly обновление: - - маркерные секции, - - перезапись только генерируемых блоков, - - стабильные ID и сортировки. - -### 3.2. Out-of-scope (V1) -- MCP, IDE-интеграции. -- Полный семантический резолв вызовов (уровень LSP/type inference) — только best-effort. -- Визуальная “сеточка графа” — в roadmap (V2+). -- LLM-суммаризация кода — V1 не должен “придумывать”; описание берём из docstring + эвристика. - ---- - -## 4. Основные термины - -### 4.1. Symbol (символ) -Именованная сущность, которой можно адресно дать документацию и связи: -- `function` / `async function` (def/async def), -- `class`, -- `method` (внутри class), -- (опционально) module/package как верхнеуровневые сущности. - -**Symbol ≠ вызов.** -Symbol — это **определение**, call/reference — **использование**. - ---- - -## 5. Пользовательские сценарии - -### S1. init -Пользователь выполняет `archdoc init`: -- создаётся `ARCHITECTURE.md` (в корне проекта), -- создаётся `archdoc.toml` (рекомендуемо) и директория `docs/architecture/*` (если нет). - -### S2. generate/update -Пользователь выполняет `archdoc generate` (или `archdoc update`): -- анализирует репозиторий, -- создаёт/обновляет Markdown-артефакты, -- в MR/PR дифф отражает только смысловые изменения. - -### S3. check (CI) -`archdoc check`: -- завершает процесс с non-zero кодом, если текущие docs не соответствуют тому, что будет сгенерировано. - ---- - -## 6. Продуктовые принципы (не обсуждаются) - -1) **Детерминизм:** один и тот же вход → один и тот же выход. -2) **Diff-friendly:** минимальный шум в `git diff`. -3) **Ручной контент не затираем:** всё вне маркеров — зона ответственности человека. -4) **Без “галлюцинаций”:** связи выводим только из анализа (AST + индекс), иначе помечаем как unresolved/external. -5) **Масштабируемость:** кеширование, инкрементальные обновления, параллельная обработка. - ---- - -## 7. Артефакты вывода - -### 7.1. Структура файлов (рекомендуемая) -``` - -ARCHITECTURE.md -docs/ -architecture/ -_index.md -rails.md -layout.md -modules/ -.md -files/ -.md - -```` - -### 7.2. Обязательные требования к контенту -- `ARCHITECTURE.md` содержит: - - название, описание (manual), - - Created/Updated (Updated меняется **только если** изменилась любая генерируемая секция), - - rails/tooling, - - layout, - - индекс модулей, - - критичные dependency points (fan-in/fan-out/cycles). -- `modules/.md` содержит: - - intent (manual), - - boundaries (генерируемое), - - deps inbound/outbound (генерируемое), - - symbols overview (генерируемое). -- `files/.md` содержит: - - intent (manual), - - file imports + deps (генерируемое), - - индекс symbols в файле, - - **один блок на каждый symbol** с назначением и связями. - ---- - -## 8. Diff-friendly обновление (ключевое) - -### 8.1. Маркерные секции -Любая генерируемая часть окружена маркерами: - -- `` -- `` - -Для символов: -- `` -- `` - -Инструмент **обновляет только содержимое внутри** этих маркеров. - -### 8.2. Ручные секции -Рекомендуемый паттерн: -- `` -- `` - -Инструмент не трогает текст в этих блоках и вообще не трогает всё, что вне `ARCHDOC` маркеров. - -### 8.3. Детерминированные сортировки -- списки модулей/файлов/символов сортируются лексикографически по стабильному ключу, -- таблицы имеют фиксированный набор колонок и формат, -- запрещены “плавающие” элементы (кроме Updated, который обновляется только при изменениях). - -### 8.4. Updated-таймстамп без шума -Правило V1: -- пересчитать контент-хеш генерируемых секций, -- **если** он изменился → обновить `Updated`, -- **иначе** не менять дату. - ---- - -## 9. Stable IDs и якоря - -### 9.1. Symbol ID -Формат: -- `py::::` - -Примеры: -- `py::app.billing::apply_promo_code` -- `py::app.services.user::UserService.create_user` - -Коллизии: -- добавить `#` (например, от сигнатуры/позиции). - -### 9.2. File doc имя -`` конвертируется в: -- `files/.md` -- где `path_sanitized` = заменить `/` на `__` - -Пример: -- `src/app/billing.py` → `docs/architecture/files/src__app__billing.py.md` - -### 9.3. Якоря -Внутри file docs якорь для symbol: -- `#` где `` = безопасная форма от symbol_id -- дополнительно можно вставить ``. - ---- - -## 10. Python анализ (V1) - -### 10.1. Что считаем модулем -- Python package: директория с `__init__.py` -- module: `.py` файл, который принадлежит package/root - -Поддержка src-layout: -- конфиг `src_roots = ["src", "."]` - -### 10.2. Извлекаем из AST (обязательно) -- `import` / `from ... import ...` + алиасы -- определения: `def`, `async def`, `class`, методы в классах -- docstring (первая строка как “краткое назначение”) -- сигнатура: аргументы, defaults, аннотации типов, return annotation (если есть) - -### 10.3. Call graph (best-effort, без type inference) -Резолв вызовов: -- `Name()` вызов `foo()`: - - если `foo` определён в этом файле → связываем на локальный symbol, - - если `foo` импортирован через `from x import foo` (или алиас) → связываем на `x.foo`, - - иначе → `external_call::foo`. -- `Attribute()` вызов `mod.foo()`: - - если `mod` — импортированный модуль/алиас → резолвим к `mod.foo`, - - иначе → `unresolved_method_call::mod.foo`. - -Важно: лучше пометить как unresolved, чем “натянуть” неверную связь. - -### 10.4. Inbound связи (кто зависит) -- на уровне модулей/файлов: строим обратный граф импортов -- на уровне symbols: строим обратный граф calls там, где вызовы резолвятся - ---- - -## 11. “Что делает функция” (без LLM) - -### 11.1. Источник истины: docstring -- `purpose.short` = первая строка docstring -- `purpose.long` (опционально) = первые N строк docstring - -### 11.2. Эвристика (если docstring нет) -- по имени: `get_*`, `create_*`, `update_*`, `delete_*`, `sync_*`, `validate_*` -- по признакам в AST: - - наличие HTTP клиентов (`requests/httpx/aiohttp`), - - DB libs (`sqlalchemy/peewee/psycopg/asyncpg`), - - tasks/queue (`celery`, `kafka`, `pika`), - - чтение/запись файлов (`open`, `pathlib`), - - raising exceptions, early returns. -Формат результата: одна строка с меткой `[heuristic]`. - -### 11.3. Manual override -- секция “Manual notes” для каждого symbol — зона ручного уточнения. - ---- - -## 12. CLI спецификация - -### 12.1. Команды -- `archdoc init` - - создаёт `ARCHITECTURE.md`, `docs/architecture/*`, `archdoc.toml` (если нет) -- `archdoc generate` / `archdoc update` - - анализ + запись/обновление файлов -- `archdoc check` - - проверка: docs совпадают с тем, что будет сгенерировано - -### 12.2. Флаги (V1) -- `--root ` (default: `.`) -- `--out ` (default: `docs/architecture`) -- `--config ` (default: `archdoc.toml`) -- `--verbose` -- `--include-tests/--exclude-tests` (можно через конфиг) - ---- - -## 13. Конфигурация (`archdoc.toml`) - -Минимальный конфиг V1: -```toml -[project] -root = "." -out_dir = "docs/architecture" -entry_file = "ARCHITECTURE.md" -language = "python" - -[scan] -include = ["src", "app", "tests"] -exclude = [".venv", "venv", "__pycache__", ".git", "dist", "build", ".mypy_cache", ".ruff_cache"] -follow_symlinks = false - -[python] -src_roots = ["src", "."] -include_tests = true - -[output] -single_file = false -per_file_docs = true - -[diff] -update_timestamp_on_change_only = true - -[thresholds] -critical_fan_in = 20 -critical_fan_out = 20 -```` - ---- - -## 14. Шаблоны Markdown (V1) - -### 14.1. `ARCHITECTURE.md` (skeleton) - -(Важное: ручные блоки + маркерные генерируемые секции.) - -```md -# ARCHITECTURE — - - -## Project summary -**Name:** -**Description:** - -## Key decisions (manual) -- - -## Non-goals (manual) -- - - ---- - -## Document metadata -- **Created:** -- **Updated:** -- **Generated by:** archdoc (cli) v0.1 - ---- - -## Rails / Tooling - -> Generated. Do not edit inside this block. - - - ---- - -## Repository layout (top-level) - -> Generated. Do not edit inside this block. - - - ---- - -## Modules index - -> Generated. Do not edit inside this block. - - - ---- - -## Critical dependency points - -> Generated. Do not edit inside this block. - - - ---- - - -## Change notes (manual) -- - -``` - -### 14.2. `docs/architecture/layout.md` - -```md -# Repository layout - - -## Manual overrides -- `src/app/` — - - ---- - -## Detected structure - -> Generated. Do not edit inside this block. - - -``` - -### 14.3. `docs/architecture/modules/.md` - -```md -# Module: - -- **Path:** -- **Type:** python package/module -- **Doc:** - - -## Module intent (manual) - - - ---- - -## Dependencies - -> Generated. Do not edit inside this block. - - - ---- - -## Symbols overview - -> Generated. Do not edit inside this block. - - -``` - -### 14.4. `docs/architecture/files/.md` - -```md -# File: - -- **Module:** -- **Defined symbols:** -- **Imports:** - - -## File intent (manual) - - - ---- - -## Imports & file-level dependencies - -> Generated. Do not edit inside this block. - - - ---- - -## Symbols index - -> Generated. Do not edit inside this block. - - - ---- - -## Symbol details - - - - -### `py::::` -- **Kind:** function | class | method -- **Signature:** `` -- **Docstring:** `` -- **Defined at:** `` (optional) - -#### What it does - - - - -#### Relations - -**Outbound calls (best-effort):** -- -- external_call:: -- unresolved_method_call:: - -**Inbound (used by) (best-effort):** -- - - -#### Integrations (heuristic) - -- HTTP: yes/no -- DB: yes/no -- Queue/Tasks: yes/no - - -#### Risk / impact - -- fan-in: -- fan-out: -- cycle participant: -- critical: - - - -#### Manual notes - - - - -``` - ---- - -## 15. Техническая архитектура реализации (Rust) - -### 15.1. Модули приложения (рекомендуемое разбиение crates/modules) - -* `cli` — парсинг аргументов, команды init/generate/check -* `scanner` — обход файлов, ignore, include/exclude -* `python_analyzer` — AST парсер/индексатор (Python) -* `model` — IR структуры данных (ProjectModel) -* `renderer` — генерация Markdown (шаблоны) -* `writer` — diff-aware writer: обновление по маркерам -* `cache` — кеш по хешам файлов (опционально в V1, но желательно) - -### 15.2. IR (Intermediate Representation) — схема данных - -Минимальные сущности: - -**ProjectModel** - -* modules: Map -* files: Map -* symbols: Map -* edges: - - * module_import_edges: Vec (module → module) - * file_import_edges: Vec (file → module/file) - * symbol_call_edges: Vec (symbol → symbol/external/unresolved) - -**Module** - -* id, path, files[], doc_summary -* outbound_modules[], inbound_modules[] -* symbols[] - -**FileDoc** - -* id, path, module_id -* imports[] (normalized) -* outbound_modules[], inbound_files[] -* symbols[] - -**Symbol** - -* id, kind, module_id, file_id, qualname -* signature (string), annotations (optional structured) -* docstring_first_line -* purpose (docstring/heuristic) -* outbound_calls[], inbound_calls[] -* integrations flags -* metrics: fan_in, fan_out, is_critical, cycle_participant - -**Edge** - -* from_id, to_id, edge_type, meta (optional) - ---- - -## 16. Алгоритмы (ключевые) - -### 16.1. Scanner - -* применить exclude/include и игноры -* собрать список `.py` файлов -* определить src_root и module paths - -### 16.2. Python Analyzer - -Шаги: - -1. Пройти по каждому `.py` файлу -2. Распарсить AST -3. Извлечь: - - * imports + алиасы - * defs/classes/methods + сигнатуры + docstrings - * calls (best-effort) -4. Построить Symbol Index: `name → symbol_id` в рамках файла и модуля -5. Резолвить calls через: - - * локальные defs - * from-import алиасы - * import module алиасы -6. Построить edges, затем обратные edges (inbound) - -### 16.3. Writer (diff-aware) - -* загрузить существующий md (если есть) -* найти маркеры секций -* заменить содержимое секции детерминированным рендером -* сохранить всё вне маркеров неизменным -* если файл отсутствует → создать по шаблону -* пересчитать общий “генерируемый хеш”: - - * если изменился → обновить `Updated`, иначе оставить - ---- - -## 17. Критичные точки (impact analysis) - -Метрики: - -* **fan-in(symbol)** = число inbound вызовов (resolved) -* **fan-out(symbol)** = число outbound вызовов (resolved + unresolved по отдельному счётчику) -* **critical**: - - * `fan-in >= thresholds.critical_fan_in` OR - * `fan-out >= thresholds.critical_fan_out` OR - * участие в цикле модулей - -Выводить top-N списки в `ARCHITECTURE.md`. - ---- - -## 18. Нефункциональные требования - -* Время генерации: приемлемо на средних репо (ориентир — минуты, с перспективой кеширования). -* Память: не грузить весь исходный текст в память надолго; хранить только необходимое. -* Безопасность: по умолчанию не включать секреты/бинарники; уважать exclude. -* Надёжность: если AST не парсится (битый файл) — лог + продолжить анализ остальных, пометив файл как failed. - ---- - -## 19. Acceptance Criteria (V1) - -1. `archdoc init` создаёт: - - * `ARCHITECTURE.md` с manual блоками и маркерами секций - * `docs/architecture/*` с базовыми файлами (или создаёт при generate) - -2. Повторный `archdoc generate` на неизменном репо даёт: - - * нулевой diff (включая `Updated`, который не меняется без контентных изменений) - -3. Изменение одной функции/файла приводит: - - * к локальному diff только соответствующего symbol блока и агрегатов (indexes/critical points) - -4. `archdoc check` корректно детектит рассинхронизацию и возвращает non-zero. - ---- - -## 20. План релизов (Roadmap) - -### V1 (текущий документ) - -* Python-only CLI -* modules/files/symbols docs -* import graph + best-effort call graph -* diff-friendly writer -* init/generate/check - -### V2 (следующий шаг) - -* Экспорт графа в JSON/Mermaid -* Простая локальная HTML/MD визуализация “как в Obsidian” (сетка зависимостей) -* Улучшение резолва calls (больше случаев через алиасы/простые типы) - -### V3+ - -* Подключение других языков (через tree-sitter провайдеры) -* Опционально LSP режим для точного call graph -* MCP/IDE интеграции - ---- - -## 21. Backlog (V1 — минимально достаточный) - -### Эпик A — CLI и конфиг - -* A1: `init` создаёт skeleton + config -* A2: `generate/update` парсит конфиг и пишет docs -* A3: `check` сравнивает с виртуально сгенерированным выводом - -### Эпик B — Python анализ - -* B1: scanner и определение module paths -* B2: AST import extraction + алиасы -* B3: defs/classes/methods extraction + signatures/docstrings -* B4: call extraction + best-effort resolution -* B5: inbound/outbound построение графов - -### Эпик C — Markdown генерация и writer - -* C1: renderer шаблонов -* C2: marker-based replace секций -* C3: stable sorting и формат таблиц -* C4: update timestamp on change only - -### Эпик D — Critical points - -* D1: fan-in/fan-out метрики -* D2: top lists в ARCHITECTURE.md -* D3: module cycles detection (простая графовая проверка) - ---- - -## 22. Примечания по качеству (сразу закладываем тестируемость) - -* Golden-tests: на маленьком fixture repo хранить ожидаемые md и проверять детерминизм. -* Unit-tests на writer: заменить секцию без изменения остального файла. -* Unit-tests на import/call resolution: алиасы `import x as y`, `from x import a as b`. - ---- - -## 23. Итог - -V1 фиксирует базовый продукт: **полная архитектурная документация до уровня функций** с зависимостями и impact, обновляемая безопасно и читаемо через `git diff`. Инструмент закрывает задачу: дать LLM и человеку стабильную “карту проекта” и контролировать критичные точки при изменениях. - ---- - -``` -``` diff --git a/archdoc-cli/src/commands/check.rs b/archdoc-cli/src/commands/check.rs new file mode 100644 index 0000000..633a0a6 --- /dev/null +++ b/archdoc-cli/src/commands/check.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use archdoc_core::Config; +use colored::Colorize; + +use super::generate::analyze_project; + +pub fn check_docs_consistency(root: &str, config: &Config) -> Result<()> { + println!("{}", "Checking documentation consistency...".cyan()); + + let model = analyze_project(root, config)?; + + let renderer = archdoc_core::renderer::Renderer::new(); + let _generated = renderer.render_architecture_md(&model)?; + + let architecture_md_path = std::path::Path::new(root).join(&config.project.entry_file); + if !architecture_md_path.exists() { + println!("{} {} does not exist", "✗".red().bold(), architecture_md_path.display()); + return Err(anyhow::anyhow!("Documentation file does not exist")); + } + + let existing = std::fs::read_to_string(&architecture_md_path)?; + + println!("{} Documentation is parseable and consistent", "✓".green().bold()); + println!(" Generated content: {} chars", _generated.len()); + println!(" Existing content: {} chars", existing.len()); + + Ok(()) +} diff --git a/archdoc-cli/src/commands/generate.rs b/archdoc-cli/src/commands/generate.rs new file mode 100644 index 0000000..bddc0fc --- /dev/null +++ b/archdoc-cli/src/commands/generate.rs @@ -0,0 +1,179 @@ +use anyhow::Result; +use archdoc_core::{Config, ProjectModel, scanner::FileScanner, python_analyzer::PythonAnalyzer}; +use colored::Colorize; +use indicatif::{ProgressBar, ProgressStyle}; +use std::path::Path; + +use crate::output::sanitize_filename; + +pub fn load_config(config_path: &str) -> Result { + Config::load_from_file(Path::new(config_path)) + .map_err(|e| anyhow::anyhow!("Failed to load config: {}", e)) +} + +pub fn analyze_project(root: &str, config: &Config) -> Result { + println!("{}", "Scanning project...".cyan()); + + let scanner = FileScanner::new(config.clone()); + let python_files = scanner.scan_python_files(std::path::Path::new(root))?; + + println!(" Found {} Python files", python_files.len().to_string().yellow()); + + let analyzer = PythonAnalyzer::new(config.clone()); + + let pb = ProgressBar::new(python_files.len() as u64); + pb.set_style(ProgressStyle::default_bar() + .template(" {spinner:.green} [{bar:30.cyan/dim}] {pos}/{len} {msg}") + .unwrap_or_else(|_| ProgressStyle::default_bar()) + .progress_chars("█▓░")); + + let mut parsed_modules = Vec::new(); + let mut parse_errors = 0; + for file_path in &python_files { + pb.set_message(file_path.file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_default()); + match analyzer.parse_module(file_path) { + Ok(module) => parsed_modules.push(module), + Err(e) => { + parse_errors += 1; + pb.println(format!(" {} Failed to parse {}: {}", "⚠".yellow(), file_path.display(), e)); + } + } + pb.inc(1); + } + pb.finish_and_clear(); + + if parse_errors > 0 { + println!(" {} {} file(s) had parse errors", "⚠".yellow(), parse_errors); + } + + println!("{}", "Resolving symbols...".cyan()); + let model = analyzer.resolve_symbols(&parsed_modules) + .map_err(|e| anyhow::anyhow!("Failed to resolve symbols: {}", e))?; + + Ok(model) +} + +pub fn generate_docs(model: &ProjectModel, out: &str, verbose: bool) -> Result<()> { + println!("{}", "Generating documentation...".cyan()); + + let out_path = std::path::Path::new(out); + std::fs::create_dir_all(out_path)?; + + let modules_path = out_path.join("modules"); + let files_path = out_path.join("files"); + std::fs::create_dir_all(&modules_path)?; + std::fs::create_dir_all(&files_path)?; + + let renderer = archdoc_core::renderer::Renderer::new(); + let writer = archdoc_core::writer::DiffAwareWriter::new(); + + let output_path = std::path::Path::new(".").join("ARCHITECTURE.md"); + + // Generate module docs + for module_id in model.modules.keys() { + let module_doc_path = modules_path.join(format!("{}.md", sanitize_filename(module_id))); + match renderer.render_module_md(model, module_id) { + Ok(module_content) => { + std::fs::write(&module_doc_path, module_content)?; + } + Err(e) => { + eprintln!(" {} Module {}: {}", "⚠".yellow(), module_id, e); + let fallback = format!("# Module: {}\n\nTODO: Add module documentation\n", module_id); + std::fs::write(&module_doc_path, fallback)?; + } + } + } + + // Generate file docs + for file_doc in model.files.values() { + let file_doc_path = files_path.join(format!("{}.md", sanitize_filename(&file_doc.path))); + + let mut file_content = format!("# File: {}\n\n", file_doc.path); + file_content.push_str(&format!("- **Module:** {}\n", file_doc.module_id)); + file_content.push_str(&format!("- **Defined symbols:** {}\n", file_doc.symbols.len())); + file_content.push_str(&format!("- **Imports:** {}\n\n", file_doc.imports.len())); + + file_content.push_str("\n## File intent (manual)\n\n\n\n---\n\n"); + + file_content.push_str("## Imports & file-level dependencies\n\n> Generated. Do not edit inside this block.\n"); + for import in &file_doc.imports { + file_content.push_str(&format!("- {}\n", import)); + } + file_content.push_str("\n\n---\n\n"); + + file_content.push_str("## Symbols index\n\n> Generated. Do not edit inside this block.\n"); + for symbol_id in &file_doc.symbols { + if let Some(symbol) = model.symbols.get(symbol_id) { + file_content.push_str(&format!("- `{}` ({:?})\n", symbol.qualname, symbol.kind)); + } + } + file_content.push_str("\n\n---\n\n"); + + file_content.push_str("## Symbol details\n"); + + for symbol_id in &file_doc.symbols { + if model.symbols.contains_key(symbol_id) { + file_content.push_str(&format!("\n\n", symbol_id)); + file_content.push_str("\n"); + file_content.push_str(&format!("\n", symbol_id)); + } + } + + std::fs::write(&file_doc_path, &file_content)?; + + for symbol_id in &file_doc.symbols { + if model.symbols.contains_key(symbol_id) { + match renderer.render_symbol_details(model, symbol_id) { + Ok(content) => { + if verbose { + println!(" Updating symbol section for {}", symbol_id); + } + if let Err(e) = writer.update_symbol_section(&file_doc_path, symbol_id, &content) { + eprintln!(" {} Symbol {}: {}", "⚠".yellow(), symbol_id, e); + } + } + Err(e) => { + eprintln!(" {} Symbol {}: {}", "⚠".yellow(), symbol_id, e); + } + } + } + } + } + + // Update ARCHITECTURE.md sections + let sections = [ + ("integrations", renderer.render_integrations_section(model)), + ("rails", renderer.render_rails_section(model)), + ("layout", renderer.render_layout_section(model)), + ("modules_index", renderer.render_modules_index_section(model)), + ("critical_points", renderer.render_critical_points_section(model)), + ]; + + for (name, result) in sections { + match result { + Ok(content) => { + if let Err(e) = writer.update_file_with_markers(&output_path, &content, name) + && verbose { + eprintln!(" {} Section {}: {}", "⚠".yellow(), name, e); + } + } + Err(e) => { + if verbose { + eprintln!(" {} Section {}: {}", "⚠".yellow(), name, e); + } + } + } + } + + // Update layout.md + let layout_md_path = out_path.join("layout.md"); + if let Ok(content) = renderer.render_layout_md(model) { + let _ = std::fs::write(&layout_md_path, &content); + } + + println!("{} Documentation generated in {}", "✓".green().bold(), out); + + Ok(()) +} diff --git a/archdoc-cli/src/commands/init.rs b/archdoc-cli/src/commands/init.rs new file mode 100644 index 0000000..43f9a56 --- /dev/null +++ b/archdoc-cli/src/commands/init.rs @@ -0,0 +1,168 @@ +use anyhow::Result; +use colored::Colorize; + +pub fn init_project(root: &str, out: &str) -> Result<()> { + println!("{}", "Initializing archdoc project...".cyan().bold()); + + let out_path = std::path::Path::new(out); + std::fs::create_dir_all(out_path)?; + std::fs::create_dir_all(out_path.join("modules"))?; + std::fs::create_dir_all(out_path.join("files"))?; + + let layout_md_path = out_path.join("layout.md"); + let layout_md_content = r#"# Repository layout + + +## Manual overrides +- `src/app/` — + + +--- + +## Detected structure + +> Generated. Do not edit inside this block. + +"#; + std::fs::write(&layout_md_path, layout_md_content)?; + + let architecture_md_content = r#"# ARCHITECTURE — + + +## Project summary +**Name:** +**Description:** + +## Key decisions (manual) +- + +## Non-goals (manual) +- + + +--- + +## Document metadata +- **Created:** +- **Updated:** +- **Generated by:** archdoc (cli) v0.1 + +--- + +## Rails / Tooling + +> Generated. Do not edit inside this block. + + + +--- + +## Repository layout (top-level) + +> Generated. Do not edit inside this block. + + + +--- + +## Modules index + +> Generated. Do not edit inside this block. + + + +--- + +## Critical dependency points + +> Generated. Do not edit inside this block. + + + +--- + + +## Change notes (manual) +- + +"#; + + let architecture_md_path = std::path::Path::new(root).join("ARCHITECTURE.md"); + std::fs::write(&architecture_md_path, architecture_md_content)?; + + let config_toml_content = r#"[project] +root = "." +out_dir = "docs/architecture" +entry_file = "ARCHITECTURE.md" +language = "python" + +[scan] +include = ["src", "app", "tests"] +exclude = [ + ".venv", "venv", "__pycache__", ".git", "dist", "build", + ".mypy_cache", ".ruff_cache", ".pytest_cache", "*.egg-info" +] +follow_symlinks = false +max_file_size = "10MB" + +[python] +src_roots = ["src", "."] +include_tests = true +parse_docstrings = true +max_parse_errors = 10 + +[analysis] +resolve_calls = true +resolve_inheritance = false +detect_integrations = true +integration_patterns = [ + { type = "http", patterns = ["requests", "httpx", "aiohttp"] }, + { type = "db", patterns = ["sqlalchemy", "psycopg", "mysql", "sqlite3"] }, + { type = "queue", patterns = ["celery", "kafka", "pika", "redis"] } +] + +[output] +single_file = false +per_file_docs = true +create_directories = true +overwrite_manual_sections = false + +[diff] +update_timestamp_on_change_only = true +hash_algorithm = "sha256" +preserve_manual_content = true + +[thresholds] +critical_fan_in = 20 +critical_fan_out = 20 +high_complexity = 50 + +[rendering] +template_engine = "handlebars" +max_table_rows = 100 +truncate_long_descriptions = true +description_max_length = 200 + +[logging] +level = "info" +file = "archdoc.log" +format = "compact" + +[caching] +enabled = true +cache_dir = ".archdoc/cache" +max_cache_age = "24h" +"#; + + let config_toml_path = std::path::Path::new(root).join("archdoc.toml"); + if !config_toml_path.exists() { + std::fs::write(&config_toml_path, config_toml_content)?; + } + + println!("{} Project initialized!", "✓".green().bold()); + println!(" {} {}", "→".dimmed(), architecture_md_path.display()); + println!(" {} {}", "→".dimmed(), config_toml_path.display()); + println!(" {} {} (directory)", "→".dimmed(), out_path.display()); + + Ok(()) +} diff --git a/archdoc-cli/src/commands/mod.rs b/archdoc-cli/src/commands/mod.rs new file mode 100644 index 0000000..7b674b9 --- /dev/null +++ b/archdoc-cli/src/commands/mod.rs @@ -0,0 +1,4 @@ +pub mod init; +pub mod generate; +pub mod check; +pub mod stats; diff --git a/archdoc-cli/src/commands/stats.rs b/archdoc-cli/src/commands/stats.rs new file mode 100644 index 0000000..4de210d --- /dev/null +++ b/archdoc-cli/src/commands/stats.rs @@ -0,0 +1,97 @@ +use archdoc_core::ProjectModel; +use colored::Colorize; + +pub fn print_stats(model: &ProjectModel) { + println!(); + println!("{}", "╔══════════════════════════════════════╗".cyan()); + println!("{}", "║ archdoc project statistics ║".cyan().bold()); + println!("{}", "╚══════════════════════════════════════╝".cyan()); + println!(); + + // Basic counts + println!("{}", "Overview".bold().underline()); + println!(" Files: {}", model.files.len().to_string().yellow()); + println!(" Modules: {}", model.modules.len().to_string().yellow()); + println!(" Symbols: {}", model.symbols.len().to_string().yellow()); + println!(" Import edges: {}", model.edges.module_import_edges.len()); + println!(" Call edges: {}", model.edges.symbol_call_edges.len()); + println!(); + + // Symbol kinds + let mut functions = 0; + let mut methods = 0; + let mut classes = 0; + let mut async_functions = 0; + for symbol in model.symbols.values() { + match symbol.kind { + archdoc_core::model::SymbolKind::Function => functions += 1, + archdoc_core::model::SymbolKind::Method => methods += 1, + archdoc_core::model::SymbolKind::Class => classes += 1, + archdoc_core::model::SymbolKind::AsyncFunction => async_functions += 1, + } + } + println!("{}", "Symbol breakdown".bold().underline()); + println!(" Classes: {}", classes); + println!(" Functions: {}", functions); + println!(" Async functions: {}", async_functions); + println!(" Methods: {}", methods); + println!(); + + // Top fan-in + let mut symbols_by_fan_in: Vec<_> = model.symbols.values().collect(); + symbols_by_fan_in.sort_by(|a, b| b.metrics.fan_in.cmp(&a.metrics.fan_in)); + + println!("{}", "Top-10 by fan-in (most called)".bold().underline()); + for (i, sym) in symbols_by_fan_in.iter().take(10).enumerate() { + if sym.metrics.fan_in == 0 { break; } + let critical = if sym.metrics.is_critical { " ⚠ CRITICAL".red().to_string() } else { String::new() }; + println!(" {}. {} (fan-in: {}){}", i + 1, sym.qualname.green(), sym.metrics.fan_in, critical); + } + println!(); + + // Top fan-out + let mut symbols_by_fan_out: Vec<_> = model.symbols.values().collect(); + symbols_by_fan_out.sort_by(|a, b| b.metrics.fan_out.cmp(&a.metrics.fan_out)); + + println!("{}", "Top-10 by fan-out (calls many)".bold().underline()); + for (i, sym) in symbols_by_fan_out.iter().take(10).enumerate() { + if sym.metrics.fan_out == 0 { break; } + let critical = if sym.metrics.is_critical { " ⚠ CRITICAL".red().to_string() } else { String::new() }; + println!(" {}. {} (fan-out: {}){}", i + 1, sym.qualname.green(), sym.metrics.fan_out, critical); + } + println!(); + + // Integrations + let http_symbols: Vec<_> = model.symbols.values().filter(|s| s.integrations_flags.http).collect(); + let db_symbols: Vec<_> = model.symbols.values().filter(|s| s.integrations_flags.db).collect(); + let queue_symbols: Vec<_> = model.symbols.values().filter(|s| s.integrations_flags.queue).collect(); + + if !http_symbols.is_empty() || !db_symbols.is_empty() || !queue_symbols.is_empty() { + println!("{}", "Detected integrations".bold().underline()); + if !http_symbols.is_empty() { + println!(" {} HTTP: {}", "●".yellow(), http_symbols.iter().map(|s| s.qualname.as_str()).collect::>().join(", ")); + } + if !db_symbols.is_empty() { + println!(" {} DB: {}", "●".blue(), db_symbols.iter().map(|s| s.qualname.as_str()).collect::>().join(", ")); + } + if !queue_symbols.is_empty() { + println!(" {} Queue: {}", "●".magenta(), queue_symbols.iter().map(|s| s.qualname.as_str()).collect::>().join(", ")); + } + println!(); + } + + // Cycles + println!("{}", "Cycle detection".bold().underline()); + let mut found_cycles = false; + for edge in &model.edges.module_import_edges { + let has_reverse = model.edges.module_import_edges.iter() + .any(|e| e.from_id == edge.to_id && e.to_id == edge.from_id); + if has_reverse && edge.from_id < edge.to_id { + println!(" {} {} ↔ {}", "⚠".red(), edge.from_id, edge.to_id); + found_cycles = true; + } + } + if !found_cycles { + println!(" {} No cycles detected", "✓".green()); + } +} diff --git a/archdoc-cli/src/main.rs b/archdoc-cli/src/main.rs index f1b4f9e..e5f4421 100644 --- a/archdoc-cli/src/main.rs +++ b/archdoc-cli/src/main.rs @@ -1,9 +1,8 @@ +mod commands; +mod output; + use clap::{Parser, Subcommand}; use anyhow::Result; -use archdoc_core::{Config, ProjectModel, scanner::FileScanner, python_analyzer::PythonAnalyzer}; -use colored::Colorize; -use indicatif::{ProgressBar, ProgressStyle}; -use std::path::Path; #[derive(Parser)] #[command(name = "archdoc")] @@ -12,7 +11,7 @@ use std::path::Path; pub struct Cli { #[command(subcommand)] command: Commands, - + /// Verbose output #[arg(short, long, global = true)] verbose: bool, @@ -22,48 +21,31 @@ pub struct Cli { enum Commands { /// Initialize archdoc in the project Init { - /// Project root directory #[arg(short, long, default_value = ".")] root: String, - - /// Output directory for documentation #[arg(short, long, default_value = "docs/architecture")] out: String, }, - /// Generate or update documentation Generate { - /// Project root directory #[arg(short, long, default_value = ".")] root: String, - - /// Output directory for documentation #[arg(short, long, default_value = "docs/architecture")] out: String, - - /// Configuration file path #[arg(short, long, default_value = "archdoc.toml")] config: String, }, - /// Check if documentation is up to date Check { - /// Project root directory #[arg(short, long, default_value = ".")] root: String, - - /// Configuration file path #[arg(short, long, default_value = "archdoc.toml")] config: String, }, - /// Show project statistics Stats { - /// Project root directory #[arg(short, long, default_value = ".")] root: String, - - /// Configuration file path #[arg(short, long, default_value = "archdoc.toml")] config: String, }, @@ -71,517 +53,27 @@ enum Commands { fn main() -> Result<()> { let cli = Cli::parse(); - + match &cli.command { Commands::Init { root, out } => { - init_project(root, out)?; + commands::init::init_project(root, out)?; } Commands::Generate { root, out, config } => { - let config = load_config(config)?; - let model = analyze_project(root, &config)?; - generate_docs(&model, out, cli.verbose)?; - print_generate_summary(&model); + let config = commands::generate::load_config(config)?; + let model = commands::generate::analyze_project(root, &config)?; + commands::generate::generate_docs(&model, out, cli.verbose)?; + output::print_generate_summary(&model); } Commands::Check { root, config } => { - let config = load_config(config)?; - check_docs_consistency(root, &config)?; + let config = commands::generate::load_config(config)?; + commands::check::check_docs_consistency(root, &config)?; } Commands::Stats { root, config } => { - let config = load_config(config)?; - let model = analyze_project(root, &config)?; - print_stats(&model); + let config = commands::generate::load_config(config)?; + let model = commands::generate::analyze_project(root, &config)?; + commands::stats::print_stats(&model); } } - - Ok(()) -} - -fn init_project(root: &str, out: &str) -> Result<()> { - println!("{}", "Initializing archdoc project...".cyan().bold()); - - let out_path = std::path::Path::new(out); - std::fs::create_dir_all(out_path)?; - std::fs::create_dir_all(out_path.join("modules"))?; - std::fs::create_dir_all(out_path.join("files"))?; - - let layout_md_path = out_path.join("layout.md"); - let layout_md_content = r#"# Repository layout - - -## Manual overrides -- `src/app/` — - - ---- - -## Detected structure - -> Generated. Do not edit inside this block. - -"#; - std::fs::write(&layout_md_path, layout_md_content)?; - - let architecture_md_content = r#"# ARCHITECTURE — - - -## Project summary -**Name:** -**Description:** - -## Key decisions (manual) -- - -## Non-goals (manual) -- - - ---- - -## Document metadata -- **Created:** -- **Updated:** -- **Generated by:** archdoc (cli) v0.1 - ---- - -## Rails / Tooling - -> Generated. Do not edit inside this block. - - - ---- - -## Repository layout (top-level) - -> Generated. Do not edit inside this block. - - - ---- - -## Modules index - -> Generated. Do not edit inside this block. - - - ---- - -## Critical dependency points - -> Generated. Do not edit inside this block. - - - ---- - - -## Change notes (manual) -- - -"#; - - let architecture_md_path = std::path::Path::new(root).join("ARCHITECTURE.md"); - std::fs::write(&architecture_md_path, architecture_md_content)?; - - let config_toml_content = r#"[project] -root = "." -out_dir = "docs/architecture" -entry_file = "ARCHITECTURE.md" -language = "python" - -[scan] -include = ["src", "app", "tests"] -exclude = [ - ".venv", "venv", "__pycache__", ".git", "dist", "build", - ".mypy_cache", ".ruff_cache", ".pytest_cache", "*.egg-info" -] -follow_symlinks = false -max_file_size = "10MB" - -[python] -src_roots = ["src", "."] -include_tests = true -parse_docstrings = true -max_parse_errors = 10 - -[analysis] -resolve_calls = true -resolve_inheritance = false -detect_integrations = true -integration_patterns = [ - { type = "http", patterns = ["requests", "httpx", "aiohttp"] }, - { type = "db", patterns = ["sqlalchemy", "psycopg", "mysql", "sqlite3"] }, - { type = "queue", patterns = ["celery", "kafka", "pika", "redis"] } -] - -[output] -single_file = false -per_file_docs = true -create_directories = true -overwrite_manual_sections = false - -[diff] -update_timestamp_on_change_only = true -hash_algorithm = "sha256" -preserve_manual_content = true - -[thresholds] -critical_fan_in = 20 -critical_fan_out = 20 -high_complexity = 50 - -[rendering] -template_engine = "handlebars" -max_table_rows = 100 -truncate_long_descriptions = true -description_max_length = 200 - -[logging] -level = "info" -file = "archdoc.log" -format = "compact" - -[caching] -enabled = true -cache_dir = ".archdoc/cache" -max_cache_age = "24h" -"#; - - let config_toml_path = std::path::Path::new(root).join("archdoc.toml"); - if !config_toml_path.exists() { - std::fs::write(&config_toml_path, config_toml_content)?; - } - - println!("{} Project initialized!", "✓".green().bold()); - println!(" {} {}", "→".dimmed(), architecture_md_path.display()); - println!(" {} {}", "→".dimmed(), config_toml_path.display()); - println!(" {} {} (directory)", "→".dimmed(), out_path.display()); - - Ok(()) -} - -fn load_config(config_path: &str) -> Result { - Config::load_from_file(Path::new(config_path)) - .map_err(|e| anyhow::anyhow!("Failed to load config: {}", e)) -} - -fn analyze_project(root: &str, config: &Config) -> Result { - println!("{}", "Scanning project...".cyan()); - - let scanner = FileScanner::new(config.clone()); - let python_files = scanner.scan_python_files(std::path::Path::new(root))?; - - println!(" Found {} Python files", python_files.len().to_string().yellow()); - - let analyzer = PythonAnalyzer::new(config.clone()); - - let pb = ProgressBar::new(python_files.len() as u64); - pb.set_style(ProgressStyle::default_bar() - .template(" {spinner:.green} [{bar:30.cyan/dim}] {pos}/{len} {msg}") - .unwrap() - .progress_chars("█▓░")); - - let mut parsed_modules = Vec::new(); - let mut parse_errors = 0; - for file_path in &python_files { - pb.set_message(file_path.file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_default()); - match analyzer.parse_module(file_path) { - Ok(module) => parsed_modules.push(module), - Err(e) => { - parse_errors += 1; - pb.println(format!(" {} Failed to parse {}: {}", "⚠".yellow(), file_path.display(), e)); - } - } - pb.inc(1); - } - pb.finish_and_clear(); - - if parse_errors > 0 { - println!(" {} {} file(s) had parse errors", "⚠".yellow(), parse_errors); - } - - println!("{}", "Resolving symbols...".cyan()); - let model = analyzer.resolve_symbols(&parsed_modules) - .map_err(|e| anyhow::anyhow!("Failed to resolve symbols: {}", e))?; - - Ok(model) -} - -fn sanitize_filename(filename: &str) -> String { - filename - .chars() - .map(|c| match c { - '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_', - c => c, - }) - .collect() -} - -fn generate_docs(model: &ProjectModel, out: &str, verbose: bool) -> Result<()> { - println!("{}", "Generating documentation...".cyan()); - - let out_path = std::path::Path::new(out); - std::fs::create_dir_all(out_path)?; - - let modules_path = out_path.join("modules"); - let files_path = out_path.join("files"); - std::fs::create_dir_all(&modules_path)?; - std::fs::create_dir_all(&files_path)?; - - let renderer = archdoc_core::renderer::Renderer::new(); - let writer = archdoc_core::writer::DiffAwareWriter::new(); - - let output_path = std::path::Path::new(".").join("ARCHITECTURE.md"); - - // Generate module docs - for (module_id, _module) in &model.modules { - let module_doc_path = modules_path.join(format!("{}.md", sanitize_filename(module_id))); - match renderer.render_module_md(model, module_id) { - Ok(module_content) => { - std::fs::write(&module_doc_path, module_content)?; - } - Err(e) => { - eprintln!(" {} Module {}: {}", "⚠".yellow(), module_id, e); - let fallback = format!("# Module: {}\n\nTODO: Add module documentation\n", module_id); - std::fs::write(&module_doc_path, fallback)?; - } - } - } - - // Generate file docs - for (_file_id, file_doc) in &model.files { - let file_doc_path = files_path.join(format!("{}.md", sanitize_filename(&file_doc.path))); - - let mut file_content = format!("# File: {}\n\n", file_doc.path); - file_content.push_str(&format!("- **Module:** {}\n", file_doc.module_id)); - file_content.push_str(&format!("- **Defined symbols:** {}\n", file_doc.symbols.len())); - file_content.push_str(&format!("- **Imports:** {}\n\n", file_doc.imports.len())); - - file_content.push_str("\n## File intent (manual)\n\n\n\n---\n\n"); - - file_content.push_str("## Imports & file-level dependencies\n\n> Generated. Do not edit inside this block.\n"); - for import in &file_doc.imports { - file_content.push_str(&format!("- {}\n", import)); - } - file_content.push_str("\n\n---\n\n"); - - file_content.push_str("## Symbols index\n\n> Generated. Do not edit inside this block.\n"); - for symbol_id in &file_doc.symbols { - if let Some(symbol) = model.symbols.get(symbol_id) { - file_content.push_str(&format!("- `{}` ({})\n", symbol.qualname, format!("{:?}", symbol.kind))); - } - } - file_content.push_str("\n\n---\n\n"); - - file_content.push_str("## Symbol details\n"); - - for symbol_id in &file_doc.symbols { - if model.symbols.contains_key(symbol_id) { - file_content.push_str(&format!("\n\n", symbol_id)); - file_content.push_str("\n"); - file_content.push_str(&format!("\n", symbol_id)); - } - } - - std::fs::write(&file_doc_path, &file_content)?; - - for symbol_id in &file_doc.symbols { - if model.symbols.contains_key(symbol_id) { - match renderer.render_symbol_details(model, symbol_id) { - Ok(content) => { - if verbose { - println!(" Updating symbol section for {}", symbol_id); - } - if let Err(e) = writer.update_symbol_section(&file_doc_path, symbol_id, &content) { - eprintln!(" {} Symbol {}: {}", "⚠".yellow(), symbol_id, e); - } - } - Err(e) => { - eprintln!(" {} Symbol {}: {}", "⚠".yellow(), symbol_id, e); - } - } - } - } - } - - // Update ARCHITECTURE.md sections - let sections = [ - ("integrations", renderer.render_integrations_section(model)), - ("rails", renderer.render_rails_section(model)), - ("layout", renderer.render_layout_section(model)), - ("modules_index", renderer.render_modules_index_section(model)), - ("critical_points", renderer.render_critical_points_section(model)), - ]; - - for (name, result) in sections { - match result { - Ok(content) => { - if let Err(e) = writer.update_file_with_markers(&output_path, &content, name) { - if verbose { - eprintln!(" {} Section {}: {}", "⚠".yellow(), name, e); - } - } - } - Err(e) => { - if verbose { - eprintln!(" {} Section {}: {}", "⚠".yellow(), name, e); - } - } - } - } - - // Update layout.md - let layout_md_path = out_path.join("layout.md"); - if let Ok(content) = renderer.render_layout_md(model) { - let _ = std::fs::write(&layout_md_path, &content); - } - - println!("{} Documentation generated in {}", "✓".green().bold(), out); - - Ok(()) -} - -fn print_generate_summary(model: &ProjectModel) { - println!(); - println!("{}", "── Summary ──────────────────────────".dimmed()); - println!(" {} {}", "Files:".bold(), model.files.len()); - println!(" {} {}", "Modules:".bold(), model.modules.len()); - println!(" {} {}", "Symbols:".bold(), model.symbols.len()); - println!(" {} {}", "Edges:".bold(), - model.edges.module_import_edges.len() + model.edges.symbol_call_edges.len()); - - let integrations: Vec<&str> = { - let mut v = Vec::new(); - if model.symbols.values().any(|s| s.integrations_flags.http) { v.push("HTTP"); } - if model.symbols.values().any(|s| s.integrations_flags.db) { v.push("DB"); } - if model.symbols.values().any(|s| s.integrations_flags.queue) { v.push("Queue"); } - v - }; - if !integrations.is_empty() { - println!(" {} {}", "Integrations:".bold(), integrations.join(", ").yellow()); - } - println!("{}", "─────────────────────────────────────".dimmed()); -} - -fn print_stats(model: &ProjectModel) { - println!(); - println!("{}", "╔══════════════════════════════════════╗".cyan()); - println!("{}", "║ archdoc project statistics ║".cyan().bold()); - println!("{}", "╚══════════════════════════════════════╝".cyan()); - println!(); - - // Basic counts - println!("{}", "Overview".bold().underline()); - println!(" Files: {}", model.files.len().to_string().yellow()); - println!(" Modules: {}", model.modules.len().to_string().yellow()); - println!(" Symbols: {}", model.symbols.len().to_string().yellow()); - println!(" Import edges: {}", model.edges.module_import_edges.len()); - println!(" Call edges: {}", model.edges.symbol_call_edges.len()); - println!(); - - // Symbol kinds - let mut functions = 0; - let mut methods = 0; - let mut classes = 0; - let mut async_functions = 0; - for symbol in model.symbols.values() { - match symbol.kind { - archdoc_core::model::SymbolKind::Function => functions += 1, - archdoc_core::model::SymbolKind::Method => methods += 1, - archdoc_core::model::SymbolKind::Class => classes += 1, - archdoc_core::model::SymbolKind::AsyncFunction => async_functions += 1, - } - } - println!("{}", "Symbol breakdown".bold().underline()); - println!(" Classes: {}", classes); - println!(" Functions: {}", functions); - println!(" Async functions: {}", async_functions); - println!(" Methods: {}", methods); - println!(); - - // Top fan-in - let mut symbols_by_fan_in: Vec<_> = model.symbols.values().collect(); - symbols_by_fan_in.sort_by(|a, b| b.metrics.fan_in.cmp(&a.metrics.fan_in)); - - println!("{}", "Top-10 by fan-in (most called)".bold().underline()); - for (i, sym) in symbols_by_fan_in.iter().take(10).enumerate() { - if sym.metrics.fan_in == 0 { break; } - let critical = if sym.metrics.is_critical { " ⚠ CRITICAL".red().to_string() } else { String::new() }; - println!(" {}. {} (fan-in: {}){}", i + 1, sym.qualname.green(), sym.metrics.fan_in, critical); - } - println!(); - - // Top fan-out - let mut symbols_by_fan_out: Vec<_> = model.symbols.values().collect(); - symbols_by_fan_out.sort_by(|a, b| b.metrics.fan_out.cmp(&a.metrics.fan_out)); - - println!("{}", "Top-10 by fan-out (calls many)".bold().underline()); - for (i, sym) in symbols_by_fan_out.iter().take(10).enumerate() { - if sym.metrics.fan_out == 0 { break; } - let critical = if sym.metrics.is_critical { " ⚠ CRITICAL".red().to_string() } else { String::new() }; - println!(" {}. {} (fan-out: {}){}", i + 1, sym.qualname.green(), sym.metrics.fan_out, critical); - } - println!(); - - // Integrations - let http_symbols: Vec<_> = model.symbols.values().filter(|s| s.integrations_flags.http).collect(); - let db_symbols: Vec<_> = model.symbols.values().filter(|s| s.integrations_flags.db).collect(); - let queue_symbols: Vec<_> = model.symbols.values().filter(|s| s.integrations_flags.queue).collect(); - - if !http_symbols.is_empty() || !db_symbols.is_empty() || !queue_symbols.is_empty() { - println!("{}", "Detected integrations".bold().underline()); - if !http_symbols.is_empty() { - println!(" {} HTTP: {}", "●".yellow(), http_symbols.iter().map(|s| s.qualname.as_str()).collect::>().join(", ")); - } - if !db_symbols.is_empty() { - println!(" {} DB: {}", "●".blue(), db_symbols.iter().map(|s| s.qualname.as_str()).collect::>().join(", ")); - } - if !queue_symbols.is_empty() { - println!(" {} Queue: {}", "●".magenta(), queue_symbols.iter().map(|s| s.qualname.as_str()).collect::>().join(", ")); - } - println!(); - } - - // Cycles (basic detection via module import edges) - println!("{}", "Cycle detection".bold().underline()); - let mut found_cycles = false; - for edge in &model.edges.module_import_edges { - // Check if there's a reverse edge - let has_reverse = model.edges.module_import_edges.iter() - .any(|e| e.from_id == edge.to_id && e.to_id == edge.from_id); - if has_reverse && edge.from_id < edge.to_id { - println!(" {} {} ↔ {}", "⚠".red(), edge.from_id, edge.to_id); - found_cycles = true; - } - } - if !found_cycles { - println!(" {} No cycles detected", "✓".green()); - } -} - -fn check_docs_consistency(root: &str, config: &Config) -> Result<()> { - println!("{}", "Checking documentation consistency...".cyan()); - - let model = analyze_project(root, config)?; - - let renderer = archdoc_core::renderer::Renderer::new(); - let _generated = renderer.render_architecture_md(&model)?; - - let architecture_md_path = std::path::Path::new(root).join(&config.project.entry_file); - if !architecture_md_path.exists() { - println!("{} {} does not exist", "✗".red().bold(), architecture_md_path.display()); - return Err(anyhow::anyhow!("Documentation file does not exist")); - } - - let existing = std::fs::read_to_string(&architecture_md_path)?; - - println!("{} Documentation is parseable and consistent", "✓".green().bold()); - println!(" Generated content: {} chars", _generated.len()); - println!(" Existing content: {} chars", existing.len()); - + Ok(()) } diff --git a/archdoc-cli/src/output.rs b/archdoc-cli/src/output.rs new file mode 100644 index 0000000..6d90224 --- /dev/null +++ b/archdoc-cli/src/output.rs @@ -0,0 +1,33 @@ +//! Colored output helpers and filename utilities for ArchDoc CLI + +use colored::Colorize; +use archdoc_core::ProjectModel; + +/// Sanitize a file path into a safe filename for docs. +/// Removes `./` prefix, replaces `/` with `__`. +pub fn sanitize_filename(filename: &str) -> String { + let cleaned = filename.strip_prefix("./").unwrap_or(filename); + cleaned.replace('/', "__") +} + +pub fn print_generate_summary(model: &ProjectModel) { + println!(); + println!("{}", "── Summary ──────────────────────────".dimmed()); + println!(" {} {}", "Files:".bold(), model.files.len()); + println!(" {} {}", "Modules:".bold(), model.modules.len()); + println!(" {} {}", "Symbols:".bold(), model.symbols.len()); + println!(" {} {}", "Edges:".bold(), + model.edges.module_import_edges.len() + model.edges.symbol_call_edges.len()); + + let integrations: Vec<&str> = { + let mut v = Vec::new(); + if model.symbols.values().any(|s| s.integrations_flags.http) { v.push("HTTP"); } + if model.symbols.values().any(|s| s.integrations_flags.db) { v.push("DB"); } + if model.symbols.values().any(|s| s.integrations_flags.queue) { v.push("Queue"); } + v + }; + if !integrations.is_empty() { + println!(" {} {}", "Integrations:".bold(), integrations.join(", ").yellow()); + } + println!("{}", "─────────────────────────────────────".dimmed()); +} diff --git a/archdoc-core/src/cache.rs b/archdoc-core/src/cache.rs index e7a1258..3b6b683 100644 --- a/archdoc-core/src/cache.rs +++ b/archdoc-core/src/cache.rs @@ -53,7 +53,7 @@ impl CacheManager { // Read cache file let content = fs::read_to_string(&cache_file) - .map_err(|e| ArchDocError::Io(e))?; + .map_err(ArchDocError::Io)?; let cache_entry: CacheEntry = serde_json::from_str(&content) .map_err(|e| ArchDocError::AnalysisError(format!("Failed to deserialize cache entry: {}", e)))?; @@ -73,10 +73,10 @@ impl CacheManager { // Check if source file has been modified since caching let metadata = fs::metadata(file_path) - .map_err(|e| ArchDocError::Io(e))?; + .map_err(ArchDocError::Io)?; let modified_time = metadata.modified() - .map_err(|e| ArchDocError::Io(e))?; + .map_err(ArchDocError::Io)?; let modified_time: DateTime = modified_time.into(); @@ -100,10 +100,10 @@ impl CacheManager { // Get file modification time let metadata = fs::metadata(file_path) - .map_err(|e| ArchDocError::Io(e))?; + .map_err(ArchDocError::Io)?; let modified_time = metadata.modified() - .map_err(|e| ArchDocError::Io(e))?; + .map_err(ArchDocError::Io)?; let modified_time: DateTime = modified_time.into(); @@ -117,7 +117,7 @@ impl CacheManager { .map_err(|e| ArchDocError::AnalysisError(format!("Failed to serialize cache entry: {}", e)))?; fs::write(&cache_file, content) - .map_err(|e| ArchDocError::Io(e)) + .map_err(ArchDocError::Io) } /// Generate cache key for a file path @@ -156,11 +156,11 @@ impl CacheManager { pub fn clear_cache(&self) -> Result<(), ArchDocError> { if Path::new(&self.cache_dir).exists() { fs::remove_dir_all(&self.cache_dir) - .map_err(|e| ArchDocError::Io(e))?; + .map_err(ArchDocError::Io)?; // Recreate cache directory fs::create_dir_all(&self.cache_dir) - .map_err(|e| ArchDocError::Io(e))?; + .map_err(ArchDocError::Io)?; } Ok(()) diff --git a/archdoc-core/src/config.rs b/archdoc-core/src/config.rs index 84f6a3e..385f51d 100644 --- a/archdoc-core/src/config.rs +++ b/archdoc-core/src/config.rs @@ -7,6 +7,7 @@ use std::path::Path; use crate::errors::ArchDocError; #[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Default)] pub struct Config { #[serde(default)] pub project: ProjectConfig, @@ -30,22 +31,6 @@ pub struct Config { pub caching: CachingConfig, } -impl Default for Config { - fn default() -> Self { - Self { - project: ProjectConfig::default(), - scan: ScanConfig::default(), - python: PythonConfig::default(), - analysis: AnalysisConfig::default(), - output: OutputConfig::default(), - diff: DiffConfig::default(), - thresholds: ThresholdsConfig::default(), - rendering: RenderingConfig::default(), - logging: LoggingConfig::default(), - caching: CachingConfig::default(), - } - } -} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProjectConfig { diff --git a/archdoc-core/src/python_analyzer.rs b/archdoc-core/src/python_analyzer.rs index 83f80c2..50770f3 100644 --- a/archdoc-core/src/python_analyzer.rs +++ b/archdoc-core/src/python_analyzer.rs @@ -13,14 +13,14 @@ use rustpython_parser::{ast, Parse}; use rustpython_ast::{Stmt, Expr, Ranged}; pub struct PythonAnalyzer { - _config: Config, + config: Config, cache_manager: CacheManager, } impl PythonAnalyzer { pub fn new(config: Config) -> Self { let cache_manager = CacheManager::new(config.clone()); - Self { _config: config, cache_manager } + Self { config, cache_manager } } pub fn parse_module(&self, file_path: &Path) -> Result { @@ -67,7 +67,7 @@ impl PythonAnalyzer { imports: &mut Vec, symbols: &mut Vec, calls: &mut Vec, - depth: usize, + _depth: usize, ) { match stmt { Stmt::Import(import_stmt) => { @@ -104,7 +104,7 @@ impl PythonAnalyzer { }; let signature = self.build_function_signature(&func_def.name, &func_def.args); - let integrations_flags = self.detect_integrations(&func_def.body, &self._config); + let integrations_flags = self.detect_integrations(&func_def.body, &self.config); let docstring = self.extract_docstring(&func_def.body); let symbol = Symbol { @@ -130,7 +130,7 @@ impl PythonAnalyzer { symbols.push(symbol); for body_stmt in &func_def.body { - self.extract_from_statement(body_stmt, parent_class, imports, symbols, calls, depth + 1); + self.extract_from_statement(body_stmt, parent_class, imports, symbols, calls, _depth + 1); } // Extract calls from body expressions recursively self.extract_calls_from_body(&func_def.body, Some(&qualname), calls); @@ -143,7 +143,7 @@ impl PythonAnalyzer { }; let signature = format!("async {}", self.build_function_signature(&func_def.name, &func_def.args)); - let integrations_flags = self.detect_integrations(&func_def.body, &self._config); + let integrations_flags = self.detect_integrations(&func_def.body, &self.config); let docstring = self.extract_docstring(&func_def.body); let symbol = Symbol { @@ -169,12 +169,12 @@ impl PythonAnalyzer { symbols.push(symbol); for body_stmt in &func_def.body { - self.extract_from_statement(body_stmt, parent_class, imports, symbols, calls, depth + 1); + self.extract_from_statement(body_stmt, parent_class, imports, symbols, calls, _depth + 1); } self.extract_calls_from_body(&func_def.body, Some(&qualname), calls); } Stmt::ClassDef(class_def) => { - let integrations_flags = self.detect_integrations(&class_def.body, &self._config); + let integrations_flags = self.detect_integrations(&class_def.body, &self.config); let docstring = self.extract_docstring(&class_def.body); let symbol = Symbol { @@ -201,7 +201,7 @@ impl PythonAnalyzer { // Process class body with class name as parent for body_stmt in &class_def.body { - self.extract_from_statement(body_stmt, Some(&class_def.name), imports, symbols, calls, depth + 1); + self.extract_from_statement(body_stmt, Some(&class_def.name), imports, symbols, calls, _depth + 1); } } Stmt::Expr(expr_stmt) => { @@ -346,10 +346,10 @@ impl PythonAnalyzer { } fn extract_docstring(&self, body: &[Stmt]) -> Option { - if let Some(first_stmt) = body.first() { - if let Stmt::Expr(expr_stmt) = first_stmt { - if let Expr::Constant(constant_expr) = &*expr_stmt.value { - if let Some(docstring) = constant_expr.value.as_str() { + if let Some(first_stmt) = body.first() + && let Stmt::Expr(expr_stmt) = first_stmt + && let Expr::Constant(constant_expr) = &*expr_stmt.value + && let Some(docstring) = constant_expr.value.as_str() { // Return full docstring, trimmed let trimmed = docstring.trim(); if trimmed.is_empty() { @@ -357,9 +357,6 @@ impl PythonAnalyzer { } return Some(trimmed.to_string()); } - } - } - } None } @@ -446,10 +443,8 @@ impl PythonAnalyzer { self.extract_from_expression(&if_exp.orelse, current_symbol, calls); } Expr::Dict(dict_expr) => { - for key in &dict_expr.keys { - if let Some(k) = key { - self.extract_from_expression(k, current_symbol, calls); - } + for k in dict_expr.keys.iter().flatten() { + self.extract_from_expression(k, current_symbol, calls); } for value in &dict_expr.values { self.extract_from_expression(value, current_symbol, calls); @@ -522,6 +517,55 @@ impl PythonAnalyzer { } } + /// Compute Python module path from file path using src_roots from config. + /// E.g. `./src/core.py` with src_root `src` → `core` + /// `./src/__init__.py` with src_root `src` → `src` (package) + /// `back-end/services/chat/agent.py` with src_root `.` → `back-end.services.chat.agent` + fn compute_module_path(&self, file_path: &Path) -> String { + let path_str = file_path.to_string_lossy().to_string(); + // Normalize: strip leading ./ + let normalized = path_str.strip_prefix("./").unwrap_or(&path_str); + let path = std::path::Path::new(normalized); + + for src_root in &self.config.python.src_roots { + let root = if src_root == "." { + std::path::Path::new("") + } else { + std::path::Path::new(src_root) + }; + + let relative = if root == std::path::Path::new("") { + Some(path.to_path_buf()) + } else { + path.strip_prefix(root).ok().map(|p| p.to_path_buf()) + }; + + if let Some(rel) = relative { + let rel_str = rel.to_string_lossy().to_string(); + // Check if it's an __init__.py → use the parent directory name as module + if rel.file_name().map(|f| f == "__init__.py").unwrap_or(false) + && let Some(parent) = rel.parent() { + if parent == std::path::Path::new("") { + // __init__.py at src_root level → use src_root as module name + if src_root == "." { + return "__init__".to_string(); + } + return src_root.replace('/', "."); + } + return parent.to_string_lossy().replace(['/', '\\'], "."); + } + + // Strip .py extension and convert path separators to dots + let without_ext = rel_str.strip_suffix(".py").unwrap_or(&rel_str); + let module_path = without_ext.replace(['/', '\\'], "."); + return module_path; + } + } + + // Fallback: use file path as-is + normalized.to_string() + } + pub fn resolve_symbols(&self, modules: &[ParsedModule]) -> Result { let mut project_model = ProjectModel::new(); @@ -537,7 +581,7 @@ impl PythonAnalyzer { } for parsed_module in modules { - let module_id = parsed_module.module_path.clone(); + let module_id = self.compute_module_path(&parsed_module.path); let file_id = parsed_module.path.to_string_lossy().to_string(); let file_doc = FileDoc { @@ -625,7 +669,7 @@ impl PythonAnalyzer { fn build_dependency_graphs(&self, project_model: &mut ProjectModel, parsed_modules: &[ParsedModule]) -> Result<(), ArchDocError> { for parsed_module in parsed_modules { - let from_module_id = parsed_module.module_path.clone(); + let from_module_id = self.compute_module_path(&parsed_module.path); for import in &parsed_module.imports { let to_module_id = import.module_name.clone(); diff --git a/archdoc-core/src/renderer.rs b/archdoc-core/src/renderer.rs index 6e20ef2..7064a14 100644 --- a/archdoc-core/src/renderer.rs +++ b/archdoc-core/src/renderer.rs @@ -20,6 +20,12 @@ pub struct Renderer { templates: Handlebars<'static>, } +impl Default for Renderer { + fn default() -> Self { + Self::new() + } +} + impl Renderer { pub fn new() -> Self { let mut handlebars = Handlebars::new(); @@ -393,7 +399,7 @@ impl Renderer { // Collect layout information from files let mut layout_items = Vec::new(); - for (_file_id, file_doc) in &model.files { + for file_doc in model.files.values() { layout_items.push(serde_json::json!({ "path": file_doc.path, "purpose": "Source file", @@ -525,7 +531,7 @@ impl Renderer { // Collect layout information from files let mut layout_items = Vec::new(); - for (_file_id, file_doc) in &model.files { + for file_doc in model.files.values() { layout_items.push(serde_json::json!({ "path": file_doc.path, "purpose": "Source file", diff --git a/archdoc-core/src/scanner.rs b/archdoc-core/src/scanner.rs index cfc180c..03db5f7 100644 --- a/archdoc-core/src/scanner.rs +++ b/archdoc-core/src/scanner.rs @@ -41,8 +41,7 @@ impl FileScanner { .into_iter() { let entry = entry.map_err(|e| { - ArchDocError::Io(std::io::Error::new( - std::io::ErrorKind::Other, + ArchDocError::Io(std::io::Error::other( format!("Failed to read directory entry: {}", e) )) })?; @@ -51,11 +50,7 @@ impl FileScanner { // Skip excluded paths if self.is_excluded(path) { - if path.is_dir() { - continue; - } else { - continue; - } + continue; } // Include Python files diff --git a/archdoc-core/src/writer.rs b/archdoc-core/src/writer.rs index 5dbb74a..f1a2f82 100644 --- a/archdoc-core/src/writer.rs +++ b/archdoc-core/src/writer.rs @@ -26,6 +26,12 @@ pub struct DiffAwareWriter { // Configuration } +impl Default for DiffAwareWriter { + fn default() -> Self { + Self::new() + } +} + impl DiffAwareWriter { pub fn new() -> Self { Self {} @@ -40,13 +46,13 @@ impl DiffAwareWriter { // Read existing file let existing_content = if file_path.exists() { fs::read_to_string(file_path) - .map_err(|e| ArchDocError::Io(e))? + .map_err(ArchDocError::Io)? } else { // Create new file with template let template_content = self.create_template_file(file_path, section_name)?; // Write template to file fs::write(file_path, &template_content) - .map_err(|e| ArchDocError::Io(e))?; + .map_err(ArchDocError::Io)?; template_content }; @@ -68,12 +74,12 @@ impl DiffAwareWriter { if content_changed { let updated_content = self.update_timestamp(new_content)?; fs::write(file_path, updated_content) - .map_err(|e| ArchDocError::Io(e))?; + .map_err(ArchDocError::Io)?; } else { // Content hasn't changed, but we might still need to update timestamp // TODO: Implement timestamp update logic based on config fs::write(file_path, new_content) - .map_err(|e| ArchDocError::Io(e))?; + .map_err(ArchDocError::Io)?; } } @@ -89,12 +95,12 @@ impl DiffAwareWriter { // Read existing file let existing_content = if file_path.exists() { fs::read_to_string(file_path) - .map_err(|e| ArchDocError::Io(e))? + .map_err(ArchDocError::Io)? } else { // If file doesn't exist, create it with a basic template let template_content = self.create_template_file(file_path, "symbol")?; fs::write(file_path, &template_content) - .map_err(|e| ArchDocError::Io(e))?; + .map_err(ArchDocError::Io)?; template_content }; @@ -116,12 +122,12 @@ impl DiffAwareWriter { if content_changed { let updated_content = self.update_timestamp(new_content)?; fs::write(file_path, updated_content) - .map_err(|e| ArchDocError::Io(e))?; + .map_err(ArchDocError::Io)?; } else { // Content hasn't changed, but we might still need to update timestamp // TODO: Implement timestamp update logic based on config fs::write(file_path, new_content) - .map_err(|e| ArchDocError::Io(e))?; + .map_err(ArchDocError::Io)?; } } else { eprintln!("Warning: No symbol marker found for {} in {}", symbol_id, file_path.display());