feat: smart integration detection with package classifier

- Add PackageClassifier with built-in dictionary (~200 popular packages)
- Hardcode Python 3.10+ stdlib list to filter out standard library imports
- Add PyPI API lookup for unknown packages (online mode, 3s timeout)
- Cache PyPI results in .wtismycode/cache/pypi.json
- Add --offline flag to skip PyPI lookups
- Classify packages into: HTTP, Database, Queue, Storage, AI/ML, Auth, Testing, Logging, Internal, Third-party
- User config integration_patterns override auto-detection
- Update renderer to show integrations grouped by category
- Update ARCHITECTURE.md template with new integration format
This commit is contained in:
2026-02-15 12:45:56 +03:00
parent f4f8b8fa34
commit b3eb591809
13 changed files with 800 additions and 192 deletions

View File

@@ -12,6 +12,10 @@ pub fn load_config(config_path: &str) -> Result<Config> {
}
pub fn analyze_project(root: &str, config: &Config) -> Result<ProjectModel> {
analyze_project_with_options(root, config, false)
}
pub fn analyze_project_with_options(root: &str, config: &Config, offline: bool) -> Result<ProjectModel> {
println!("{}", "Scanning project...".cyan());
let scanner = FileScanner::new(config.clone());
@@ -19,7 +23,7 @@ pub fn analyze_project(root: &str, config: &Config) -> Result<ProjectModel> {
println!(" Found {} Python files", python_files.len().to_string().yellow());
let analyzer = PythonAnalyzer::new(config.clone());
let analyzer = PythonAnalyzer::new_with_options(config.clone(), offline);
let pb = ProgressBar::new(python_files.len() as u64);
pb.set_style(ProgressStyle::default_bar()

View File

@@ -37,6 +37,9 @@ enum Commands {
/// Show what would be generated without writing files
#[arg(long)]
dry_run: bool,
/// Skip PyPI API lookups, use only built-in dictionary
#[arg(long)]
offline: bool,
},
/// Check if documentation is up to date
Check {
@@ -61,9 +64,9 @@ fn main() -> Result<()> {
Commands::Init { root, out } => {
commands::init::init_project(root, out)?;
}
Commands::Generate { root, out, config, dry_run } => {
Commands::Generate { root, out, config, dry_run, offline } => {
let config = commands::generate::load_config(config)?;
let model = commands::generate::analyze_project(root, &config)?;
let model = commands::generate::analyze_project_with_options(root, &config, *offline)?;
if *dry_run {
commands::generate::dry_run_docs(&model, out, &config)?;
} else {

View File

@@ -19,17 +19,14 @@ pub fn print_generate_summary(model: &ProjectModel) {
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"); }
if model.symbols.values().any(|s| s.integrations_flags.storage) { v.push("Storage"); }
if model.symbols.values().any(|s| s.integrations_flags.ai) { v.push("AI/ML"); }
v
};
if !integrations.is_empty() {
println!(" {} {}", "Integrations:".bold(), integrations.join(", ").yellow());
if !model.classified_integrations.is_empty() {
let cats: Vec<String> = model.classified_integrations.iter()
.filter(|(_, pkgs)| !pkgs.is_empty())
.map(|(cat, pkgs)| format!("{} ({})", cat, pkgs.join(", ")))
.collect();
if !cats.is_empty() {
println!(" {} {}", "Integrations:".bold(), cats.join(" | ").yellow());
}
}
println!("{}", "─────────────────────────────────────".dimmed());
}