Compare commits
10 Commits
9f823d2a2a
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| f4f8b8fa34 | |||
| 136697caf0 | |||
| 8e79e3950f | |||
| a3ee003947 | |||
| c095560e13 | |||
| 25fdf400fa | |||
| df52f80999 | |||
| 73154e5865 | |||
| d237650f47 | |||
| 40f87f4d61 |
26
CHANGELOG.md
Normal file
26
CHANGELOG.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to WTIsMyCode are documented in this file.
|
||||||
|
|
||||||
|
Format follows [Keep a Changelog](https://keepachangelog.com/).
|
||||||
|
|
||||||
|
## [Unreleased] — feature/improvements-v2
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Config validation** (`Config::validate()`) — checks project root, language, scan includes, src_roots, cache age, and file size formats with helpful error messages
|
||||||
|
- **Duration & file size parsers** — `parse_duration()` (s/m/h/d/w) and `parse_file_size()` (B/KB/MB/GB) utility functions
|
||||||
|
- **Dependency cycle detection** (`cycle_detector.rs`) — DFS-based algorithm to find circular module dependencies
|
||||||
|
- **Cycle detection in renderer** — Critical points section now shows detected dependency cycles
|
||||||
|
- **Full pipeline integration tests** — Tests for config validation, scanning, cycle detection, and rendering
|
||||||
|
- **Stats command** — `wtismycode stats` displays project-level statistics (files, modules, symbols, edges)
|
||||||
|
- **Check command** — `wtismycode check` verifies documentation consistency with code
|
||||||
|
- **Colored CLI output** — Progress bars and colored status messages
|
||||||
|
- **Comprehensive README** — Badges, configuration reference table, command documentation, architecture overview
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **CLI architecture** — Decomposed into separate command modules (generate, check, stats, init)
|
||||||
|
- **Error handling** — Improved error messages with `thiserror` and `anyhow`
|
||||||
|
- **Clippy compliance** — All warnings resolved
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Various clippy warnings and code style issues
|
||||||
2090
Cargo.lock
generated
Normal file
2090
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["archdoc-cli", "archdoc-core"]
|
members = ["wtismycode-cli", "wtismycode-core"]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|||||||
51
PR_DESCRIPTION.md
Normal file
51
PR_DESCRIPTION.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# PR: Major improvements to WTIsMyCode
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Comprehensive refactoring and feature additions to WTIsMyCode — the Python architecture documentation generator. This PR improves code quality, adds new features, and significantly enhances the development experience.
|
||||||
|
|
||||||
|
**Stats:** 24 files changed, ~3900 insertions, ~1400 deletions, 50 tests
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### 🏗️ Architecture
|
||||||
|
- **Decomposed monolithic `main.rs`** into `commands/` module structure (generate, init, check, stats)
|
||||||
|
- **Added workspace `Cargo.toml`** for unified builds across both crates
|
||||||
|
- **New `cycle_detector` module** with DFS-based dependency cycle detection
|
||||||
|
|
||||||
|
### 🐍 Python Analyzer
|
||||||
|
- **Full AST traversal** — properly walks all statement types (if/for/while/try/with/match)
|
||||||
|
- **Function signatures** — extracts parameter names, types, defaults, return types
|
||||||
|
- **Method detection** — distinguishes methods from standalone functions via `self`/`cls` parameter
|
||||||
|
- **Docstring extraction** — parses first line of docstrings for symbol documentation
|
||||||
|
- **Module path computation** — correctly computes module IDs from `src_roots` config
|
||||||
|
|
||||||
|
### ✨ New Features
|
||||||
|
- **`stats` command** — project statistics with colored output and progress bar
|
||||||
|
- **Config validation** — validates project root, language, scan paths, cache age, file size formats
|
||||||
|
- **Cycle detection** — finds circular dependencies in module graph, shown in critical points section
|
||||||
|
- **`--dry-run` flag** — preview what would be generated without writing files
|
||||||
|
- **Dynamic project data** — uses config project name and current date instead of hardcoded values
|
||||||
|
- **Real usage examples** — generates Python import/call examples from analyzed symbols
|
||||||
|
- **Skip-unchanged optimization** — writer skips files that haven't changed
|
||||||
|
|
||||||
|
### 🧹 Code Quality
|
||||||
|
- **Zero `unwrap()` calls** in non-test code — proper error handling throughout
|
||||||
|
- **Zero clippy warnings** — all lints resolved
|
||||||
|
- **50 tests** — unit tests for config validation, cycle detection, caching, integration detection, error handling, and full pipeline integration tests
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
- **README.md** — badges, full command reference, configuration table, architecture overview
|
||||||
|
- **CHANGELOG.md** — complete changelog for this branch
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test # 50 tests, all passing
|
||||||
|
cargo clippy # 0 warnings
|
||||||
|
cargo build # clean build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
None. All existing functionality preserved.
|
||||||
184
README.md
184
README.md
@@ -1,68 +1,145 @@
|
|||||||
# ArchDoc
|
# WTIsMyCode
|
||||||
|
|
||||||
ArchDoc is a tool for automatically generating architecture documentation for Python projects. It analyzes your Python codebase and creates comprehensive documentation that helps developers understand the structure, dependencies, and key components of the project.
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
**Automatic architecture documentation generator for Python projects.**
|
||||||
|
|
||||||
|
WTIsMyCode analyzes your Python codebase using AST parsing and generates comprehensive Markdown documentation covering module structure, dependencies, integration points, and critical hotspots.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Automatic Documentation Generation**: Automatically generates architecture documentation from Python source code
|
- **AST-Based Analysis** — Full Python AST traversal for imports, classes, functions, calls, and docstrings
|
||||||
- **AST-Based Analysis**: Uses Python AST to extract imports, definitions, and function calls
|
- **Dependency Graph** — Module-level and file-level dependency tracking with cycle detection
|
||||||
- **Diff-Aware Updates**: Preserves manual content while updating generated sections
|
- **Integration Detection** — Automatically identifies HTTP, database, and message queue integrations
|
||||||
- **Caching**: Caches analysis results for faster subsequent runs
|
- **Diff-Aware Updates** — Preserves manually written sections while regenerating docs
|
||||||
- **Configurable**: Highly configurable through `archdoc.toml`
|
- **Caching** — Content-hash based caching for fast incremental regeneration
|
||||||
- **Template-Based Rendering**: Uses Handlebars templates for customizable output
|
- **Config Validation** — Comprehensive validation of `wtismycode.toml` with helpful error messages
|
||||||
|
- **Statistics** — Project-level stats: file counts, symbol counts, fan-in/fan-out metrics
|
||||||
|
- **Consistency Checks** — Verify documentation stays in sync with code changes
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
To install ArchDoc, you'll need Rust installed on your system. Then run:
|
Requires Rust 1.85+:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo install --path archdoc-cli
|
cargo install --path wtismycode-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Quick Start
|
||||||
|
|
||||||
### Initialize Configuration
|
|
||||||
|
|
||||||
First, initialize the configuration in your project:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
archdoc init
|
# Initialize config in your Python project
|
||||||
|
wtismycode init
|
||||||
|
|
||||||
|
# Generate architecture docs
|
||||||
|
wtismycode generate
|
||||||
|
|
||||||
|
# View project statistics
|
||||||
|
wtismycode stats
|
||||||
|
|
||||||
|
# Check docs are up-to-date
|
||||||
|
wtismycode check
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates an `archdoc.toml` file with default settings.
|
## Commands
|
||||||
|
|
||||||
### Generate Documentation
|
### `wtismycode generate`
|
||||||
|
|
||||||
Generate architecture documentation for your project:
|
Scans the project, analyzes Python files, and generates documentation:
|
||||||
|
|
||||||
```bash
|
```
|
||||||
archdoc generate
|
$ wtismycode generate
|
||||||
|
🔍 Scanning project...
|
||||||
|
📂 Found 24 Python files in 6 modules
|
||||||
|
🔬 Analyzing dependencies...
|
||||||
|
📝 Generating documentation...
|
||||||
|
✅ Generated docs/architecture/ARCHITECTURE.md
|
||||||
|
✅ Generated 6 module docs
|
||||||
```
|
```
|
||||||
|
|
||||||
This will create documentation files in the configured output directory.
|
Output includes:
|
||||||
|
- **ARCHITECTURE.md** — Top-level overview with module index, dependency graph, and critical points
|
||||||
|
- **Per-module docs** — Detailed documentation for each module with symbols, imports, and metrics
|
||||||
|
- **Integration map** — HTTP, database, and queue integration points
|
||||||
|
- **Critical points** — High fan-in/fan-out symbols and dependency cycles
|
||||||
|
|
||||||
### Check Documentation Consistency
|
### `wtismycode stats`
|
||||||
|
|
||||||
Verify that your documentation is consistent with the code:
|
Displays project statistics without generating docs:
|
||||||
|
|
||||||
```bash
|
```
|
||||||
archdoc check
|
$ wtismycode stats
|
||||||
|
📊 Project Statistics
|
||||||
|
Files: 24
|
||||||
|
Modules: 6
|
||||||
|
Classes: 12
|
||||||
|
Functions: 47
|
||||||
|
Imports: 89
|
||||||
|
Edges: 134
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
### `wtismycode check`
|
||||||
|
|
||||||
ArchDoc is configured through an `archdoc.toml` file. Here's an example configuration:
|
Verifies documentation consistency with the current codebase:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ wtismycode check
|
||||||
|
✅ Documentation is up-to-date
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns non-zero exit code if docs are stale — useful in CI pipelines.
|
||||||
|
|
||||||
|
### `wtismycode init`
|
||||||
|
|
||||||
|
Creates a default `wtismycode.toml` configuration file:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ wtismycode init
|
||||||
|
✅ Created wtismycode.toml with default settings
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
WTIsMyCode is configured via `wtismycode.toml`:
|
||||||
|
|
||||||
|
| Section | Key | Default | Description |
|
||||||
|
|---------|-----|---------|-------------|
|
||||||
|
| `project` | `root` | `"."` | Project root directory |
|
||||||
|
| `project` | `out_dir` | `"docs/architecture"` | Output directory for generated docs |
|
||||||
|
| `project` | `entry_file` | `"ARCHITECTURE.md"` | Main documentation file name |
|
||||||
|
| `project` | `language` | `"python"` | Project language (only `python` supported) |
|
||||||
|
| `scan` | `include` | `["src", "app", "tests"]` | Directories to scan |
|
||||||
|
| `scan` | `exclude` | `[".venv", "__pycache__", ...]` | Directories to skip |
|
||||||
|
| `scan` | `max_file_size` | `"10MB"` | Skip files larger than this (supports KB, MB, GB) |
|
||||||
|
| `scan` | `follow_symlinks` | `false` | Whether to follow symbolic links |
|
||||||
|
| `python` | `src_roots` | `["src", "."]` | Python source roots for import resolution |
|
||||||
|
| `python` | `include_tests` | `true` | Include test files in analysis |
|
||||||
|
| `python` | `parse_docstrings` | `true` | Extract docstrings from symbols |
|
||||||
|
| `python` | `max_parse_errors` | `10` | Max parse errors before aborting |
|
||||||
|
| `analysis` | `resolve_calls` | `true` | Resolve function call targets |
|
||||||
|
| `analysis` | `detect_integrations` | `true` | Detect HTTP/DB/queue integrations |
|
||||||
|
| `output` | `single_file` | `false` | Generate everything in one file |
|
||||||
|
| `output` | `per_file_docs` | `true` | Generate per-module documentation |
|
||||||
|
| `thresholds` | `critical_fan_in` | `20` | Fan-in threshold for critical symbols |
|
||||||
|
| `thresholds` | `critical_fan_out` | `20` | Fan-out threshold for critical symbols |
|
||||||
|
| `caching` | `enabled` | `true` | Enable analysis caching |
|
||||||
|
| `caching` | `cache_dir` | `".wtismycode/cache"` | Cache directory |
|
||||||
|
| `caching` | `max_cache_age` | `"24h"` | Cache TTL (supports s, m, h, d, w) |
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
root = "."
|
root = "."
|
||||||
out_dir = "docs/architecture"
|
out_dir = "docs/architecture"
|
||||||
entry_file = "ARCHITECTURE.md"
|
|
||||||
language = "python"
|
language = "python"
|
||||||
|
|
||||||
[scan]
|
[scan]
|
||||||
include = ["src"]
|
include = ["src", "app"]
|
||||||
exclude = [".venv", "venv", "__pycache__", ".git", "dist", "build"]
|
exclude = [".venv", "__pycache__", ".git"]
|
||||||
|
max_file_size = "10MB"
|
||||||
|
|
||||||
[python]
|
[python]
|
||||||
src_roots = ["src"]
|
src_roots = ["src"]
|
||||||
@@ -72,25 +149,46 @@ parse_docstrings = true
|
|||||||
[analysis]
|
[analysis]
|
||||||
resolve_calls = true
|
resolve_calls = true
|
||||||
detect_integrations = true
|
detect_integrations = true
|
||||||
|
integration_patterns = [
|
||||||
[output]
|
{ type = "http", patterns = ["requests", "httpx", "aiohttp"] },
|
||||||
single_file = false
|
{ type = "db", patterns = ["sqlalchemy", "psycopg", "sqlite3"] },
|
||||||
per_file_docs = true
|
{ type = "queue", patterns = ["celery", "kafka", "redis"] }
|
||||||
create_directories = true
|
]
|
||||||
|
|
||||||
[caching]
|
[caching]
|
||||||
enabled = true
|
enabled = true
|
||||||
cache_dir = ".archdoc/cache"
|
|
||||||
max_cache_age = "24h"
|
max_cache_age = "24h"
|
||||||
```
|
```
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
1. **Scanning**: ArchDoc scans your project directory for Python files based on the configuration
|
1. **Scan** — Walks the project tree, filtering by include/exclude patterns
|
||||||
2. **Parsing**: It parses each Python file using AST to extract structure and relationships
|
2. **Parse** — Parses each Python file with a full AST traversal (via `rustpython-parser`)
|
||||||
3. **Analysis**: It analyzes the code to identify imports, definitions, and function calls
|
3. **Analyze** — Builds a project model with modules, symbols, edges, and metrics
|
||||||
4. **Documentation Generation**: It generates documentation using templates
|
4. **Detect** — Identifies integration points (HTTP, DB, queues) and dependency cycles
|
||||||
5. **Output**: It writes the documentation to files, preserving manual content
|
5. **Render** — Generates Markdown using Handlebars templates
|
||||||
|
6. **Write** — Outputs files with diff-aware updates preserving manual sections
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
wtismycode/
|
||||||
|
├── wtismycode-cli/ # CLI binary (commands, output formatting)
|
||||||
|
│ └── src/
|
||||||
|
│ ├── main.rs
|
||||||
|
│ └── commands/ # generate, check, stats, init
|
||||||
|
├── wtismycode-core/ # Core library
|
||||||
|
│ └── src/
|
||||||
|
│ ├── config.rs # Config loading & validation
|
||||||
|
│ ├── scanner.rs # File discovery
|
||||||
|
│ ├── python_analyzer.rs # AST analysis
|
||||||
|
│ ├── model.rs # Project IR (modules, symbols, edges)
|
||||||
|
│ ├── cycle_detector.rs # Dependency cycle detection
|
||||||
|
│ ├── renderer.rs # Markdown generation
|
||||||
|
│ ├── writer.rs # File output with diff awareness
|
||||||
|
│ └── cache.rs # Analysis caching
|
||||||
|
└── test-project/ # Example Python project for testing
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -98,4 +196,4 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
This project is licensed under the MIT License — see the LICENSE file for details.
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
## Document metadata
|
## Document metadata
|
||||||
- **Created:** 2026-01-25
|
- **Created:** 2026-01-25
|
||||||
- **Updated:** 2026-01-25
|
- **Updated:** 2026-02-15
|
||||||
- **Generated by:** archdoc (cli) v0.1
|
- **Generated by:** wtismycode (cli) v0.1
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@ No tooling information available.
|
|||||||
|
|
||||||
| Path | Purpose | Link |
|
| Path | Purpose | Link |
|
||||||
|------|---------|------|
|
|------|---------|------|
|
||||||
| ./src/__init__.py | Source file | [details](docs/architecture/files/._src___init__.py.md) |
|
| ./src/__init__.py | Test project package. | [details](docs/architecture/files/src____init__.py.md) |
|
||||||
| ./src/utils.py | Source file | [details](docs/architecture/files/._src_utils.py.md) |
|
| ./src/utils.py | Utility functions for the test project. | [details](docs/architecture/files/src__utils.py.md) |
|
||||||
| ./src/core.py | Source file | [details](docs/architecture/files/._src_core.py.md) |
|
| ./src/core.py | Core module with database and HTTP integrations. | [details](docs/architecture/files/src__core.py.md) |
|
||||||
<!-- ARCHDOC:END section=layout -->
|
<!-- ARCHDOC:END section=layout -->
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -46,9 +46,9 @@ No tooling information available.
|
|||||||
|
|
||||||
| Module | Symbols | Inbound | Outbound | Link |
|
| Module | Symbols | Inbound | Outbound | Link |
|
||||||
|--------|---------|---------|----------|------|
|
|--------|---------|---------|----------|------|
|
||||||
| ./src/__init__.py | 0 | 0 | 0 | [details](docs/architecture/modules/._src___init__.py.md) |
|
| utils | 4 | 0 | 0 | [details](docs/architecture/modules/utils.md) |
|
||||||
| ./src/utils.py | 4 | 0 | 0 | [details](docs/architecture/modules/._src_utils.py.md) |
|
| src | 0 | 0 | 0 | [details](docs/architecture/modules/src.md) |
|
||||||
| ./src/core.py | 6 | 0 | 0 | [details](docs/architecture/modules/._src_core.py.md) |
|
| core | 6 | 0 | 0 | [details](docs/architecture/modules/core.md) |
|
||||||
<!-- ARCHDOC:END section=modules_index -->
|
<!-- ARCHDOC:END section=modules_index -->
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Test Project
|
# Test Project
|
||||||
|
|
||||||
A test project for ArchDoc development and testing.
|
A test project for WTIsMyCode development and testing.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# File: ../test-project/src/__init__.py
|
|
||||||
|
|
||||||
TODO: Add file documentation
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# File: ../test-project/src/core.py
|
|
||||||
|
|
||||||
TODO: Add file documentation
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# File: ../test-project/src/utils.py
|
|
||||||
|
|
||||||
TODO: Add file documentation
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# File: ./src/core.py
|
|
||||||
|
|
||||||
- **Module:** ./src/core.py
|
|
||||||
- **Defined symbols:** 6
|
|
||||||
- **Imports:** 2
|
|
||||||
|
|
||||||
<!-- MANUAL:BEGIN -->
|
|
||||||
## File intent (manual)
|
|
||||||
<FILL_MANUALLY>
|
|
||||||
<!-- MANUAL:END -->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Imports & file-level dependencies
|
|
||||||
<!-- ARCHDOC:BEGIN section=file_imports -->
|
|
||||||
> Generated. Do not edit inside this block.
|
|
||||||
- sqlite3
|
|
||||||
- requests
|
|
||||||
<!-- ARCHDOC:END section=file_imports -->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Symbols index
|
|
||||||
<!-- ARCHDOC:BEGIN section=symbols_index -->
|
|
||||||
> Generated. Do not edit inside this block.
|
|
||||||
- [DatabaseManager](._src_core.py#DatabaseManager)
|
|
||||||
- [__init__](._src_core.py#__init__)
|
|
||||||
- [connect](._src_core.py#connect)
|
|
||||||
- [execute_query](._src_core.py#execute_query)
|
|
||||||
- [fetch_external_data](._src_core.py#fetch_external_data)
|
|
||||||
- [process_user_data](._src_core.py#process_user_data)
|
|
||||||
<!-- ARCHDOC:END section=symbols_index -->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Symbol details
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# File: ./src/utils.py
|
|
||||||
|
|
||||||
- **Module:** ./src/utils.py
|
|
||||||
- **Defined symbols:** 4
|
|
||||||
- **Imports:** 2
|
|
||||||
|
|
||||||
<!-- MANUAL:BEGIN -->
|
|
||||||
## File intent (manual)
|
|
||||||
<FILL_MANUALLY>
|
|
||||||
<!-- MANUAL:END -->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Imports & file-level dependencies
|
|
||||||
<!-- ARCHDOC:BEGIN section=file_imports -->
|
|
||||||
> Generated. Do not edit inside this block.
|
|
||||||
- json
|
|
||||||
- os
|
|
||||||
<!-- ARCHDOC:END section=file_imports -->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Symbols index
|
|
||||||
<!-- ARCHDOC:BEGIN section=symbols_index -->
|
|
||||||
> Generated. Do not edit inside this block.
|
|
||||||
- [load_config](._src_utils.py#load_config)
|
|
||||||
- [save_config](._src_utils.py#save_config)
|
|
||||||
- [get_file_size](._src_utils.py#get_file_size)
|
|
||||||
- [format_bytes](._src_utils.py#format_bytes)
|
|
||||||
<!-- ARCHDOC:END section=symbols_index -->
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Symbol details
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# File: ./src/__init__.py
|
# File: ./src/__init__.py
|
||||||
|
|
||||||
- **Module:** ./src/__init__.py
|
- **Module:** src
|
||||||
- **Defined symbols:** 0
|
- **Defined symbols:** 0
|
||||||
- **Imports:** 0
|
- **Imports:** 0
|
||||||
|
|
||||||
276
test-project/docs/architecture/files/src__core.py.md
Normal file
276
test-project/docs/architecture/files/src__core.py.md
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
# File: ./src/core.py
|
||||||
|
|
||||||
|
- **Module:** core
|
||||||
|
- **Defined symbols:** 6
|
||||||
|
- **Imports:** 2
|
||||||
|
|
||||||
|
<!-- MANUAL:BEGIN -->
|
||||||
|
## File intent (manual)
|
||||||
|
<FILL_MANUALLY>
|
||||||
|
<!-- MANUAL:END -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Imports & file-level dependencies
|
||||||
|
<!-- ARCHDOC:BEGIN section=file_imports -->
|
||||||
|
> Generated. Do not edit inside this block.
|
||||||
|
- sqlite3
|
||||||
|
- requests
|
||||||
|
<!-- ARCHDOC:END section=file_imports -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Symbols index
|
||||||
|
<!-- ARCHDOC:BEGIN section=symbols_index -->
|
||||||
|
> Generated. Do not edit inside this block.
|
||||||
|
- `DatabaseManager` (Class)
|
||||||
|
- `DatabaseManager.__init__` (Method)
|
||||||
|
- `DatabaseManager.connect` (Method)
|
||||||
|
- `DatabaseManager.execute_query` (Method)
|
||||||
|
- `fetch_external_data` (Function)
|
||||||
|
- `process_user_data` (Function)
|
||||||
|
<!-- ARCHDOC:END section=symbols_index -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Symbol details
|
||||||
|
|
||||||
|
<!-- ARCHDOC:BEGIN symbol id=DatabaseManager --><a id="DatabaseManager"></a>
|
||||||
|
|
||||||
|
### `DatabaseManager`
|
||||||
|
- **Kind:** Class
|
||||||
|
- **Signature:** `class DatabaseManager`
|
||||||
|
- **Docstring:** `Manages database connections and operations.`
|
||||||
|
|
||||||
|
#### What it does
|
||||||
|
<!-- ARCHDOC:BEGIN section=purpose -->
|
||||||
|
extracted from AST
|
||||||
|
<!-- ARCHDOC:END section=purpose -->
|
||||||
|
|
||||||
|
#### Relations
|
||||||
|
<!-- ARCHDOC:BEGIN section=relations -->
|
||||||
|
**Outbound calls (best-effort):**
|
||||||
|
|
||||||
|
**Inbound (used by) (best-effort):**
|
||||||
|
<!-- ARCHDOC:END section=relations -->
|
||||||
|
|
||||||
|
#### Integrations (heuristic)
|
||||||
|
<!-- ARCHDOC:BEGIN section=integrations -->
|
||||||
|
- HTTP: no
|
||||||
|
- DB: yes
|
||||||
|
- Queue/Tasks: no
|
||||||
|
<!-- ARCHDOC:END section=integrations -->
|
||||||
|
|
||||||
|
#### Risk / impact
|
||||||
|
<!-- ARCHDOC:BEGIN section=impact -->
|
||||||
|
- fan-in: 2
|
||||||
|
- fan-out: 4
|
||||||
|
- cycle participant: no
|
||||||
|
- critical: no
|
||||||
|
<!-- ARCHDOC:END section=impact -->
|
||||||
|
|
||||||
|
<!-- MANUAL:BEGIN -->
|
||||||
|
#### Manual notes
|
||||||
|
<FILL_MANUALLY>
|
||||||
|
<!-- MANUAL:END -->
|
||||||
|
<!-- ARCHDOC:END symbol id=DatabaseManager -->
|
||||||
|
|
||||||
|
<!-- ARCHDOC:BEGIN symbol id=DatabaseManager.__init__ --><a id="DatabaseManager.__init__"></a>
|
||||||
|
|
||||||
|
### `DatabaseManager.__init__`
|
||||||
|
- **Kind:** Method
|
||||||
|
- **Signature:** `def __init__(self, db_path: str)`
|
||||||
|
- **Docstring:** `No documentation available`
|
||||||
|
|
||||||
|
#### What it does
|
||||||
|
<!-- ARCHDOC:BEGIN section=purpose -->
|
||||||
|
extracted from AST
|
||||||
|
<!-- ARCHDOC:END section=purpose -->
|
||||||
|
|
||||||
|
#### Relations
|
||||||
|
<!-- ARCHDOC:BEGIN section=relations -->
|
||||||
|
**Outbound calls (best-effort):**
|
||||||
|
|
||||||
|
**Inbound (used by) (best-effort):**
|
||||||
|
<!-- ARCHDOC:END section=relations -->
|
||||||
|
|
||||||
|
#### Integrations (heuristic)
|
||||||
|
<!-- ARCHDOC:BEGIN section=integrations -->
|
||||||
|
- HTTP: no
|
||||||
|
- DB: no
|
||||||
|
- Queue/Tasks: no
|
||||||
|
<!-- ARCHDOC:END section=integrations -->
|
||||||
|
|
||||||
|
#### Risk / impact
|
||||||
|
<!-- ARCHDOC:BEGIN section=impact -->
|
||||||
|
- fan-in: 0
|
||||||
|
- fan-out: 0
|
||||||
|
- cycle participant: no
|
||||||
|
- critical: no
|
||||||
|
<!-- ARCHDOC:END section=impact -->
|
||||||
|
|
||||||
|
<!-- MANUAL:BEGIN -->
|
||||||
|
#### Manual notes
|
||||||
|
<FILL_MANUALLY>
|
||||||
|
<!-- MANUAL:END -->
|
||||||
|
<!-- ARCHDOC:END symbol id=DatabaseManager.__init__ -->
|
||||||
|
|
||||||
|
<!-- ARCHDOC:BEGIN symbol id=DatabaseManager.connect --><a id="DatabaseManager.connect"></a>
|
||||||
|
|
||||||
|
### `DatabaseManager.connect`
|
||||||
|
- **Kind:** Method
|
||||||
|
- **Signature:** `def connect(self)`
|
||||||
|
- **Docstring:** `Connect to the database.`
|
||||||
|
|
||||||
|
#### What it does
|
||||||
|
<!-- ARCHDOC:BEGIN section=purpose -->
|
||||||
|
extracted from AST
|
||||||
|
<!-- ARCHDOC:END section=purpose -->
|
||||||
|
|
||||||
|
#### Relations
|
||||||
|
<!-- ARCHDOC:BEGIN section=relations -->
|
||||||
|
**Outbound calls (best-effort):**
|
||||||
|
|
||||||
|
**Inbound (used by) (best-effort):**
|
||||||
|
<!-- ARCHDOC:END section=relations -->
|
||||||
|
|
||||||
|
#### Integrations (heuristic)
|
||||||
|
<!-- ARCHDOC:BEGIN section=integrations -->
|
||||||
|
- HTTP: no
|
||||||
|
- DB: yes
|
||||||
|
- Queue/Tasks: no
|
||||||
|
<!-- ARCHDOC:END section=integrations -->
|
||||||
|
|
||||||
|
#### Risk / impact
|
||||||
|
<!-- ARCHDOC:BEGIN section=impact -->
|
||||||
|
- fan-in: 0
|
||||||
|
- fan-out: 1
|
||||||
|
- cycle participant: no
|
||||||
|
- critical: no
|
||||||
|
<!-- ARCHDOC:END section=impact -->
|
||||||
|
|
||||||
|
<!-- MANUAL:BEGIN -->
|
||||||
|
#### Manual notes
|
||||||
|
<FILL_MANUALLY>
|
||||||
|
<!-- MANUAL:END -->
|
||||||
|
<!-- ARCHDOC:END symbol id=DatabaseManager.connect -->
|
||||||
|
|
||||||
|
<!-- ARCHDOC:BEGIN symbol id=DatabaseManager.execute_query --><a id="DatabaseManager.execute_query"></a>
|
||||||
|
|
||||||
|
### `DatabaseManager.execute_query`
|
||||||
|
- **Kind:** Method
|
||||||
|
- **Signature:** `def execute_query(self, query: str)`
|
||||||
|
- **Docstring:** `Execute a database query.`
|
||||||
|
|
||||||
|
#### What it does
|
||||||
|
<!-- ARCHDOC:BEGIN section=purpose -->
|
||||||
|
extracted from AST
|
||||||
|
<!-- ARCHDOC:END section=purpose -->
|
||||||
|
|
||||||
|
#### Relations
|
||||||
|
<!-- ARCHDOC:BEGIN section=relations -->
|
||||||
|
**Outbound calls (best-effort):**
|
||||||
|
|
||||||
|
**Inbound (used by) (best-effort):**
|
||||||
|
<!-- ARCHDOC:END section=relations -->
|
||||||
|
|
||||||
|
#### Integrations (heuristic)
|
||||||
|
<!-- ARCHDOC:BEGIN section=integrations -->
|
||||||
|
- HTTP: no
|
||||||
|
- DB: no
|
||||||
|
- Queue/Tasks: no
|
||||||
|
<!-- ARCHDOC:END section=integrations -->
|
||||||
|
|
||||||
|
#### Risk / impact
|
||||||
|
<!-- ARCHDOC:BEGIN section=impact -->
|
||||||
|
- fan-in: 0
|
||||||
|
- fan-out: 3
|
||||||
|
- cycle participant: no
|
||||||
|
- critical: no
|
||||||
|
<!-- ARCHDOC:END section=impact -->
|
||||||
|
|
||||||
|
<!-- MANUAL:BEGIN -->
|
||||||
|
#### Manual notes
|
||||||
|
<FILL_MANUALLY>
|
||||||
|
<!-- MANUAL:END -->
|
||||||
|
<!-- ARCHDOC:END symbol id=DatabaseManager.execute_query -->
|
||||||
|
|
||||||
|
<!-- ARCHDOC:BEGIN symbol id=fetch_external_data --><a id="fetch_external_data"></a>
|
||||||
|
|
||||||
|
### `fetch_external_data`
|
||||||
|
- **Kind:** Function
|
||||||
|
- **Signature:** `def fetch_external_data(url: str)`
|
||||||
|
- **Docstring:** `Fetch data from an external API.`
|
||||||
|
|
||||||
|
#### What it does
|
||||||
|
<!-- ARCHDOC:BEGIN section=purpose -->
|
||||||
|
extracted from AST
|
||||||
|
<!-- ARCHDOC:END section=purpose -->
|
||||||
|
|
||||||
|
#### Relations
|
||||||
|
<!-- ARCHDOC:BEGIN section=relations -->
|
||||||
|
**Outbound calls (best-effort):**
|
||||||
|
|
||||||
|
**Inbound (used by) (best-effort):**
|
||||||
|
<!-- ARCHDOC:END section=relations -->
|
||||||
|
|
||||||
|
#### Integrations (heuristic)
|
||||||
|
<!-- ARCHDOC:BEGIN section=integrations -->
|
||||||
|
- HTTP: yes
|
||||||
|
- DB: no
|
||||||
|
- Queue/Tasks: no
|
||||||
|
<!-- ARCHDOC:END section=integrations -->
|
||||||
|
|
||||||
|
#### Risk / impact
|
||||||
|
<!-- ARCHDOC:BEGIN section=impact -->
|
||||||
|
- fan-in: 2
|
||||||
|
- fan-out: 2
|
||||||
|
- cycle participant: no
|
||||||
|
- critical: no
|
||||||
|
<!-- ARCHDOC:END section=impact -->
|
||||||
|
|
||||||
|
<!-- MANUAL:BEGIN -->
|
||||||
|
#### Manual notes
|
||||||
|
<FILL_MANUALLY>
|
||||||
|
<!-- MANUAL:END -->
|
||||||
|
<!-- ARCHDOC:END symbol id=fetch_external_data -->
|
||||||
|
|
||||||
|
<!-- ARCHDOC:BEGIN symbol id=process_user_data --><a id="process_user_data"></a>
|
||||||
|
|
||||||
|
### `process_user_data`
|
||||||
|
- **Kind:** Function
|
||||||
|
- **Signature:** `def process_user_data(user_id: int)`
|
||||||
|
- **Docstring:** `Process user data with database and external API calls.`
|
||||||
|
|
||||||
|
#### What it does
|
||||||
|
<!-- ARCHDOC:BEGIN section=purpose -->
|
||||||
|
extracted from AST
|
||||||
|
<!-- ARCHDOC:END section=purpose -->
|
||||||
|
|
||||||
|
#### Relations
|
||||||
|
<!-- ARCHDOC:BEGIN section=relations -->
|
||||||
|
**Outbound calls (best-effort):**
|
||||||
|
|
||||||
|
**Inbound (used by) (best-effort):**
|
||||||
|
<!-- ARCHDOC:END section=relations -->
|
||||||
|
|
||||||
|
#### Integrations (heuristic)
|
||||||
|
<!-- ARCHDOC:BEGIN section=integrations -->
|
||||||
|
- HTTP: no
|
||||||
|
- DB: no
|
||||||
|
- Queue/Tasks: no
|
||||||
|
<!-- ARCHDOC:END section=integrations -->
|
||||||
|
|
||||||
|
#### Risk / impact
|
||||||
|
<!-- ARCHDOC:BEGIN section=impact -->
|
||||||
|
- fan-in: 0
|
||||||
|
- fan-out: 4
|
||||||
|
- cycle participant: no
|
||||||
|
- critical: no
|
||||||
|
<!-- ARCHDOC:END section=impact -->
|
||||||
|
|
||||||
|
<!-- MANUAL:BEGIN -->
|
||||||
|
#### Manual notes
|
||||||
|
<FILL_MANUALLY>
|
||||||
|
<!-- MANUAL:END -->
|
||||||
|
<!-- ARCHDOC:END symbol id=process_user_data -->
|
||||||
194
test-project/docs/architecture/files/src__utils.py.md
Normal file
194
test-project/docs/architecture/files/src__utils.py.md
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
# File: ./src/utils.py
|
||||||
|
|
||||||
|
- **Module:** utils
|
||||||
|
- **Defined symbols:** 4
|
||||||
|
- **Imports:** 2
|
||||||
|
|
||||||
|
<!-- MANUAL:BEGIN -->
|
||||||
|
## File intent (manual)
|
||||||
|
<FILL_MANUALLY>
|
||||||
|
<!-- MANUAL:END -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Imports & file-level dependencies
|
||||||
|
<!-- ARCHDOC:BEGIN section=file_imports -->
|
||||||
|
> Generated. Do not edit inside this block.
|
||||||
|
- json
|
||||||
|
- os
|
||||||
|
<!-- ARCHDOC:END section=file_imports -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Symbols index
|
||||||
|
<!-- ARCHDOC:BEGIN section=symbols_index -->
|
||||||
|
> Generated. Do not edit inside this block.
|
||||||
|
- `load_config` (Function)
|
||||||
|
- `save_config` (Function)
|
||||||
|
- `get_file_size` (Function)
|
||||||
|
- `format_bytes` (Function)
|
||||||
|
<!-- ARCHDOC:END section=symbols_index -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Symbol details
|
||||||
|
|
||||||
|
<!-- ARCHDOC:BEGIN symbol id=load_config --><a id="load_config"></a>
|
||||||
|
|
||||||
|
### `load_config`
|
||||||
|
- **Kind:** Function
|
||||||
|
- **Signature:** `def load_config(config_path: str)`
|
||||||
|
- **Docstring:** `Load configuration from a JSON file.`
|
||||||
|
|
||||||
|
#### What it does
|
||||||
|
<!-- ARCHDOC:BEGIN section=purpose -->
|
||||||
|
extracted from AST
|
||||||
|
<!-- ARCHDOC:END section=purpose -->
|
||||||
|
|
||||||
|
#### Relations
|
||||||
|
<!-- ARCHDOC:BEGIN section=relations -->
|
||||||
|
**Outbound calls (best-effort):**
|
||||||
|
|
||||||
|
**Inbound (used by) (best-effort):**
|
||||||
|
<!-- ARCHDOC:END section=relations -->
|
||||||
|
|
||||||
|
#### Integrations (heuristic)
|
||||||
|
<!-- ARCHDOC:BEGIN section=integrations -->
|
||||||
|
- HTTP: no
|
||||||
|
- DB: no
|
||||||
|
- Queue/Tasks: no
|
||||||
|
<!-- ARCHDOC:END section=integrations -->
|
||||||
|
|
||||||
|
#### Risk / impact
|
||||||
|
<!-- ARCHDOC:BEGIN section=impact -->
|
||||||
|
- fan-in: 0
|
||||||
|
- fan-out: 2
|
||||||
|
- cycle participant: no
|
||||||
|
- critical: no
|
||||||
|
<!-- ARCHDOC:END section=impact -->
|
||||||
|
|
||||||
|
<!-- MANUAL:BEGIN -->
|
||||||
|
#### Manual notes
|
||||||
|
<FILL_MANUALLY>
|
||||||
|
<!-- MANUAL:END -->
|
||||||
|
<!-- ARCHDOC:END symbol id=load_config -->
|
||||||
|
|
||||||
|
<!-- ARCHDOC:BEGIN symbol id=save_config --><a id="save_config"></a>
|
||||||
|
|
||||||
|
### `save_config`
|
||||||
|
- **Kind:** Function
|
||||||
|
- **Signature:** `def save_config(config: dict, config_path: str)`
|
||||||
|
- **Docstring:** `Save configuration to a JSON file.`
|
||||||
|
|
||||||
|
#### What it does
|
||||||
|
<!-- ARCHDOC:BEGIN section=purpose -->
|
||||||
|
extracted from AST
|
||||||
|
<!-- ARCHDOC:END section=purpose -->
|
||||||
|
|
||||||
|
#### Relations
|
||||||
|
<!-- ARCHDOC:BEGIN section=relations -->
|
||||||
|
**Outbound calls (best-effort):**
|
||||||
|
|
||||||
|
**Inbound (used by) (best-effort):**
|
||||||
|
<!-- ARCHDOC:END section=relations -->
|
||||||
|
|
||||||
|
#### Integrations (heuristic)
|
||||||
|
<!-- ARCHDOC:BEGIN section=integrations -->
|
||||||
|
- HTTP: no
|
||||||
|
- DB: no
|
||||||
|
- Queue/Tasks: no
|
||||||
|
<!-- ARCHDOC:END section=integrations -->
|
||||||
|
|
||||||
|
#### Risk / impact
|
||||||
|
<!-- ARCHDOC:BEGIN section=impact -->
|
||||||
|
- fan-in: 0
|
||||||
|
- fan-out: 2
|
||||||
|
- cycle participant: no
|
||||||
|
- critical: no
|
||||||
|
<!-- ARCHDOC:END section=impact -->
|
||||||
|
|
||||||
|
<!-- MANUAL:BEGIN -->
|
||||||
|
#### Manual notes
|
||||||
|
<FILL_MANUALLY>
|
||||||
|
<!-- MANUAL:END -->
|
||||||
|
<!-- ARCHDOC:END symbol id=save_config -->
|
||||||
|
|
||||||
|
<!-- ARCHDOC:BEGIN symbol id=get_file_size --><a id="get_file_size"></a>
|
||||||
|
|
||||||
|
### `get_file_size`
|
||||||
|
- **Kind:** Function
|
||||||
|
- **Signature:** `def get_file_size(filepath: str)`
|
||||||
|
- **Docstring:** `Get the size of a file in bytes.`
|
||||||
|
|
||||||
|
#### What it does
|
||||||
|
<!-- ARCHDOC:BEGIN section=purpose -->
|
||||||
|
extracted from AST
|
||||||
|
<!-- ARCHDOC:END section=purpose -->
|
||||||
|
|
||||||
|
#### Relations
|
||||||
|
<!-- ARCHDOC:BEGIN section=relations -->
|
||||||
|
**Outbound calls (best-effort):**
|
||||||
|
|
||||||
|
**Inbound (used by) (best-effort):**
|
||||||
|
<!-- ARCHDOC:END section=relations -->
|
||||||
|
|
||||||
|
#### Integrations (heuristic)
|
||||||
|
<!-- ARCHDOC:BEGIN section=integrations -->
|
||||||
|
- HTTP: no
|
||||||
|
- DB: no
|
||||||
|
- Queue/Tasks: no
|
||||||
|
<!-- ARCHDOC:END section=integrations -->
|
||||||
|
|
||||||
|
#### Risk / impact
|
||||||
|
<!-- ARCHDOC:BEGIN section=impact -->
|
||||||
|
- fan-in: 0
|
||||||
|
- fan-out: 1
|
||||||
|
- cycle participant: no
|
||||||
|
- critical: no
|
||||||
|
<!-- ARCHDOC:END section=impact -->
|
||||||
|
|
||||||
|
<!-- MANUAL:BEGIN -->
|
||||||
|
#### Manual notes
|
||||||
|
<FILL_MANUALLY>
|
||||||
|
<!-- MANUAL:END -->
|
||||||
|
<!-- ARCHDOC:END symbol id=get_file_size -->
|
||||||
|
|
||||||
|
<!-- ARCHDOC:BEGIN symbol id=format_bytes --><a id="format_bytes"></a>
|
||||||
|
|
||||||
|
### `format_bytes`
|
||||||
|
- **Kind:** Function
|
||||||
|
- **Signature:** `def format_bytes(size: int)`
|
||||||
|
- **Docstring:** `Format bytes into a human-readable string.`
|
||||||
|
|
||||||
|
#### What it does
|
||||||
|
<!-- ARCHDOC:BEGIN section=purpose -->
|
||||||
|
extracted from AST
|
||||||
|
<!-- ARCHDOC:END section=purpose -->
|
||||||
|
|
||||||
|
#### Relations
|
||||||
|
<!-- ARCHDOC:BEGIN section=relations -->
|
||||||
|
**Outbound calls (best-effort):**
|
||||||
|
|
||||||
|
**Inbound (used by) (best-effort):**
|
||||||
|
<!-- ARCHDOC:END section=relations -->
|
||||||
|
|
||||||
|
#### Integrations (heuristic)
|
||||||
|
<!-- ARCHDOC:BEGIN section=integrations -->
|
||||||
|
- HTTP: no
|
||||||
|
- DB: no
|
||||||
|
- Queue/Tasks: no
|
||||||
|
<!-- ARCHDOC:END section=integrations -->
|
||||||
|
|
||||||
|
#### Risk / impact
|
||||||
|
<!-- ARCHDOC:BEGIN section=impact -->
|
||||||
|
- fan-in: 0
|
||||||
|
- fan-out: 0
|
||||||
|
- cycle participant: no
|
||||||
|
- critical: no
|
||||||
|
<!-- ARCHDOC:END section=impact -->
|
||||||
|
|
||||||
|
<!-- MANUAL:BEGIN -->
|
||||||
|
#### Manual notes
|
||||||
|
<FILL_MANUALLY>
|
||||||
|
<!-- MANUAL:END -->
|
||||||
|
<!-- ARCHDOC:END symbol id=format_bytes -->
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Repository layout
|
||||||
|
|
||||||
|
<!-- MANUAL:BEGIN -->
|
||||||
|
## Manual overrides
|
||||||
|
- `src/app/` — <FILL_MANUALLY>
|
||||||
|
<!-- MANUAL:END -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detected structure
|
||||||
|
<!-- ARCHDOC:BEGIN section=layout_detected -->
|
||||||
|
> Generated. Do not edit inside this block.
|
||||||
|
| Path | Purpose | Link |
|
||||||
|
|------|---------|------|
|
||||||
|
| ./src/__init__.py | Test project package. | [details](files/src____init__.py.md) |
|
||||||
|
| ./src/utils.py | Utility functions for the test project. | [details](files/src__utils.py.md) |
|
||||||
|
| ./src/core.py | Core module with database and HTTP integrations. | [details](files/src__core.py.md) |
|
||||||
|
<!-- ARCHDOC:END section=layout_detected -->
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
# Module: ../test-project/src/__init__.py
|
|
||||||
|
|
||||||
No summary available
|
|
||||||
|
|
||||||
## Symbols
|
|
||||||
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
### Imports
|
|
||||||
|
|
||||||
### Outbound Modules
|
|
||||||
|
|
||||||
### Inbound Modules
|
|
||||||
|
|
||||||
## Integrations
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
```python
|
|
||||||
// Example usage of module functions
|
|
||||||
// TODO: Add real usage examples based on module analysis
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
# Module: ../test-project/src/core.py
|
|
||||||
|
|
||||||
No summary available
|
|
||||||
|
|
||||||
## Symbols
|
|
||||||
|
|
||||||
### DatabaseManager
|
|
||||||
|
|
||||||
class DatabaseManager
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Class
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### __init__
|
|
||||||
|
|
||||||
def __init__(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### connect
|
|
||||||
|
|
||||||
def connect(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### execute_query
|
|
||||||
|
|
||||||
def execute_query(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### fetch_external_data
|
|
||||||
|
|
||||||
def fetch_external_data(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### process_user_data
|
|
||||||
|
|
||||||
def process_user_data(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 1
|
|
||||||
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
### Imports
|
|
||||||
- sqlite3
|
|
||||||
- requests
|
|
||||||
|
|
||||||
### Outbound Modules
|
|
||||||
|
|
||||||
### Inbound Modules
|
|
||||||
|
|
||||||
## Integrations
|
|
||||||
|
|
||||||
### Database Integrations
|
|
||||||
- DatabaseManager
|
|
||||||
- connect
|
|
||||||
|
|
||||||
### HTTP/API Integrations
|
|
||||||
- fetch_external_data
|
|
||||||
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
```python
|
|
||||||
// Example usage of module functions
|
|
||||||
// TODO: Add real usage examples based on module analysis
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# Module: ../test-project/src/utils.py
|
|
||||||
|
|
||||||
No summary available
|
|
||||||
|
|
||||||
## Symbols
|
|
||||||
|
|
||||||
### load_config
|
|
||||||
|
|
||||||
def load_config(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### save_config
|
|
||||||
|
|
||||||
def save_config(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### get_file_size
|
|
||||||
|
|
||||||
def get_file_size(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### format_bytes
|
|
||||||
|
|
||||||
def format_bytes(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
### Imports
|
|
||||||
- json
|
|
||||||
- os
|
|
||||||
|
|
||||||
### Outbound Modules
|
|
||||||
|
|
||||||
### Inbound Modules
|
|
||||||
|
|
||||||
## Integrations
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
```python
|
|
||||||
// Example usage of module functions
|
|
||||||
// TODO: Add real usage examples based on module analysis
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# Module: ./src/__init__.py
|
|
||||||
|
|
||||||
No summary available
|
|
||||||
|
|
||||||
## Symbols
|
|
||||||
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
### Imports
|
|
||||||
|
|
||||||
### Outbound Modules
|
|
||||||
|
|
||||||
### Inbound Modules
|
|
||||||
|
|
||||||
## Integrations
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
```python
|
|
||||||
// Example usage of module functions
|
|
||||||
// TODO: Add real usage examples based on module analysis
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
# Module: ./src/core.py
|
|
||||||
|
|
||||||
No summary available
|
|
||||||
|
|
||||||
## Symbols
|
|
||||||
|
|
||||||
### DatabaseManager
|
|
||||||
|
|
||||||
class DatabaseManager
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Class
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### __init__
|
|
||||||
|
|
||||||
def __init__(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### connect
|
|
||||||
|
|
||||||
def connect(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### execute_query
|
|
||||||
|
|
||||||
def execute_query(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### fetch_external_data
|
|
||||||
|
|
||||||
def fetch_external_data(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### process_user_data
|
|
||||||
|
|
||||||
def process_user_data(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 1
|
|
||||||
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
### Imports
|
|
||||||
- sqlite3
|
|
||||||
- requests
|
|
||||||
|
|
||||||
### Outbound Modules
|
|
||||||
|
|
||||||
### Inbound Modules
|
|
||||||
|
|
||||||
## Integrations
|
|
||||||
|
|
||||||
### Database Integrations
|
|
||||||
- DatabaseManager
|
|
||||||
- connect
|
|
||||||
|
|
||||||
### HTTP/API Integrations
|
|
||||||
- fetch_external_data
|
|
||||||
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
```python
|
|
||||||
// Example usage of module functions
|
|
||||||
// TODO: Add real usage examples based on module analysis
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# Module: ./src/utils.py
|
|
||||||
|
|
||||||
No summary available
|
|
||||||
|
|
||||||
## Symbols
|
|
||||||
|
|
||||||
### load_config
|
|
||||||
|
|
||||||
def load_config(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### save_config
|
|
||||||
|
|
||||||
def save_config(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### get_file_size
|
|
||||||
|
|
||||||
def get_file_size(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
### format_bytes
|
|
||||||
|
|
||||||
def format_bytes(...)
|
|
||||||
|
|
||||||
No documentation available
|
|
||||||
|
|
||||||
**Type:** Function
|
|
||||||
|
|
||||||
**Metrics:**
|
|
||||||
- Fan-in: 0
|
|
||||||
- Fan-out: 0
|
|
||||||
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
### Imports
|
|
||||||
- json
|
|
||||||
- os
|
|
||||||
|
|
||||||
### Outbound Modules
|
|
||||||
|
|
||||||
### Inbound Modules
|
|
||||||
|
|
||||||
## Integrations
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
```python
|
|
||||||
// Example usage of module functions
|
|
||||||
// TODO: Add real usage examples based on module analysis
|
|
||||||
```
|
|
||||||
|
|
||||||
116
test-project/docs/architecture/modules/core.md
Normal file
116
test-project/docs/architecture/modules/core.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# Module: core
|
||||||
|
|
||||||
|
Core module with database and HTTP integrations.
|
||||||
|
|
||||||
|
## Symbols
|
||||||
|
|
||||||
|
### DatabaseManager
|
||||||
|
|
||||||
|
class DatabaseManager
|
||||||
|
|
||||||
|
Manages database connections and operations.
|
||||||
|
|
||||||
|
**Type:** Class
|
||||||
|
|
||||||
|
**Metrics:**
|
||||||
|
- Fan-in: 2
|
||||||
|
- Fan-out: 4
|
||||||
|
|
||||||
|
### DatabaseManager.__init__
|
||||||
|
|
||||||
|
def __init__(self, db_path: str)
|
||||||
|
|
||||||
|
No documentation available
|
||||||
|
|
||||||
|
**Type:** Method
|
||||||
|
|
||||||
|
**Metrics:**
|
||||||
|
- Fan-in: 0
|
||||||
|
- Fan-out: 0
|
||||||
|
|
||||||
|
### DatabaseManager.connect
|
||||||
|
|
||||||
|
def connect(self)
|
||||||
|
|
||||||
|
Connect to the database.
|
||||||
|
|
||||||
|
**Type:** Method
|
||||||
|
|
||||||
|
**Metrics:**
|
||||||
|
- Fan-in: 0
|
||||||
|
- Fan-out: 1
|
||||||
|
|
||||||
|
### DatabaseManager.execute_query
|
||||||
|
|
||||||
|
def execute_query(self, query: str)
|
||||||
|
|
||||||
|
Execute a database query.
|
||||||
|
|
||||||
|
**Type:** Method
|
||||||
|
|
||||||
|
**Metrics:**
|
||||||
|
- Fan-in: 0
|
||||||
|
- Fan-out: 3
|
||||||
|
|
||||||
|
### fetch_external_data
|
||||||
|
|
||||||
|
def fetch_external_data(url: str)
|
||||||
|
|
||||||
|
Fetch data from an external API.
|
||||||
|
|
||||||
|
**Type:** Function
|
||||||
|
|
||||||
|
**Metrics:**
|
||||||
|
- Fan-in: 2
|
||||||
|
- Fan-out: 2
|
||||||
|
|
||||||
|
### process_user_data
|
||||||
|
|
||||||
|
def process_user_data(user_id: int)
|
||||||
|
|
||||||
|
Process user data with database and external API calls.
|
||||||
|
|
||||||
|
**Type:** Function
|
||||||
|
|
||||||
|
**Metrics:**
|
||||||
|
- Fan-in: 0
|
||||||
|
- Fan-out: 4
|
||||||
|
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
- sqlite3
|
||||||
|
- requests
|
||||||
|
|
||||||
|
### Outbound Modules
|
||||||
|
|
||||||
|
### Inbound Modules
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
### Database Integrations
|
||||||
|
- DatabaseManager
|
||||||
|
- DatabaseManager.connect
|
||||||
|
|
||||||
|
### HTTP/API Integrations
|
||||||
|
- fetch_external_data
|
||||||
|
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core import DatabaseManager
|
||||||
|
instance = DatabaseManager()
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core import fetch_external_data
|
||||||
|
result = fetch_external_data(url)
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core import process_user_data
|
||||||
|
result = process_user_data(user_id)
|
||||||
|
```
|
||||||
|
|
||||||
26
test-project/docs/architecture/modules/src.md
Normal file
26
test-project/docs/architecture/modules/src.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Module: src
|
||||||
|
|
||||||
|
Test project package.
|
||||||
|
|
||||||
|
## Symbols
|
||||||
|
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
|
||||||
|
### Outbound Modules
|
||||||
|
|
||||||
|
### Inbound Modules
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
```python
|
||||||
|
import src
|
||||||
|
```
|
||||||
|
|
||||||
92
test-project/docs/architecture/modules/utils.md
Normal file
92
test-project/docs/architecture/modules/utils.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Module: utils
|
||||||
|
|
||||||
|
Utility functions for the test project.
|
||||||
|
|
||||||
|
## Symbols
|
||||||
|
|
||||||
|
### load_config
|
||||||
|
|
||||||
|
def load_config(config_path: str)
|
||||||
|
|
||||||
|
Load configuration from a JSON file.
|
||||||
|
|
||||||
|
**Type:** Function
|
||||||
|
|
||||||
|
**Metrics:**
|
||||||
|
- Fan-in: 0
|
||||||
|
- Fan-out: 2
|
||||||
|
|
||||||
|
### save_config
|
||||||
|
|
||||||
|
def save_config(config: dict, config_path: str)
|
||||||
|
|
||||||
|
Save configuration to a JSON file.
|
||||||
|
|
||||||
|
**Type:** Function
|
||||||
|
|
||||||
|
**Metrics:**
|
||||||
|
- Fan-in: 0
|
||||||
|
- Fan-out: 2
|
||||||
|
|
||||||
|
### get_file_size
|
||||||
|
|
||||||
|
def get_file_size(filepath: str)
|
||||||
|
|
||||||
|
Get the size of a file in bytes.
|
||||||
|
|
||||||
|
**Type:** Function
|
||||||
|
|
||||||
|
**Metrics:**
|
||||||
|
- Fan-in: 0
|
||||||
|
- Fan-out: 1
|
||||||
|
|
||||||
|
### format_bytes
|
||||||
|
|
||||||
|
def format_bytes(size: int)
|
||||||
|
|
||||||
|
Format bytes into a human-readable string.
|
||||||
|
|
||||||
|
**Type:** Function
|
||||||
|
|
||||||
|
**Metrics:**
|
||||||
|
- Fan-in: 0
|
||||||
|
- Fan-out: 0
|
||||||
|
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
- json
|
||||||
|
- os
|
||||||
|
|
||||||
|
### Outbound Modules
|
||||||
|
|
||||||
|
### Inbound Modules
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
```python
|
||||||
|
from utils import load_config
|
||||||
|
result = load_config(config_path)
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
from utils import save_config
|
||||||
|
result = save_config(config, config_path)
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
from utils import get_file_size
|
||||||
|
result = get_file_size(filepath)
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
from utils import format_bytes
|
||||||
|
result = format_bytes(size)
|
||||||
|
```
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
[project]
|
[project]
|
||||||
name = "test-project"
|
name = "test-project"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "A test project for ArchDoc"
|
description = "A test project for WTIsMyCode"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Test Author", email = "test@example.com"}
|
{name = "Test Author", email = "test@example.com"}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -53,10 +53,10 @@ description_max_length = 200
|
|||||||
|
|
||||||
[logging]
|
[logging]
|
||||||
level = "info"
|
level = "info"
|
||||||
file = "archdoc.log"
|
file = "wtismycode.log"
|
||||||
format = "compact"
|
format = "compact"
|
||||||
|
|
||||||
[caching]
|
[caching]
|
||||||
enabled = true
|
enabled = true
|
||||||
cache_dir = ".archdoc/cache"
|
cache_dir = ".wtismycode/cache"
|
||||||
max_cache_age = "24h"
|
max_cache_age = "24h"
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archdoc-cli"
|
name = "wtismycode-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "wtismycode"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
archdoc-core = { path = "../archdoc-core" }
|
wtismycode-core = { path = "../wtismycode-core" }
|
||||||
clap = { version = "4.0", features = ["derive"] }
|
clap = { version = "4.0", features = ["derive"] }
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use archdoc_core::Config;
|
use wtismycode_core::Config;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
|
|
||||||
use super::generate::analyze_project;
|
use super::generate::analyze_project;
|
||||||
@@ -9,8 +9,8 @@ pub fn check_docs_consistency(root: &str, config: &Config) -> Result<()> {
|
|||||||
|
|
||||||
let model = analyze_project(root, config)?;
|
let model = analyze_project(root, config)?;
|
||||||
|
|
||||||
let renderer = archdoc_core::renderer::Renderer::new();
|
let renderer = wtismycode_core::renderer::Renderer::new();
|
||||||
let _generated = renderer.render_architecture_md(&model)?;
|
let _generated = renderer.render_architecture_md(&model, None)?;
|
||||||
|
|
||||||
let architecture_md_path = std::path::Path::new(root).join(&config.project.entry_file);
|
let architecture_md_path = std::path::Path::new(root).join(&config.project.entry_file);
|
||||||
if !architecture_md_path.exists() {
|
if !architecture_md_path.exists() {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use archdoc_core::{Config, ProjectModel, scanner::FileScanner, python_analyzer::PythonAnalyzer};
|
use wtismycode_core::{Config, ProjectModel, scanner::FileScanner, python_analyzer::PythonAnalyzer};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -55,7 +55,46 @@ pub fn analyze_project(root: &str, config: &Config) -> Result<ProjectModel> {
|
|||||||
Ok(model)
|
Ok(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_docs(model: &ProjectModel, out: &str, verbose: bool) -> Result<()> {
|
pub fn dry_run_docs(model: &ProjectModel, out: &str, config: &Config) -> Result<()> {
|
||||||
|
println!("{}", "Dry run — no files will be written.".cyan().bold());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let out_path = std::path::Path::new(out);
|
||||||
|
let arch_path = std::path::Path::new(".").join("ARCHITECTURE.md");
|
||||||
|
|
||||||
|
// ARCHITECTURE.md
|
||||||
|
let exists = arch_path.exists();
|
||||||
|
println!(" {} {}", if exists { "UPDATE" } else { "CREATE" }, arch_path.display());
|
||||||
|
|
||||||
|
// layout.md
|
||||||
|
let layout_path = out_path.join("layout.md");
|
||||||
|
let exists = layout_path.exists();
|
||||||
|
println!(" {} {}", if exists { "UPDATE" } else { "CREATE" }, layout_path.display());
|
||||||
|
|
||||||
|
// Module docs
|
||||||
|
for module_id in model.modules.keys() {
|
||||||
|
let p = out_path.join("modules").join(format!("{}.md", sanitize_filename(module_id)));
|
||||||
|
let exists = p.exists();
|
||||||
|
println!(" {} {}", if exists { "UPDATE" } else { "CREATE" }, p.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
// File docs
|
||||||
|
for file_doc in model.files.values() {
|
||||||
|
let p = out_path.join("files").join(format!("{}.md", sanitize_filename(&file_doc.path)));
|
||||||
|
let exists = p.exists();
|
||||||
|
println!(" {} {}", if exists { "UPDATE" } else { "CREATE" }, p.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = config; // used for future extensions
|
||||||
|
println!();
|
||||||
|
println!("{} {} file(s) would be generated/updated",
|
||||||
|
"✓".green().bold(),
|
||||||
|
2 + model.modules.len() + model.files.len());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_docs(model: &ProjectModel, out: &str, verbose: bool, _config: &Config) -> Result<()> {
|
||||||
println!("{}", "Generating documentation...".cyan());
|
println!("{}", "Generating documentation...".cyan());
|
||||||
|
|
||||||
let out_path = std::path::Path::new(out);
|
let out_path = std::path::Path::new(out);
|
||||||
@@ -66,14 +105,30 @@ pub fn generate_docs(model: &ProjectModel, out: &str, verbose: bool) -> Result<(
|
|||||||
std::fs::create_dir_all(&modules_path)?;
|
std::fs::create_dir_all(&modules_path)?;
|
||||||
std::fs::create_dir_all(&files_path)?;
|
std::fs::create_dir_all(&files_path)?;
|
||||||
|
|
||||||
let renderer = archdoc_core::renderer::Renderer::new();
|
// Clean up stale files from previous runs
|
||||||
let writer = archdoc_core::writer::DiffAwareWriter::new();
|
for subdir in &["modules", "files"] {
|
||||||
|
let dir = out_path.join(subdir);
|
||||||
|
if dir.exists()
|
||||||
|
&& let Ok(entries) = std::fs::read_dir(&dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
if entry.path().extension().map(|e| e == "md").unwrap_or(false) {
|
||||||
|
let _ = std::fs::remove_file(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderer = wtismycode_core::renderer::Renderer::new();
|
||||||
|
let writer = wtismycode_core::writer::DiffAwareWriter::new();
|
||||||
|
|
||||||
let output_path = std::path::Path::new(".").join("ARCHITECTURE.md");
|
let output_path = std::path::Path::new(".").join("ARCHITECTURE.md");
|
||||||
|
|
||||||
// Generate module docs
|
// Generate module docs
|
||||||
for module_id in model.modules.keys() {
|
for module_id in model.modules.keys() {
|
||||||
let module_doc_path = modules_path.join(format!("{}.md", sanitize_filename(module_id)));
|
let module_doc_path = modules_path.join(format!("{}.md", sanitize_filename(module_id)));
|
||||||
|
if verbose {
|
||||||
|
println!(" Generating module doc: {}", module_id);
|
||||||
|
}
|
||||||
match renderer.render_module_md(model, module_id) {
|
match renderer.render_module_md(model, module_id) {
|
||||||
Ok(module_content) => {
|
Ok(module_content) => {
|
||||||
std::fs::write(&module_doc_path, module_content)?;
|
std::fs::write(&module_doc_path, module_content)?;
|
||||||
@@ -88,6 +143,9 @@ pub fn generate_docs(model: &ProjectModel, out: &str, verbose: bool) -> Result<(
|
|||||||
|
|
||||||
// Generate file docs
|
// Generate file docs
|
||||||
for file_doc in model.files.values() {
|
for file_doc in model.files.values() {
|
||||||
|
if verbose {
|
||||||
|
println!(" Generating file doc: {}", file_doc.path);
|
||||||
|
}
|
||||||
let file_doc_path = files_path.join(format!("{}.md", sanitize_filename(&file_doc.path)));
|
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);
|
let mut file_content = format!("# File: {}\n\n", file_doc.path);
|
||||||
@@ -1,8 +1,47 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
|
|
||||||
|
/// Detect project name from pyproject.toml or directory basename.
|
||||||
|
fn detect_project_name(root: &str) -> String {
|
||||||
|
let root_path = std::path::Path::new(root);
|
||||||
|
|
||||||
|
// Try pyproject.toml
|
||||||
|
let pyproject_path = root_path.join("pyproject.toml");
|
||||||
|
if let Ok(content) = std::fs::read_to_string(&pyproject_path) {
|
||||||
|
let mut in_project = false;
|
||||||
|
for line in content.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed == "[project]" {
|
||||||
|
in_project = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if trimmed.starts_with('[') {
|
||||||
|
in_project = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if in_project && trimmed.starts_with("name") {
|
||||||
|
if let Some(val) = trimmed.split('=').nth(1) {
|
||||||
|
let name = val.trim().trim_matches('"').trim_matches('\'');
|
||||||
|
if !name.is_empty() {
|
||||||
|
return name.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: directory basename
|
||||||
|
root_path
|
||||||
|
.canonicalize()
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
|
||||||
|
.unwrap_or_else(|| "Project".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init_project(root: &str, out: &str) -> Result<()> {
|
pub fn init_project(root: &str, out: &str) -> Result<()> {
|
||||||
println!("{}", "Initializing archdoc project...".cyan().bold());
|
println!("{}", "Initializing wtismycode project...".cyan().bold());
|
||||||
|
|
||||||
|
let project_name = detect_project_name(root);
|
||||||
|
|
||||||
let out_path = std::path::Path::new(out);
|
let out_path = std::path::Path::new(out);
|
||||||
std::fs::create_dir_all(out_path)?;
|
std::fs::create_dir_all(out_path)?;
|
||||||
@@ -45,7 +84,15 @@ pub fn init_project(root: &str, out: &str) -> Result<()> {
|
|||||||
## Document metadata
|
## Document metadata
|
||||||
- **Created:** <AUTO_ON_INIT: YYYY-MM-DD>
|
- **Created:** <AUTO_ON_INIT: YYYY-MM-DD>
|
||||||
- **Updated:** <AUTO_ON_CHANGE: YYYY-MM-DD>
|
- **Updated:** <AUTO_ON_CHANGE: YYYY-MM-DD>
|
||||||
- **Generated by:** archdoc (cli) v0.1
|
- **Generated by:** wtismycode (cli) v0.1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
<!-- ARCHDOC:BEGIN section=integrations -->
|
||||||
|
> Generated. Do not edit inside this block.
|
||||||
|
<AUTO: detected integrations by category>
|
||||||
|
<!-- ARCHDOC:END section=integrations -->
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -87,8 +134,10 @@ pub fn init_project(root: &str, out: &str) -> Result<()> {
|
|||||||
<!-- MANUAL:END -->
|
<!-- MANUAL:END -->
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
let architecture_md_content = architecture_md_content.replace("<PROJECT_NAME>", &project_name);
|
||||||
|
|
||||||
let architecture_md_path = std::path::Path::new(root).join("ARCHITECTURE.md");
|
let architecture_md_path = std::path::Path::new(root).join("ARCHITECTURE.md");
|
||||||
std::fs::write(&architecture_md_path, architecture_md_content)?;
|
std::fs::write(&architecture_md_path, &architecture_md_content)?;
|
||||||
|
|
||||||
let config_toml_content = r#"[project]
|
let config_toml_content = r#"[project]
|
||||||
root = "."
|
root = "."
|
||||||
@@ -145,16 +194,16 @@ description_max_length = 200
|
|||||||
|
|
||||||
[logging]
|
[logging]
|
||||||
level = "info"
|
level = "info"
|
||||||
file = "archdoc.log"
|
file = "wtismycode.log"
|
||||||
format = "compact"
|
format = "compact"
|
||||||
|
|
||||||
[caching]
|
[caching]
|
||||||
enabled = true
|
enabled = true
|
||||||
cache_dir = ".archdoc/cache"
|
cache_dir = ".wtismycode/cache"
|
||||||
max_cache_age = "24h"
|
max_cache_age = "24h"
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let config_toml_path = std::path::Path::new(root).join("archdoc.toml");
|
let config_toml_path = std::path::Path::new(root).join("wtismycode.toml");
|
||||||
if !config_toml_path.exists() {
|
if !config_toml_path.exists() {
|
||||||
std::fs::write(&config_toml_path, config_toml_content)?;
|
std::fs::write(&config_toml_path, config_toml_content)?;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
use archdoc_core::ProjectModel;
|
use wtismycode_core::ProjectModel;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
|
|
||||||
pub fn print_stats(model: &ProjectModel) {
|
pub fn print_stats(model: &ProjectModel) {
|
||||||
println!();
|
println!();
|
||||||
println!("{}", "╔══════════════════════════════════════╗".cyan());
|
println!("{}", "╔══════════════════════════════════════╗".cyan());
|
||||||
println!("{}", "║ archdoc project statistics ║".cyan().bold());
|
println!("{}", "║ wtismycode project statistics ║".cyan().bold());
|
||||||
println!("{}", "╚══════════════════════════════════════╝".cyan());
|
println!("{}", "╚══════════════════════════════════════╝".cyan());
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
@@ -24,10 +24,10 @@ pub fn print_stats(model: &ProjectModel) {
|
|||||||
let mut async_functions = 0;
|
let mut async_functions = 0;
|
||||||
for symbol in model.symbols.values() {
|
for symbol in model.symbols.values() {
|
||||||
match symbol.kind {
|
match symbol.kind {
|
||||||
archdoc_core::model::SymbolKind::Function => functions += 1,
|
wtismycode_core::model::SymbolKind::Function => functions += 1,
|
||||||
archdoc_core::model::SymbolKind::Method => methods += 1,
|
wtismycode_core::model::SymbolKind::Method => methods += 1,
|
||||||
archdoc_core::model::SymbolKind::Class => classes += 1,
|
wtismycode_core::model::SymbolKind::Class => classes += 1,
|
||||||
archdoc_core::model::SymbolKind::AsyncFunction => async_functions += 1,
|
wtismycode_core::model::SymbolKind::AsyncFunction => async_functions += 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("{}", "Symbol breakdown".bold().underline());
|
println!("{}", "Symbol breakdown".bold().underline());
|
||||||
@@ -5,7 +5,7 @@ use clap::{Parser, Subcommand};
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "archdoc")]
|
#[command(name = "wtismycode")]
|
||||||
#[command(about = "Generate architecture documentation for Python projects")]
|
#[command(about = "Generate architecture documentation for Python projects")]
|
||||||
#[command(version = "0.1.0")]
|
#[command(version = "0.1.0")]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
@@ -19,7 +19,7 @@ pub struct Cli {
|
|||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// Initialize archdoc in the project
|
/// Initialize wtismycode in the project
|
||||||
Init {
|
Init {
|
||||||
#[arg(short, long, default_value = ".")]
|
#[arg(short, long, default_value = ".")]
|
||||||
root: String,
|
root: String,
|
||||||
@@ -32,21 +32,24 @@ enum Commands {
|
|||||||
root: String,
|
root: String,
|
||||||
#[arg(short, long, default_value = "docs/architecture")]
|
#[arg(short, long, default_value = "docs/architecture")]
|
||||||
out: String,
|
out: String,
|
||||||
#[arg(short, long, default_value = "archdoc.toml")]
|
#[arg(short, long, default_value = "wtismycode.toml")]
|
||||||
config: String,
|
config: String,
|
||||||
|
/// Show what would be generated without writing files
|
||||||
|
#[arg(long)]
|
||||||
|
dry_run: bool,
|
||||||
},
|
},
|
||||||
/// Check if documentation is up to date
|
/// Check if documentation is up to date
|
||||||
Check {
|
Check {
|
||||||
#[arg(short, long, default_value = ".")]
|
#[arg(short, long, default_value = ".")]
|
||||||
root: String,
|
root: String,
|
||||||
#[arg(short, long, default_value = "archdoc.toml")]
|
#[arg(short, long, default_value = "wtismycode.toml")]
|
||||||
config: String,
|
config: String,
|
||||||
},
|
},
|
||||||
/// Show project statistics
|
/// Show project statistics
|
||||||
Stats {
|
Stats {
|
||||||
#[arg(short, long, default_value = ".")]
|
#[arg(short, long, default_value = ".")]
|
||||||
root: String,
|
root: String,
|
||||||
#[arg(short, long, default_value = "archdoc.toml")]
|
#[arg(short, long, default_value = "wtismycode.toml")]
|
||||||
config: String,
|
config: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -58,10 +61,14 @@ fn main() -> Result<()> {
|
|||||||
Commands::Init { root, out } => {
|
Commands::Init { root, out } => {
|
||||||
commands::init::init_project(root, out)?;
|
commands::init::init_project(root, out)?;
|
||||||
}
|
}
|
||||||
Commands::Generate { root, out, config } => {
|
Commands::Generate { root, out, config, dry_run } => {
|
||||||
let config = commands::generate::load_config(config)?;
|
let config = commands::generate::load_config(config)?;
|
||||||
let model = commands::generate::analyze_project(root, &config)?;
|
let model = commands::generate::analyze_project(root, &config)?;
|
||||||
commands::generate::generate_docs(&model, out, cli.verbose)?;
|
if *dry_run {
|
||||||
|
commands::generate::dry_run_docs(&model, out, &config)?;
|
||||||
|
} else {
|
||||||
|
commands::generate::generate_docs(&model, out, cli.verbose, &config)?;
|
||||||
|
}
|
||||||
output::print_generate_summary(&model);
|
output::print_generate_summary(&model);
|
||||||
}
|
}
|
||||||
Commands::Check { root, config } => {
|
Commands::Check { root, config } => {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Colored output helpers and filename utilities for ArchDoc CLI
|
//! Colored output helpers and filename utilities for WTIsMyCode CLI
|
||||||
|
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use archdoc_core::ProjectModel;
|
use wtismycode_core::ProjectModel;
|
||||||
|
|
||||||
/// Sanitize a file path into a safe filename for docs.
|
/// Sanitize a file path into a safe filename for docs.
|
||||||
/// Removes `./` prefix, replaces `/` with `__`.
|
/// Removes `./` prefix, replaces `/` with `__`.
|
||||||
@@ -24,6 +24,8 @@ pub fn print_generate_summary(model: &ProjectModel) {
|
|||||||
if model.symbols.values().any(|s| s.integrations_flags.http) { v.push("HTTP"); }
|
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.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.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
|
v
|
||||||
};
|
};
|
||||||
if !integrations.is_empty() {
|
if !integrations.is_empty() {
|
||||||
1
wtismycode-core/.wtismycode/cache/1dd9479f63eeeea5.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/1dd9479f63eeeea5.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:21.939017204Z","file_modified_at":"2026-02-15T09:12:21.938241573Z","parsed_module":{"path":"/tmp/.tmpjrzBI1/test.py","module_path":"/tmp/.tmpjrzBI1/test.py","imports":[],"symbols":[{"id":"calculate_sum","kind":"Function","module_id":"","file_id":"","qualname":"calculate_sum","signature":"def calculate_sum(a, b)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/22f137dfd1267b44.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/22f137dfd1267b44.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:21.929046662Z","file_modified_at":"2026-02-15T09:12:21.928241645Z","parsed_module":{"path":"/tmp/.tmpucjtMF/test.py","module_path":"/tmp/.tmpucjtMF/test.py","imports":[{"module_name":"redis","alias":null,"line_number":8}],"symbols":[{"id":"process_job","kind":"Function","module_id":"","file_id":"","qualname":"process_job","signature":"def process_job(job_data)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[{"caller_symbol":"unknown","callee_expr":"redis.Redis","line_number":55,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"client.lpush","line_number":73,"call_type":"Unresolved"},{"caller_symbol":"process_job","callee_expr":"redis.Redis","line_number":55,"call_type":"Unresolved"},{"caller_symbol":"process_job","callee_expr":"client.lpush","line_number":73,"call_type":"Unresolved"}],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/242d46dd3d930a62.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/242d46dd3d930a62.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:21.901000313Z","file_modified_at":"2026-02-15T09:12:21.900241847Z","parsed_module":{"path":"/tmp/.tmpQwpTTi/test.py","module_path":"/tmp/.tmpQwpTTi/test.py","imports":[],"symbols":[{"id":"hello","kind":"Function","module_id":"","file_id":"","qualname":"hello","signature":"def hello()","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"Calculator","kind":"Class","module_id":"","file_id":"","qualname":"Calculator","signature":"class Calculator","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"Calculator.add","kind":"Method","module_id":"","file_id":"","qualname":"Calculator.add","signature":"def add(self, a, b)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/2d1d3488fad06abc.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/2d1d3488fad06abc.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:27.638281687Z","file_modified_at":"2026-02-15T09:12:27.637200566Z","parsed_module":{"path":"/tmp/.tmp5HECBh/test.py","module_path":"/tmp/.tmp5HECBh/test.py","imports":[{"module_name":"requests","alias":null,"line_number":8}],"symbols":[{"id":"fetch_data","kind":"Function","module_id":"","file_id":"","qualname":"fetch_data","signature":"def fetch_data()","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[{"caller_symbol":"unknown","callee_expr":"requests.get","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"response.json","line_number":107,"call_type":"Unresolved"},{"caller_symbol":"fetch_data","callee_expr":"requests.get","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"fetch_data","callee_expr":"response.json","line_number":107,"call_type":"Unresolved"}],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/323af6c33c893dc9.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/323af6c33c893dc9.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:21.938417589Z","file_modified_at":"2026-02-15T09:12:21.937241580Z","parsed_module":{"path":"/tmp/.tmpHn93FX/test.py","module_path":"/tmp/.tmpHn93FX/test.py","imports":[{"module_name":"requests","alias":null,"line_number":8}],"symbols":[{"id":"fetch_data","kind":"Function","module_id":"","file_id":"","qualname":"fetch_data","signature":"def fetch_data()","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[{"caller_symbol":"unknown","callee_expr":"requests.get","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"response.json","line_number":107,"call_type":"Unresolved"},{"caller_symbol":"fetch_data","callee_expr":"requests.get","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"fetch_data","callee_expr":"response.json","line_number":107,"call_type":"Unresolved"}],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/332464b9176fa65a.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/332464b9176fa65a.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:21.900267168Z","file_modified_at":"2026-02-15T09:12:21.899241854Z","parsed_module":{"path":"/tmp/.tmpVPUjB4/test.py","module_path":"/tmp/.tmpVPUjB4/test.py","imports":[],"symbols":[{"id":"hello","kind":"Function","module_id":"","file_id":"","qualname":"hello","signature":"def hello()","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/34c7d0f0a5859bc4.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/34c7d0f0a5859bc4.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
wtismycode-core/.wtismycode/cache/3f48e681f7e81aa3.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/3f48e681f7e81aa3.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:21.939756459Z","file_modified_at":"2026-02-15T09:12:21.938241573Z","parsed_module":{"path":"/tmp/.tmp5yAI8O/test.py","module_path":"/tmp/.tmp5yAI8O/test.py","imports":[{"module_name":"redis","alias":null,"line_number":8}],"symbols":[{"id":"process_job","kind":"Function","module_id":"","file_id":"","qualname":"process_job","signature":"def process_job(job_data)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[{"caller_symbol":"unknown","callee_expr":"redis.Redis","line_number":55,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"client.lpush","line_number":73,"call_type":"Unresolved"},{"caller_symbol":"process_job","callee_expr":"redis.Redis","line_number":55,"call_type":"Unresolved"},{"caller_symbol":"process_job","callee_expr":"client.lpush","line_number":73,"call_type":"Unresolved"}],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/4427b32031669c3a.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/4427b32031669c3a.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:21.949122466Z","file_modified_at":"2026-02-15T00:22:51.124088300Z","parsed_module":{"path":"../test-project/src/utils.py","module_path":"../test-project/src/utils.py","imports":[{"module_name":"json","alias":null,"line_number":54},{"module_name":"os","alias":null,"line_number":66}],"symbols":[{"id":"load_config","kind":"Function","module_id":"","file_id":"","qualname":"load_config","signature":"def load_config(config_path: str)","annotations":null,"docstring_first_line":"Load configuration from a JSON file.","purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"save_config","kind":"Function","module_id":"","file_id":"","qualname":"save_config","signature":"def save_config(config: dict, config_path: str)","annotations":null,"docstring_first_line":"Save configuration to a JSON file.","purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"get_file_size","kind":"Function","module_id":"","file_id":"","qualname":"get_file_size","signature":"def get_file_size(filepath: str)","annotations":null,"docstring_first_line":"Get the size of a file in bytes.","purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"format_bytes","kind":"Function","module_id":"","file_id":"","qualname":"format_bytes","signature":"def format_bytes(size: int)","annotations":null,"docstring_first_line":"Format bytes into a human-readable string.","purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[{"caller_symbol":"unknown","callee_expr":"open","line_number":169,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"json.load","line_number":213,"call_type":"Unresolved"},{"caller_symbol":"load_config","callee_expr":"open","line_number":169,"call_type":"Unresolved"},{"caller_symbol":"load_config","callee_expr":"json.load","line_number":213,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"open","line_number":330,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"json.dump","line_number":367,"call_type":"Unresolved"},{"caller_symbol":"save_config","callee_expr":"open","line_number":330,"call_type":"Unresolved"},{"caller_symbol":"save_config","callee_expr":"json.dump","line_number":367,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"os.path.getsize","line_number":494,"call_type":"Unresolved"},{"caller_symbol":"get_file_size","callee_expr":"os.path.getsize","line_number":494,"call_type":"Unresolved"}],"file_docstring":"Utility functions for the test project."}}
|
||||||
1
wtismycode-core/.wtismycode/cache/44b31aff14e80d6b.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/44b31aff14e80d6b.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:21.932282950Z","file_modified_at":"2026-02-15T09:12:21.931241624Z","parsed_module":{"path":"/tmp/.tmpMK4GyS/test.py","module_path":"/tmp/.tmpMK4GyS/test.py","imports":[],"symbols":[{"id":"hello","kind":"Function","module_id":"","file_id":"","qualname":"hello","signature":"def hello()","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"goodbye","kind":"Function","module_id":"","file_id":"","qualname":"goodbye","signature":"def goodbye()","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/6b46d7daa9d35ecf.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/6b46d7daa9d35ecf.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:27.646855488Z","file_modified_at":"2026-02-15T09:12:27.645200509Z","parsed_module":{"path":"/tmp/.tmpXh0uQg/test.py","module_path":"/tmp/.tmpXh0uQg/test.py","imports":[],"symbols":[{"id":"calculate_sum","kind":"Function","module_id":"","file_id":"","qualname":"calculate_sum","signature":"def calculate_sum(a, b)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/7ff0f715bb184391.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/7ff0f715bb184391.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:21.932289740Z","file_modified_at":"2026-02-15T09:12:21.931241624Z","parsed_module":{"path":"/tmp/.tmpn1WePQ/test.py","module_path":"/tmp/.tmpn1WePQ/test.py","imports":[],"symbols":[{"id":"hello","kind":"Function","module_id":"","file_id":"","qualname":"hello","signature":"def hello()","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"Calculator","kind":"Class","module_id":"","file_id":"","qualname":"Calculator","signature":"class Calculator","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"Calculator.add","kind":"Method","module_id":"","file_id":"","qualname":"Calculator.add","signature":"def add(self, a, b)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/80d24a35240626da.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/80d24a35240626da.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:27.646347331Z","file_modified_at":"2026-02-15T09:12:27.645200509Z","parsed_module":{"path":"/tmp/.tmpFFmDl3/test.py","module_path":"/tmp/.tmpFFmDl3/test.py","imports":[{"module_name":"sqlite3","alias":null,"line_number":8}],"symbols":[{"id":"get_user","kind":"Function","module_id":"","file_id":"","qualname":"get_user","signature":"def get_user(user_id)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[{"caller_symbol":"unknown","callee_expr":"sqlite3.connect","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"conn.cursor","line_number":95,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"cursor.execute","line_number":113,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"cursor.fetchone","line_number":187,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"sqlite3.connect","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"conn.cursor","line_number":95,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"cursor.execute","line_number":113,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"cursor.fetchone","line_number":187,"call_type":"Unresolved"}],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/8e89f71b0bea2e6d.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/8e89f71b0bea2e6d.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:21.937802033Z","file_modified_at":"2026-02-15T09:12:21.936241587Z","parsed_module":{"path":"/tmp/.tmpU9hOcm/test.py","module_path":"/tmp/.tmpU9hOcm/test.py","imports":[{"module_name":"sqlite3","alias":null,"line_number":8}],"symbols":[{"id":"get_user","kind":"Function","module_id":"","file_id":"","qualname":"get_user","signature":"def get_user(user_id)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[{"caller_symbol":"unknown","callee_expr":"sqlite3.connect","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"conn.cursor","line_number":95,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"cursor.execute","line_number":113,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"cursor.fetchone","line_number":187,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"sqlite3.connect","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"conn.cursor","line_number":95,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"cursor.execute","line_number":113,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"cursor.fetchone","line_number":187,"call_type":"Unresolved"}],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/90460d6c369f9d4c.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/90460d6c369f9d4c.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:27.646167123Z","file_modified_at":"2026-02-15T09:12:27.645200509Z","parsed_module":{"path":"/tmp/.tmpj84SS2/test.py","module_path":"/tmp/.tmpj84SS2/test.py","imports":[{"module_name":"requests","alias":null,"line_number":8}],"symbols":[{"id":"fetch_data","kind":"Function","module_id":"","file_id":"","qualname":"fetch_data","signature":"def fetch_data()","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[{"caller_symbol":"unknown","callee_expr":"requests.get","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"response.json","line_number":107,"call_type":"Unresolved"},{"caller_symbol":"fetch_data","callee_expr":"requests.get","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"fetch_data","callee_expr":"response.json","line_number":107,"call_type":"Unresolved"}],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/a8dcf5363a5ef953.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/a8dcf5363a5ef953.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:27.647109436Z","file_modified_at":"2026-02-15T09:12:27.646200502Z","parsed_module":{"path":"/tmp/.tmpTS6Kf7/test.py","module_path":"/tmp/.tmpTS6Kf7/test.py","imports":[{"module_name":"redis","alias":null,"line_number":8}],"symbols":[{"id":"process_job","kind":"Function","module_id":"","file_id":"","qualname":"process_job","signature":"def process_job(job_data)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[{"caller_symbol":"unknown","callee_expr":"redis.Redis","line_number":55,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"client.lpush","line_number":73,"call_type":"Unresolved"},{"caller_symbol":"process_job","callee_expr":"redis.Redis","line_number":55,"call_type":"Unresolved"},{"caller_symbol":"process_job","callee_expr":"client.lpush","line_number":73,"call_type":"Unresolved"}],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/ae981a5f144a6f7a.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/ae981a5f144a6f7a.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:21.906280597Z","file_modified_at":"2026-02-15T00:21:25.872722975Z","parsed_module":{"path":"tests/golden/test_project/src/example.py","module_path":"tests/golden/test_project/src/example.py","imports":[{"module_name":"os","alias":null,"line_number":42},{"module_name":"typing.List","alias":null,"line_number":64}],"symbols":[{"id":"Calculator","kind":"Class","module_id":"","file_id":"","qualname":"Calculator","signature":"class Calculator","annotations":null,"docstring_first_line":"A simple calculator class.","purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"Calculator.__init__","kind":"Method","module_id":"","file_id":"","qualname":"Calculator.__init__","signature":"def __init__(self)","annotations":null,"docstring_first_line":"Initialize the calculator.","purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"Calculator.add","kind":"Method","module_id":"","file_id":"","qualname":"Calculator.add","signature":"def add(self, a: int, b: int)","annotations":null,"docstring_first_line":"Add two numbers.","purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"Calculator.multiply","kind":"Method","module_id":"","file_id":"","qualname":"Calculator.multiply","signature":"def multiply(self, a: int, b: int)","annotations":null,"docstring_first_line":"Multiply two numbers.","purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"process_numbers","kind":"Function","module_id":"","file_id":"","qualname":"process_numbers","signature":"def process_numbers(numbers: List[int])","annotations":null,"docstring_first_line":"Process a list of numbers.","purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[{"caller_symbol":"unknown","callee_expr":"Calculator","line_number":519,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"calc.add","line_number":544,"call_type":"Unresolved"},{"caller_symbol":"process_numbers","callee_expr":"Calculator","line_number":519,"call_type":"Unresolved"},{"caller_symbol":"process_numbers","callee_expr":"calc.add","line_number":544,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"process_numbers","line_number":648,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"print","line_number":677,"call_type":"Unresolved"}],"file_docstring":"Example module for testing."}}
|
||||||
1
wtismycode-core/.wtismycode/cache/af6c11e9a59f28dd.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/af6c11e9a59f28dd.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:27.639487788Z","file_modified_at":"2026-02-15T09:12:27.638200559Z","parsed_module":{"path":"/tmp/.tmp7gcSsx/test.py","module_path":"/tmp/.tmp7gcSsx/test.py","imports":[{"module_name":"redis","alias":null,"line_number":8}],"symbols":[{"id":"process_job","kind":"Function","module_id":"","file_id":"","qualname":"process_job","signature":"def process_job(job_data)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[{"caller_symbol":"unknown","callee_expr":"redis.Redis","line_number":55,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"client.lpush","line_number":73,"call_type":"Unresolved"},{"caller_symbol":"process_job","callee_expr":"redis.Redis","line_number":55,"call_type":"Unresolved"},{"caller_symbol":"process_job","callee_expr":"client.lpush","line_number":73,"call_type":"Unresolved"}],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/b74dd266405fda26.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/b74dd266405fda26.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:27.623913794Z","file_modified_at":"2026-02-15T09:12:27.622200674Z","parsed_module":{"path":"/tmp/.tmpY5jXEG/test.py","module_path":"/tmp/.tmpY5jXEG/test.py","imports":[],"symbols":[{"id":"hello","kind":"Function","module_id":"","file_id":"","qualname":"hello","signature":"def hello()","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"Calculator","kind":"Class","module_id":"","file_id":"","qualname":"Calculator","signature":"class Calculator","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"Calculator.add","kind":"Method","module_id":"","file_id":"","qualname":"Calculator.add","signature":"def add(self, a, b)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/b967ef0258ec1d92.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/b967ef0258ec1d92.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:27.623293468Z","file_modified_at":"2026-02-15T09:12:27.622200674Z","parsed_module":{"path":"/tmp/.tmpbimwTO/test.py","module_path":"/tmp/.tmpbimwTO/test.py","imports":[],"symbols":[{"id":"hello","kind":"Function","module_id":"","file_id":"","qualname":"hello","signature":"def hello()","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/ca89f5c4de39cd5c.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/ca89f5c4de39cd5c.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:27.638405646Z","file_modified_at":"2026-02-15T09:12:27.637200566Z","parsed_module":{"path":"/tmp/.tmpDqAWXp/test.py","module_path":"/tmp/.tmpDqAWXp/test.py","imports":[{"module_name":"sqlite3","alias":null,"line_number":8}],"symbols":[{"id":"get_user","kind":"Function","module_id":"","file_id":"","qualname":"get_user","signature":"def get_user(user_id)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[{"caller_symbol":"unknown","callee_expr":"sqlite3.connect","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"conn.cursor","line_number":95,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"cursor.execute","line_number":113,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"cursor.fetchone","line_number":187,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"sqlite3.connect","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"conn.cursor","line_number":95,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"cursor.execute","line_number":113,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"cursor.fetchone","line_number":187,"call_type":"Unresolved"}],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/cc39a913d23e0148.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/cc39a913d23e0148.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:21.928408667Z","file_modified_at":"2026-02-15T09:12:21.927241652Z","parsed_module":{"path":"/tmp/.tmpkuoSO4/test.py","module_path":"/tmp/.tmpkuoSO4/test.py","imports":[],"symbols":[{"id":"calculate_sum","kind":"Function","module_id":"","file_id":"","qualname":"calculate_sum","signature":"def calculate_sum(a, b)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/d49cc1c393cf173e.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/d49cc1c393cf173e.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:27.642603187Z","file_modified_at":"2026-02-15T09:12:27.641200538Z","parsed_module":{"path":"/tmp/.tmplZ7Gfg/test.py","module_path":"/tmp/.tmplZ7Gfg/test.py","imports":[],"symbols":[{"id":"hello","kind":"Function","module_id":"","file_id":"","qualname":"hello","signature":"def hello()","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"Calculator","kind":"Class","module_id":"","file_id":"","qualname":"Calculator","signature":"class Calculator","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"Calculator.add","kind":"Method","module_id":"","file_id":"","qualname":"Calculator.add","signature":"def add(self, a, b)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/d93abaa965fa2d8d.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/d93abaa965fa2d8d.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:27.642573298Z","file_modified_at":"2026-02-15T09:12:27.641200538Z","parsed_module":{"path":"/tmp/.tmpiVOCMi/test.py","module_path":"/tmp/.tmpiVOCMi/test.py","imports":[],"symbols":[{"id":"hello","kind":"Function","module_id":"","file_id":"","qualname":"hello","signature":"def hello()","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}},{"id":"goodbye","kind":"Function","module_id":"","file_id":"","qualname":"goodbye","signature":"def goodbye()","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/ddc166202153e62e.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/ddc166202153e62e.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:21.927910330Z","file_modified_at":"2026-02-15T09:12:21.926241659Z","parsed_module":{"path":"/tmp/.tmp1gFjk3/test.py","module_path":"/tmp/.tmp1gFjk3/test.py","imports":[{"module_name":"sqlite3","alias":null,"line_number":8}],"symbols":[{"id":"get_user","kind":"Function","module_id":"","file_id":"","qualname":"get_user","signature":"def get_user(user_id)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[{"caller_symbol":"unknown","callee_expr":"sqlite3.connect","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"conn.cursor","line_number":95,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"cursor.execute","line_number":113,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"cursor.fetchone","line_number":187,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"sqlite3.connect","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"conn.cursor","line_number":95,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"cursor.execute","line_number":113,"call_type":"Unresolved"},{"caller_symbol":"get_user","callee_expr":"cursor.fetchone","line_number":187,"call_type":"Unresolved"}],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/e9433f25871e418.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/e9433f25871e418.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:21.927753122Z","file_modified_at":"2026-02-15T09:12:21.926241659Z","parsed_module":{"path":"/tmp/.tmpp9A45l/test.py","module_path":"/tmp/.tmpp9A45l/test.py","imports":[{"module_name":"requests","alias":null,"line_number":8}],"symbols":[{"id":"fetch_data","kind":"Function","module_id":"","file_id":"","qualname":"fetch_data","signature":"def fetch_data()","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[{"caller_symbol":"unknown","callee_expr":"requests.get","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"unknown","callee_expr":"response.json","line_number":107,"call_type":"Unresolved"},{"caller_symbol":"fetch_data","callee_expr":"requests.get","line_number":51,"call_type":"Unresolved"},{"caller_symbol":"fetch_data","callee_expr":"response.json","line_number":107,"call_type":"Unresolved"}],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/f1a291dc5a093458.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/f1a291dc5a093458.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"created_at":"2026-02-15T09:12:27.638896492Z","file_modified_at":"2026-02-15T09:12:27.638200559Z","parsed_module":{"path":"/tmp/.tmp7IEFw5/test.py","module_path":"/tmp/.tmp7IEFw5/test.py","imports":[],"symbols":[{"id":"calculate_sum","kind":"Function","module_id":"","file_id":"","qualname":"calculate_sum","signature":"def calculate_sum(a, b)","annotations":null,"docstring_first_line":null,"purpose":"extracted from AST","outbound_calls":[],"inbound_calls":[],"integrations_flags":{"http":false,"db":false,"queue":false,"storage":false,"ai":false},"metrics":{"fan_in":0,"fan_out":0,"is_critical":false,"cycle_participant":false}}],"calls":[],"file_docstring":null}}
|
||||||
1
wtismycode-core/.wtismycode/cache/f1b45c4f58b2d0dc.json
vendored
Normal file
1
wtismycode-core/.wtismycode/cache/f1b45c4f58b2d0dc.json
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archdoc-core"
|
name = "wtismycode-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
//! Caching module for ArchDoc
|
//! Caching module for WTIsMyCode
|
||||||
//!
|
//!
|
||||||
//! This module provides caching functionality to speed up repeated analysis
|
//! This module provides caching functionality to speed up repeated analysis
|
||||||
//! by storing parsed ASTs and analysis results.
|
//! by storing parsed ASTs and analysis results.
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::errors::ArchDocError;
|
use crate::errors::WTIsMyCodeError;
|
||||||
use crate::model::ParsedModule;
|
use crate::model::ParsedModule;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -39,7 +39,7 @@ impl CacheManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get cached parsed module if available and not expired
|
/// Get cached parsed module if available and not expired
|
||||||
pub fn get_cached_module(&self, file_path: &Path) -> Result<Option<ParsedModule>, ArchDocError> {
|
pub fn get_cached_module(&self, file_path: &Path) -> Result<Option<ParsedModule>, WTIsMyCodeError> {
|
||||||
if !self.config.caching.enabled {
|
if !self.config.caching.enabled {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@@ -53,10 +53,10 @@ impl CacheManager {
|
|||||||
|
|
||||||
// Read cache file
|
// Read cache file
|
||||||
let content = fs::read_to_string(&cache_file)
|
let content = fs::read_to_string(&cache_file)
|
||||||
.map_err(ArchDocError::Io)?;
|
.map_err(WTIsMyCodeError::Io)?;
|
||||||
|
|
||||||
let cache_entry: CacheEntry = serde_json::from_str(&content)
|
let cache_entry: CacheEntry = serde_json::from_str(&content)
|
||||||
.map_err(|e| ArchDocError::AnalysisError(format!("Failed to deserialize cache entry: {}", e)))?;
|
.map_err(|e| WTIsMyCodeError::AnalysisError(format!("Failed to deserialize cache entry: {}", e)))?;
|
||||||
|
|
||||||
// Check if cache is expired
|
// Check if cache is expired
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
@@ -73,10 +73,10 @@ impl CacheManager {
|
|||||||
|
|
||||||
// Check if source file has been modified since caching
|
// Check if source file has been modified since caching
|
||||||
let metadata = fs::metadata(file_path)
|
let metadata = fs::metadata(file_path)
|
||||||
.map_err(ArchDocError::Io)?;
|
.map_err(WTIsMyCodeError::Io)?;
|
||||||
|
|
||||||
let modified_time = metadata.modified()
|
let modified_time = metadata.modified()
|
||||||
.map_err(ArchDocError::Io)?;
|
.map_err(WTIsMyCodeError::Io)?;
|
||||||
|
|
||||||
let modified_time: DateTime<Utc> = modified_time.into();
|
let modified_time: DateTime<Utc> = modified_time.into();
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ impl CacheManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Store parsed module in cache
|
/// Store parsed module in cache
|
||||||
pub fn store_module(&self, file_path: &Path, parsed_module: ParsedModule) -> Result<(), ArchDocError> {
|
pub fn store_module(&self, file_path: &Path, parsed_module: ParsedModule) -> Result<(), WTIsMyCodeError> {
|
||||||
if !self.config.caching.enabled {
|
if !self.config.caching.enabled {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -100,10 +100,10 @@ impl CacheManager {
|
|||||||
|
|
||||||
// Get file modification time
|
// Get file modification time
|
||||||
let metadata = fs::metadata(file_path)
|
let metadata = fs::metadata(file_path)
|
||||||
.map_err(ArchDocError::Io)?;
|
.map_err(WTIsMyCodeError::Io)?;
|
||||||
|
|
||||||
let modified_time = metadata.modified()
|
let modified_time = metadata.modified()
|
||||||
.map_err(ArchDocError::Io)?;
|
.map_err(WTIsMyCodeError::Io)?;
|
||||||
|
|
||||||
let modified_time: DateTime<Utc> = modified_time.into();
|
let modified_time: DateTime<Utc> = modified_time.into();
|
||||||
|
|
||||||
@@ -114,10 +114,10 @@ impl CacheManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let content = serde_json::to_string(&cache_entry)
|
let content = serde_json::to_string(&cache_entry)
|
||||||
.map_err(|e| ArchDocError::AnalysisError(format!("Failed to serialize cache entry: {}", e)))?;
|
.map_err(|e| WTIsMyCodeError::AnalysisError(format!("Failed to serialize cache entry: {}", e)))?;
|
||||||
|
|
||||||
fs::write(&cache_file, content)
|
fs::write(&cache_file, content)
|
||||||
.map_err(ArchDocError::Io)
|
.map_err(WTIsMyCodeError::Io)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate cache key for a file path
|
/// Generate cache key for a file path
|
||||||
@@ -133,7 +133,7 @@ impl CacheManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse duration string like "24h" or "7d" into seconds
|
/// Parse duration string like "24h" or "7d" into seconds
|
||||||
fn parse_duration(&self, duration_str: &str) -> Result<u64, ArchDocError> {
|
fn parse_duration(&self, duration_str: &str) -> Result<u64, WTIsMyCodeError> {
|
||||||
if duration_str.is_empty() {
|
if duration_str.is_empty() {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
@@ -141,26 +141,26 @@ impl CacheManager {
|
|||||||
let chars: Vec<char> = duration_str.chars().collect();
|
let chars: Vec<char> = duration_str.chars().collect();
|
||||||
let (number_str, unit) = chars.split_at(chars.len() - 1);
|
let (number_str, unit) = chars.split_at(chars.len() - 1);
|
||||||
let number: u64 = number_str.iter().collect::<String>().parse()
|
let number: u64 = number_str.iter().collect::<String>().parse()
|
||||||
.map_err(|_| ArchDocError::AnalysisError(format!("Invalid duration format: {}", duration_str)))?;
|
.map_err(|_| WTIsMyCodeError::AnalysisError(format!("Invalid duration format: {}", duration_str)))?;
|
||||||
|
|
||||||
match unit[0] {
|
match unit[0] {
|
||||||
's' => Ok(number), // seconds
|
's' => Ok(number), // seconds
|
||||||
'm' => Ok(number * 60), // minutes
|
'm' => Ok(number * 60), // minutes
|
||||||
'h' => Ok(number * 3600), // hours
|
'h' => Ok(number * 3600), // hours
|
||||||
'd' => Ok(number * 86400), // days
|
'd' => Ok(number * 86400), // days
|
||||||
_ => Err(ArchDocError::AnalysisError(format!("Unknown duration unit: {}", unit[0]))),
|
_ => Err(WTIsMyCodeError::AnalysisError(format!("Unknown duration unit: {}", unit[0]))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all cache entries
|
/// Clear all cache entries
|
||||||
pub fn clear_cache(&self) -> Result<(), ArchDocError> {
|
pub fn clear_cache(&self) -> Result<(), WTIsMyCodeError> {
|
||||||
if Path::new(&self.cache_dir).exists() {
|
if Path::new(&self.cache_dir).exists() {
|
||||||
fs::remove_dir_all(&self.cache_dir)
|
fs::remove_dir_all(&self.cache_dir)
|
||||||
.map_err(ArchDocError::Io)?;
|
.map_err(WTIsMyCodeError::Io)?;
|
||||||
|
|
||||||
// Recreate cache directory
|
// Recreate cache directory
|
||||||
fs::create_dir_all(&self.cache_dir)
|
fs::create_dir_all(&self.cache_dir)
|
||||||
.map_err(ArchDocError::Io)?;
|
.map_err(WTIsMyCodeError::Io)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
//! Configuration management for ArchDoc
|
//! Configuration management for WTIsMyCode
|
||||||
//!
|
//!
|
||||||
//! This module handles loading and validating the archdoc.toml configuration file.
|
//! This module handles loading and validating the wtismycode.toml configuration file.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use crate::errors::ArchDocError;
|
use crate::errors::WTIsMyCodeError;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -383,7 +383,7 @@ fn default_log_level() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_log_file() -> String {
|
fn default_log_file() -> String {
|
||||||
"archdoc.log".to_string()
|
"wtismycode.log".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_log_format() -> String {
|
fn default_log_format() -> String {
|
||||||
@@ -415,7 +415,7 @@ fn default_caching_enabled() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_cache_dir() -> String {
|
fn default_cache_dir() -> String {
|
||||||
".archdoc/cache".to_string()
|
".wtismycode/cache".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_max_cache_age() -> String {
|
fn default_max_cache_age() -> String {
|
||||||
@@ -423,21 +423,213 @@ fn default_max_cache_age() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
/// Validate the configuration for correctness.
|
||||||
|
///
|
||||||
|
/// Checks that paths exist, values are parseable, and settings are sensible.
|
||||||
|
pub fn validate(&self) -> Result<(), WTIsMyCodeError> {
|
||||||
|
// Check project.root exists and is a directory
|
||||||
|
let root = Path::new(&self.project.root);
|
||||||
|
if !root.exists() {
|
||||||
|
return Err(WTIsMyCodeError::ConfigError(format!(
|
||||||
|
"project.root '{}' does not exist",
|
||||||
|
self.project.root
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if !root.is_dir() {
|
||||||
|
return Err(WTIsMyCodeError::ConfigError(format!(
|
||||||
|
"project.root '{}' is not a directory",
|
||||||
|
self.project.root
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check language is python
|
||||||
|
if self.project.language != "python" {
|
||||||
|
return Err(WTIsMyCodeError::ConfigError(format!(
|
||||||
|
"project.language '{}' is not supported. Only 'python' is currently supported",
|
||||||
|
self.project.language
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check scan.include is not empty
|
||||||
|
if self.scan.include.is_empty() {
|
||||||
|
return Err(WTIsMyCodeError::ConfigError(
|
||||||
|
"scan.include must not be empty — at least one directory must be specified".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check python.src_roots exist relative to project.root
|
||||||
|
for src_root in &self.python.src_roots {
|
||||||
|
let path = root.join(src_root);
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(WTIsMyCodeError::ConfigError(format!(
|
||||||
|
"python.src_roots entry '{}' does not exist (resolved to '{}')",
|
||||||
|
src_root,
|
||||||
|
path.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse max_cache_age
|
||||||
|
parse_duration(&self.caching.max_cache_age).map_err(|e| {
|
||||||
|
WTIsMyCodeError::ConfigError(format!(
|
||||||
|
"caching.max_cache_age '{}' is not valid: {}. Use formats like '24h', '7d', '30m'",
|
||||||
|
self.caching.max_cache_age, e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Parse max_file_size
|
||||||
|
parse_file_size(&self.scan.max_file_size).map_err(|e| {
|
||||||
|
WTIsMyCodeError::ConfigError(format!(
|
||||||
|
"scan.max_file_size '{}' is not valid: {}. Use formats like '10MB', '1GB', '500KB'",
|
||||||
|
self.scan.max_file_size, e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Load configuration from a TOML file
|
/// Load configuration from a TOML file
|
||||||
pub fn load_from_file(path: &Path) -> Result<Self, ArchDocError> {
|
pub fn load_from_file(path: &Path) -> Result<Self, WTIsMyCodeError> {
|
||||||
let content = std::fs::read_to_string(path)
|
let content = std::fs::read_to_string(path)
|
||||||
.map_err(|e| ArchDocError::ConfigError(format!("Failed to read config file: {}", e)))?;
|
.map_err(|e| WTIsMyCodeError::ConfigError(format!("Failed to read config file: {}", e)))?;
|
||||||
|
|
||||||
toml::from_str(&content)
|
toml::from_str(&content)
|
||||||
.map_err(|e| ArchDocError::ConfigError(format!("Failed to parse config file: {}", e)))
|
.map_err(|e| WTIsMyCodeError::ConfigError(format!("Failed to parse config file: {}", e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save configuration to a TOML file
|
/// Save configuration to a TOML file
|
||||||
pub fn save_to_file(&self, path: &Path) -> Result<(), ArchDocError> {
|
pub fn save_to_file(&self, path: &Path) -> Result<(), WTIsMyCodeError> {
|
||||||
let content = toml::to_string_pretty(self)
|
let content = toml::to_string_pretty(self)
|
||||||
.map_err(|e| ArchDocError::ConfigError(format!("Failed to serialize config: {}", e)))?;
|
.map_err(|e| WTIsMyCodeError::ConfigError(format!("Failed to serialize config: {}", e)))?;
|
||||||
|
|
||||||
std::fs::write(path, content)
|
std::fs::write(path, content)
|
||||||
.map_err(|e| ArchDocError::ConfigError(format!("Failed to write config file: {}", e)))
|
.map_err(|e| WTIsMyCodeError::ConfigError(format!("Failed to write config file: {}", e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a duration string like "24h", "7d", "30m" into seconds.
|
||||||
|
pub fn parse_duration(s: &str) -> Result<u64, String> {
|
||||||
|
let s = s.trim();
|
||||||
|
if s.is_empty() {
|
||||||
|
return Err("empty duration string".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (num_str, suffix) = split_numeric_suffix(s)?;
|
||||||
|
let value: u64 = num_str
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("'{}' is not a valid number", num_str))?;
|
||||||
|
|
||||||
|
match suffix {
|
||||||
|
"s" => Ok(value),
|
||||||
|
"m" => Ok(value * 60),
|
||||||
|
"h" => Ok(value * 3600),
|
||||||
|
"d" => Ok(value * 86400),
|
||||||
|
"w" => Ok(value * 604800),
|
||||||
|
_ => Err(format!("unknown duration suffix '{}'. Use s, m, h, d, or w", suffix)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a file size string like "10MB", "1GB", "500KB" into bytes.
|
||||||
|
pub fn parse_file_size(s: &str) -> Result<u64, String> {
|
||||||
|
let s = s.trim();
|
||||||
|
if s.is_empty() {
|
||||||
|
return Err("empty file size string".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (num_str, suffix) = split_numeric_suffix(s)?;
|
||||||
|
let value: u64 = num_str
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| format!("'{}' is not a valid number", num_str))?;
|
||||||
|
|
||||||
|
let suffix_upper = suffix.to_uppercase();
|
||||||
|
match suffix_upper.as_str() {
|
||||||
|
"B" => Ok(value),
|
||||||
|
"KB" | "K" => Ok(value * 1024),
|
||||||
|
"MB" | "M" => Ok(value * 1024 * 1024),
|
||||||
|
"GB" | "G" => Ok(value * 1024 * 1024 * 1024),
|
||||||
|
_ => Err(format!("unknown size suffix '{}'. Use B, KB, MB, or GB", suffix)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_numeric_suffix(s: &str) -> Result<(&str, &str), String> {
|
||||||
|
let pos = s
|
||||||
|
.find(|c: char| !c.is_ascii_digit())
|
||||||
|
.ok_or_else(|| format!("no unit suffix found in '{}'", s))?;
|
||||||
|
if pos == 0 {
|
||||||
|
return Err(format!("no numeric value found in '{}'", s));
|
||||||
|
}
|
||||||
|
Ok((&s[..pos], &s[pos..]))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_duration() {
|
||||||
|
assert_eq!(parse_duration("24h").unwrap(), 86400);
|
||||||
|
assert_eq!(parse_duration("7d").unwrap(), 604800);
|
||||||
|
assert_eq!(parse_duration("30m").unwrap(), 1800);
|
||||||
|
assert_eq!(parse_duration("60s").unwrap(), 60);
|
||||||
|
assert!(parse_duration("abc").is_err());
|
||||||
|
assert!(parse_duration("").is_err());
|
||||||
|
assert!(parse_duration("10x").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_file_size() {
|
||||||
|
assert_eq!(parse_file_size("10MB").unwrap(), 10 * 1024 * 1024);
|
||||||
|
assert_eq!(parse_file_size("1GB").unwrap(), 1024 * 1024 * 1024);
|
||||||
|
assert_eq!(parse_file_size("500KB").unwrap(), 500 * 1024);
|
||||||
|
assert!(parse_file_size("abc").is_err());
|
||||||
|
assert!(parse_file_size("").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_default_config() {
|
||||||
|
// Default config with "." as root should validate if we're in a valid dir
|
||||||
|
let config = Config::default();
|
||||||
|
// This should work since "." exists and is a directory
|
||||||
|
assert!(config.validate().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_bad_language() {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.project.language = "java".to_string();
|
||||||
|
let err = config.validate().unwrap_err();
|
||||||
|
assert!(err.to_string().contains("not supported"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_empty_include() {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.scan.include = vec![];
|
||||||
|
let err = config.validate().unwrap_err();
|
||||||
|
assert!(err.to_string().contains("must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_bad_root() {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.project.root = "/nonexistent/path/xyz".to_string();
|
||||||
|
let err = config.validate().unwrap_err();
|
||||||
|
assert!(err.to_string().contains("does not exist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_bad_cache_age() {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.caching.max_cache_age = "invalid".to_string();
|
||||||
|
let err = config.validate().unwrap_err();
|
||||||
|
assert!(err.to_string().contains("not valid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_bad_file_size() {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.scan.max_file_size = "notasize".to_string();
|
||||||
|
let err = config.validate().unwrap_err();
|
||||||
|
assert!(err.to_string().contains("not valid"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
183
wtismycode-core/src/cycle_detector.rs
Normal file
183
wtismycode-core/src/cycle_detector.rs
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
//! Dependency cycle detection for module graphs.
|
||||||
|
//!
|
||||||
|
//! Uses DFS-based cycle detection to find circular dependencies
|
||||||
|
//! in the module dependency graph.
|
||||||
|
|
||||||
|
use crate::model::ProjectModel;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
/// Detect cycles in the module dependency graph.
|
||||||
|
///
|
||||||
|
/// Returns a list of cycles, where each cycle is a list of module IDs
|
||||||
|
/// forming a circular dependency chain.
|
||||||
|
pub fn detect_cycles(model: &ProjectModel) -> Vec<Vec<String>> {
|
||||||
|
let mut visited = HashSet::new();
|
||||||
|
let mut rec_stack = HashSet::new();
|
||||||
|
let mut path = Vec::new();
|
||||||
|
let mut cycles = Vec::new();
|
||||||
|
|
||||||
|
// Build adjacency list from model
|
||||||
|
let adj = build_adjacency_list(model);
|
||||||
|
|
||||||
|
for module_id in model.modules.keys() {
|
||||||
|
if !visited.contains(module_id.as_str()) {
|
||||||
|
dfs(
|
||||||
|
module_id,
|
||||||
|
&adj,
|
||||||
|
&mut visited,
|
||||||
|
&mut rec_stack,
|
||||||
|
&mut path,
|
||||||
|
&mut cycles,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate cycles (normalize by rotating to smallest element first)
|
||||||
|
deduplicate_cycles(cycles)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_adjacency_list(model: &ProjectModel) -> HashMap<String, Vec<String>> {
|
||||||
|
let mut adj: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
|
|
||||||
|
for (module_id, module) in &model.modules {
|
||||||
|
let neighbors: Vec<String> = module
|
||||||
|
.outbound_modules
|
||||||
|
.iter()
|
||||||
|
.filter(|target| model.modules.contains_key(*target))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
adj.insert(module_id.clone(), neighbors);
|
||||||
|
}
|
||||||
|
|
||||||
|
adj
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dfs(
|
||||||
|
node: &str,
|
||||||
|
adj: &HashMap<String, Vec<String>>,
|
||||||
|
visited: &mut HashSet<String>,
|
||||||
|
rec_stack: &mut HashSet<String>,
|
||||||
|
path: &mut Vec<String>,
|
||||||
|
cycles: &mut Vec<Vec<String>>,
|
||||||
|
) {
|
||||||
|
visited.insert(node.to_string());
|
||||||
|
rec_stack.insert(node.to_string());
|
||||||
|
path.push(node.to_string());
|
||||||
|
|
||||||
|
if let Some(neighbors) = adj.get(node) {
|
||||||
|
for neighbor in neighbors {
|
||||||
|
if !visited.contains(neighbor.as_str()) {
|
||||||
|
dfs(neighbor, adj, visited, rec_stack, path, cycles);
|
||||||
|
} else if rec_stack.contains(neighbor.as_str()) {
|
||||||
|
// Found a cycle: extract it from path
|
||||||
|
if let Some(start_idx) = path.iter().position(|n| n == neighbor) {
|
||||||
|
let cycle: Vec<String> = path[start_idx..].to_vec();
|
||||||
|
cycles.push(cycle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path.pop();
|
||||||
|
rec_stack.remove(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deduplicate_cycles(cycles: Vec<Vec<String>>) -> Vec<Vec<String>> {
|
||||||
|
let mut seen: HashSet<Vec<String>> = HashSet::new();
|
||||||
|
let mut unique = Vec::new();
|
||||||
|
|
||||||
|
for cycle in cycles {
|
||||||
|
if cycle.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Normalize: rotate so the lexicographically smallest element is first
|
||||||
|
let min_idx = cycle
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.min_by_key(|(_, v)| v.as_str())
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let mut normalized = Vec::with_capacity(cycle.len());
|
||||||
|
for i in 0..cycle.len() {
|
||||||
|
normalized.push(cycle[(min_idx + i) % cycle.len()].clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if seen.insert(normalized.clone()) {
|
||||||
|
unique.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unique
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::model::{Edges, Module, ProjectModel};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
fn make_module(id: &str, outbound: Vec<&str>) -> Module {
|
||||||
|
Module {
|
||||||
|
id: id.to_string(),
|
||||||
|
path: format!("{}.py", id),
|
||||||
|
files: vec![],
|
||||||
|
doc_summary: None,
|
||||||
|
outbound_modules: outbound.into_iter().map(String::from).collect(),
|
||||||
|
inbound_modules: vec![],
|
||||||
|
symbols: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_cycles() {
|
||||||
|
let mut model = ProjectModel::new();
|
||||||
|
model.modules.insert("a".into(), make_module("a", vec!["b"]));
|
||||||
|
model.modules.insert("b".into(), make_module("b", vec!["c"]));
|
||||||
|
model.modules.insert("c".into(), make_module("c", vec![]));
|
||||||
|
|
||||||
|
let cycles = detect_cycles(&model);
|
||||||
|
assert!(cycles.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple_cycle() {
|
||||||
|
let mut model = ProjectModel::new();
|
||||||
|
model.modules.insert("a".into(), make_module("a", vec!["b"]));
|
||||||
|
model.modules.insert("b".into(), make_module("b", vec!["a"]));
|
||||||
|
|
||||||
|
let cycles = detect_cycles(&model);
|
||||||
|
assert_eq!(cycles.len(), 1);
|
||||||
|
assert!(cycles[0].contains(&"a".to_string()));
|
||||||
|
assert!(cycles[0].contains(&"b".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_three_node_cycle() {
|
||||||
|
let mut model = ProjectModel::new();
|
||||||
|
model.modules.insert("a".into(), make_module("a", vec!["b"]));
|
||||||
|
model.modules.insert("b".into(), make_module("b", vec!["c"]));
|
||||||
|
model.modules.insert("c".into(), make_module("c", vec!["a"]));
|
||||||
|
|
||||||
|
let cycles = detect_cycles(&model);
|
||||||
|
assert_eq!(cycles.len(), 1);
|
||||||
|
assert_eq!(cycles[0].len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_graph() {
|
||||||
|
let model = ProjectModel::new();
|
||||||
|
let cycles = detect_cycles(&model);
|
||||||
|
assert!(cycles.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_self_cycle() {
|
||||||
|
let mut model = ProjectModel::new();
|
||||||
|
model.modules.insert("a".into(), make_module("a", vec!["a"]));
|
||||||
|
|
||||||
|
let cycles = detect_cycles(&model);
|
||||||
|
assert_eq!(cycles.len(), 1);
|
||||||
|
assert_eq!(cycles[0], vec!["a".to_string()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ArchDocError {
|
pub enum WTIsMyCodeError {
|
||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//! ArchDoc Core Library
|
//! WTIsMyCode Core Library
|
||||||
//!
|
//!
|
||||||
//! This crate provides the core functionality for analyzing Python projects
|
//! This crate provides the core functionality for analyzing Python projects
|
||||||
//! and generating architecture documentation.
|
//! and generating architecture documentation.
|
||||||
@@ -12,9 +12,10 @@ pub mod python_analyzer;
|
|||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
pub mod writer;
|
pub mod writer;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
|
pub mod cycle_detector;
|
||||||
|
|
||||||
// Re-export commonly used types
|
// Re-export commonly used types
|
||||||
pub use errors::ArchDocError;
|
pub use errors::WTIsMyCodeError;
|
||||||
pub use config::Config;
|
pub use config::Config;
|
||||||
pub use model::ProjectModel;
|
pub use model::ProjectModel;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Intermediate Representation (IR) for ArchDoc
|
//! Intermediate Representation (IR) for WTIsMyCode
|
||||||
//!
|
//!
|
||||||
//! This module defines the data structures that represent the analyzed Python project
|
//! This module defines the data structures that represent the analyzed Python project
|
||||||
//! and are used for generating documentation.
|
//! and are used for generating documentation.
|
||||||
@@ -51,6 +51,7 @@ pub struct FileDoc {
|
|||||||
pub outbound_modules: Vec<String>,
|
pub outbound_modules: Vec<String>,
|
||||||
pub inbound_files: Vec<String>,
|
pub inbound_files: Vec<String>,
|
||||||
pub symbols: Vec<String>,
|
pub symbols: Vec<String>,
|
||||||
|
pub file_purpose: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -83,6 +84,10 @@ pub struct IntegrationFlags {
|
|||||||
pub http: bool,
|
pub http: bool,
|
||||||
pub db: bool,
|
pub db: bool,
|
||||||
pub queue: bool,
|
pub queue: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub storage: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ai: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -142,6 +147,7 @@ pub struct ParsedModule {
|
|||||||
pub imports: Vec<Import>,
|
pub imports: Vec<Import>,
|
||||||
pub symbols: Vec<Symbol>,
|
pub symbols: Vec<Symbol>,
|
||||||
pub calls: Vec<Call>,
|
pub calls: Vec<Call>,
|
||||||
|
pub file_docstring: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
//! Python AST analyzer for ArchDoc
|
//! Python AST analyzer for WTIsMyCode
|
||||||
//!
|
//!
|
||||||
//! This module handles parsing Python files using AST and extracting
|
//! This module handles parsing Python files using AST and extracting
|
||||||
//! imports, definitions, and calls.
|
//! imports, definitions, and calls.
|
||||||
|
|
||||||
use crate::model::{ParsedModule, ProjectModel, Import, Call, CallType, Symbol, Module, FileDoc};
|
use crate::model::{ParsedModule, ProjectModel, Import, Call, CallType, Symbol, Module, FileDoc};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::errors::ArchDocError;
|
use crate::errors::WTIsMyCodeError;
|
||||||
use crate::cache::CacheManager;
|
use crate::cache::CacheManager;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -23,17 +23,17 @@ impl PythonAnalyzer {
|
|||||||
Self { config, cache_manager }
|
Self { config, cache_manager }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_module(&self, file_path: &Path) -> Result<ParsedModule, ArchDocError> {
|
pub fn parse_module(&self, file_path: &Path) -> Result<ParsedModule, WTIsMyCodeError> {
|
||||||
// Try to get from cache first
|
// Try to get from cache first
|
||||||
if let Some(cached_module) = self.cache_manager.get_cached_module(file_path)? {
|
if let Some(cached_module) = self.cache_manager.get_cached_module(file_path)? {
|
||||||
return Ok(cached_module);
|
return Ok(cached_module);
|
||||||
}
|
}
|
||||||
|
|
||||||
let code = fs::read_to_string(file_path)
|
let code = fs::read_to_string(file_path)
|
||||||
.map_err(ArchDocError::Io)?;
|
.map_err(WTIsMyCodeError::Io)?;
|
||||||
|
|
||||||
let ast = ast::Suite::parse(&code, file_path.to_str().unwrap_or("<unknown>"))
|
let ast = ast::Suite::parse(&code, file_path.to_str().unwrap_or("<unknown>"))
|
||||||
.map_err(|e| ArchDocError::ParseError {
|
.map_err(|e| WTIsMyCodeError::ParseError {
|
||||||
file: file_path.to_string_lossy().to_string(),
|
file: file_path.to_string_lossy().to_string(),
|
||||||
line: 0,
|
line: 0,
|
||||||
message: format!("Failed to parse: {}", e),
|
message: format!("Failed to parse: {}", e),
|
||||||
@@ -43,6 +43,9 @@ impl PythonAnalyzer {
|
|||||||
let mut symbols = Vec::new();
|
let mut symbols = Vec::new();
|
||||||
let mut calls = Vec::new();
|
let mut calls = Vec::new();
|
||||||
|
|
||||||
|
// Extract file-level docstring (first statement if it's a string expression)
|
||||||
|
let file_docstring = self.extract_docstring(&ast);
|
||||||
|
|
||||||
for stmt in &ast {
|
for stmt in &ast {
|
||||||
self.extract_from_statement(stmt, None, &mut imports, &mut symbols, &mut calls, 0);
|
self.extract_from_statement(stmt, None, &mut imports, &mut symbols, &mut calls, 0);
|
||||||
}
|
}
|
||||||
@@ -53,6 +56,7 @@ impl PythonAnalyzer {
|
|||||||
imports,
|
imports,
|
||||||
symbols,
|
symbols,
|
||||||
calls,
|
calls,
|
||||||
|
file_docstring,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.cache_manager.store_module(file_path, parsed_module.clone())?;
|
self.cache_manager.store_module(file_path, parsed_module.clone())?;
|
||||||
@@ -360,42 +364,61 @@ impl PythonAnalyzer {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detect_integrations(&self, body: &[Stmt], config: &Config) -> crate::model::IntegrationFlags {
|
fn detect_integrations(&self, _body: &[Stmt], _config: &Config) -> crate::model::IntegrationFlags {
|
||||||
|
// Integration detection is now done at module level in resolve_symbols
|
||||||
|
// based on actual imports, not AST body debug strings
|
||||||
|
crate::model::IntegrationFlags {
|
||||||
|
http: false,
|
||||||
|
db: false,
|
||||||
|
queue: false,
|
||||||
|
storage: false,
|
||||||
|
ai: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect integrations for a module based on its actual imports
|
||||||
|
fn detect_module_integrations(&self, imports: &[Import], config: &Config) -> crate::model::IntegrationFlags {
|
||||||
let mut flags = crate::model::IntegrationFlags {
|
let mut flags = crate::model::IntegrationFlags {
|
||||||
http: false,
|
http: false,
|
||||||
db: false,
|
db: false,
|
||||||
queue: false,
|
queue: false,
|
||||||
|
storage: false,
|
||||||
|
ai: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if !config.analysis.detect_integrations {
|
if !config.analysis.detect_integrations {
|
||||||
return flags;
|
return flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
let body_str = format!("{:?}", body);
|
// Build a set of all import names (both module names and their parts)
|
||||||
|
let import_names: Vec<String> = imports.iter().flat_map(|imp| {
|
||||||
|
let mut names = vec![imp.module_name.clone()];
|
||||||
|
// Also add individual parts: "from minio import Minio" -> module_name is "minio.Minio"
|
||||||
|
for part in imp.module_name.split('.') {
|
||||||
|
names.push(part.to_lowercase());
|
||||||
|
}
|
||||||
|
names
|
||||||
|
}).collect();
|
||||||
|
|
||||||
for pattern in &config.analysis.integration_patterns {
|
for pattern in &config.analysis.integration_patterns {
|
||||||
if pattern.type_ == "http" {
|
|
||||||
for lib in &pattern.patterns {
|
for lib in &pattern.patterns {
|
||||||
if body_str.contains(lib) {
|
let lib_lower = lib.to_lowercase();
|
||||||
flags.http = true;
|
let matched = import_names.iter().any(|name| {
|
||||||
|
let name_lower = name.to_lowercase();
|
||||||
|
name_lower.contains(&lib_lower)
|
||||||
|
});
|
||||||
|
if matched {
|
||||||
|
match pattern.type_.as_str() {
|
||||||
|
"http" => flags.http = true,
|
||||||
|
"db" => flags.db = true,
|
||||||
|
"queue" => flags.queue = true,
|
||||||
|
"storage" => flags.storage = true,
|
||||||
|
"ai" => flags.ai = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if pattern.type_ == "db" {
|
|
||||||
for lib in &pattern.patterns {
|
|
||||||
if body_str.contains(lib) {
|
|
||||||
flags.db = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if pattern.type_ == "queue" {
|
|
||||||
for lib in &pattern.patterns {
|
|
||||||
if body_str.contains(lib) {
|
|
||||||
flags.queue = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flags
|
flags
|
||||||
@@ -566,7 +589,7 @@ impl PythonAnalyzer {
|
|||||||
normalized.to_string()
|
normalized.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_symbols(&self, modules: &[ParsedModule]) -> Result<ProjectModel, ArchDocError> {
|
pub fn resolve_symbols(&self, modules: &[ParsedModule]) -> Result<ProjectModel, WTIsMyCodeError> {
|
||||||
let mut project_model = ProjectModel::new();
|
let mut project_model = ProjectModel::new();
|
||||||
|
|
||||||
// Build import alias map for call resolution
|
// Build import alias map for call resolution
|
||||||
@@ -580,10 +603,25 @@ impl PythonAnalyzer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First pass: collect __init__.py docstrings keyed by module_id
|
||||||
|
let mut init_docstrings: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
||||||
|
for parsed_module in modules {
|
||||||
|
if parsed_module.path.file_name().map(|f| f == "__init__.py").unwrap_or(false)
|
||||||
|
&& let Some(ref ds) = parsed_module.file_docstring {
|
||||||
|
let module_id = self.compute_module_path(&parsed_module.path);
|
||||||
|
init_docstrings.insert(module_id, ds.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for parsed_module in modules {
|
for parsed_module in modules {
|
||||||
let module_id = self.compute_module_path(&parsed_module.path);
|
let module_id = self.compute_module_path(&parsed_module.path);
|
||||||
let file_id = parsed_module.path.to_string_lossy().to_string();
|
let file_id = parsed_module.path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
// Use file docstring first line as file purpose
|
||||||
|
let file_purpose = parsed_module.file_docstring.as_ref().map(|ds| {
|
||||||
|
ds.lines().next().unwrap_or(ds).to_string()
|
||||||
|
});
|
||||||
|
|
||||||
let file_doc = FileDoc {
|
let file_doc = FileDoc {
|
||||||
id: file_id.clone(),
|
id: file_id.clone(),
|
||||||
path: parsed_module.path.to_string_lossy().to_string(),
|
path: parsed_module.path.to_string_lossy().to_string(),
|
||||||
@@ -591,24 +629,48 @@ impl PythonAnalyzer {
|
|||||||
imports: parsed_module.imports.iter().map(|i| i.module_name.clone()).collect(),
|
imports: parsed_module.imports.iter().map(|i| i.module_name.clone()).collect(),
|
||||||
outbound_modules: Vec::new(),
|
outbound_modules: Vec::new(),
|
||||||
inbound_files: Vec::new(),
|
inbound_files: Vec::new(),
|
||||||
symbols: parsed_module.symbols.iter().map(|s| s.id.clone()).collect(),
|
symbols: parsed_module.symbols.iter().map(|s| format!("{}::{}", module_id, s.id)).collect(),
|
||||||
|
file_purpose,
|
||||||
};
|
};
|
||||||
project_model.files.insert(file_id.clone(), file_doc);
|
project_model.files.insert(file_id.clone(), file_doc);
|
||||||
|
|
||||||
|
// Detect integrations based on actual imports
|
||||||
|
let module_integrations = self.detect_module_integrations(&parsed_module.imports, &self.config);
|
||||||
|
let mut module_symbol_ids = Vec::new();
|
||||||
for mut symbol in parsed_module.symbols.clone() {
|
for mut symbol in parsed_module.symbols.clone() {
|
||||||
symbol.module_id = module_id.clone();
|
symbol.module_id = module_id.clone();
|
||||||
symbol.file_id = file_id.clone();
|
symbol.file_id = file_id.clone();
|
||||||
project_model.symbols.insert(symbol.id.clone(), symbol);
|
// Make symbol ID unique by prefixing with module
|
||||||
|
let unique_id = format!("{}::{}", module_id, symbol.id);
|
||||||
|
symbol.id = unique_id.clone();
|
||||||
|
// Apply module-level integration flags to all symbols
|
||||||
|
symbol.integrations_flags.http |= module_integrations.http;
|
||||||
|
symbol.integrations_flags.db |= module_integrations.db;
|
||||||
|
symbol.integrations_flags.queue |= module_integrations.queue;
|
||||||
|
symbol.integrations_flags.storage |= module_integrations.storage;
|
||||||
|
symbol.integrations_flags.ai |= module_integrations.ai;
|
||||||
|
module_symbol_ids.push(unique_id.clone());
|
||||||
|
project_model.symbols.insert(unique_id, symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use __init__.py docstring for module doc_summary, or file docstring for single-file modules
|
||||||
|
let is_init = parsed_module.path.file_name().map(|f| f == "__init__.py").unwrap_or(false);
|
||||||
|
let doc_summary = if is_init {
|
||||||
|
parsed_module.file_docstring.clone()
|
||||||
|
} else {
|
||||||
|
// For non-init files, use file docstring first, then check __init__.py
|
||||||
|
parsed_module.file_docstring.clone()
|
||||||
|
.or_else(|| init_docstrings.get(&module_id).cloned())
|
||||||
|
};
|
||||||
|
|
||||||
let module = Module {
|
let module = Module {
|
||||||
id: module_id.clone(),
|
id: module_id.clone(),
|
||||||
path: parsed_module.path.to_string_lossy().to_string(),
|
path: parsed_module.path.to_string_lossy().to_string(),
|
||||||
files: vec![file_id.clone()],
|
files: vec![file_id.clone()],
|
||||||
doc_summary: None,
|
doc_summary,
|
||||||
outbound_modules: Vec::new(),
|
outbound_modules: Vec::new(),
|
||||||
inbound_modules: Vec::new(),
|
inbound_modules: Vec::new(),
|
||||||
symbols: parsed_module.symbols.iter().map(|s| s.id.clone()).collect(),
|
symbols: module_symbol_ids,
|
||||||
};
|
};
|
||||||
project_model.modules.insert(module_id, module);
|
project_model.modules.insert(module_id, module);
|
||||||
}
|
}
|
||||||
@@ -667,7 +729,10 @@ impl PythonAnalyzer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_dependency_graphs(&self, project_model: &mut ProjectModel, parsed_modules: &[ParsedModule]) -> Result<(), ArchDocError> {
|
fn build_dependency_graphs(&self, project_model: &mut ProjectModel, parsed_modules: &[ParsedModule]) -> Result<(), WTIsMyCodeError> {
|
||||||
|
// Collect known internal module IDs
|
||||||
|
let known_modules: std::collections::HashSet<String> = project_model.modules.keys().cloned().collect();
|
||||||
|
|
||||||
for parsed_module in parsed_modules {
|
for parsed_module in parsed_modules {
|
||||||
let from_module_id = self.compute_module_path(&parsed_module.path);
|
let from_module_id = self.compute_module_path(&parsed_module.path);
|
||||||
|
|
||||||
@@ -683,6 +748,41 @@ impl PythonAnalyzer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Populate outbound_modules and inbound_modules from edges
|
||||||
|
// Only include internal modules (ones that exist in project_model.modules)
|
||||||
|
for edge in &project_model.edges.module_import_edges {
|
||||||
|
let from_id = &edge.from_id;
|
||||||
|
// Try to match the import to an internal module
|
||||||
|
// Import "src.core.SomeClass" should match module "src.core"
|
||||||
|
let to_internal = if known_modules.contains(&edge.to_id) {
|
||||||
|
Some(edge.to_id.clone())
|
||||||
|
} else {
|
||||||
|
// Try prefix matching: "foo.bar.baz" -> check "foo.bar", "foo"
|
||||||
|
let parts: Vec<&str> = edge.to_id.split('.').collect();
|
||||||
|
let mut found = None;
|
||||||
|
for i in (1..parts.len()).rev() {
|
||||||
|
let prefix = parts[..i].join(".");
|
||||||
|
if known_modules.contains(&prefix) {
|
||||||
|
found = Some(prefix);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ref target_module) = to_internal
|
||||||
|
&& target_module != from_id {
|
||||||
|
if let Some(module) = project_model.modules.get_mut(from_id)
|
||||||
|
&& !module.outbound_modules.contains(target_module) {
|
||||||
|
module.outbound_modules.push(target_module.clone());
|
||||||
|
}
|
||||||
|
if let Some(module) = project_model.modules.get_mut(target_module)
|
||||||
|
&& !module.inbound_modules.contains(from_id) {
|
||||||
|
module.inbound_modules.push(from_id.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for parsed_module in parsed_modules {
|
for parsed_module in parsed_modules {
|
||||||
for call in &parsed_module.calls {
|
for call in &parsed_module.calls {
|
||||||
let callee_expr = call.callee_expr.clone();
|
let callee_expr = call.callee_expr.clone();
|
||||||
@@ -699,7 +799,26 @@ impl PythonAnalyzer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_metrics(&self, project_model: &mut ProjectModel) -> Result<(), ArchDocError> {
|
/// Check if a class symbol is a simple data container (dataclass-like).
|
||||||
|
/// A class is considered a dataclass if it has ≤2 methods (typically __init__ and __repr__/__str__).
|
||||||
|
fn is_dataclass_like(symbol_id: &str, project_model: &ProjectModel) -> bool {
|
||||||
|
let symbol = match project_model.symbols.get(symbol_id) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
if symbol.kind != crate::model::SymbolKind::Class {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Count methods belonging to this class
|
||||||
|
let class_name = &symbol.qualname;
|
||||||
|
let method_prefix = format!("{}::{}.", symbol.module_id, class_name);
|
||||||
|
let method_count = project_model.symbols.values()
|
||||||
|
.filter(|s| s.kind == crate::model::SymbolKind::Method && s.id.starts_with(&method_prefix))
|
||||||
|
.count();
|
||||||
|
method_count <= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_metrics(&self, project_model: &mut ProjectModel) -> Result<(), WTIsMyCodeError> {
|
||||||
// Collect fan-in/fan-out first to avoid borrow issues
|
// Collect fan-in/fan-out first to avoid borrow issues
|
||||||
let mut metrics: std::collections::HashMap<String, (usize, usize)> = std::collections::HashMap::new();
|
let mut metrics: std::collections::HashMap<String, (usize, usize)> = std::collections::HashMap::new();
|
||||||
|
|
||||||
@@ -715,11 +834,20 @@ impl PythonAnalyzer {
|
|||||||
metrics.insert(symbol_id.clone(), (fan_in, fan_out));
|
metrics.insert(symbol_id.clone(), (fan_in, fan_out));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-compute which symbols are dataclass-like (need immutable borrow)
|
||||||
|
let dataclass_ids: std::collections::HashSet<String> = metrics.keys()
|
||||||
|
.filter(|id| Self::is_dataclass_like(id, project_model))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
for (symbol_id, (fan_in, fan_out)) in &metrics {
|
for (symbol_id, (fan_in, fan_out)) in &metrics {
|
||||||
if let Some(symbol) = project_model.symbols.get_mut(symbol_id) {
|
if let Some(symbol) = project_model.symbols.get_mut(symbol_id) {
|
||||||
symbol.metrics.fan_in = *fan_in;
|
symbol.metrics.fan_in = *fan_in;
|
||||||
symbol.metrics.fan_out = *fan_out;
|
symbol.metrics.fan_out = *fan_out;
|
||||||
symbol.metrics.is_critical = *fan_in > 10 || *fan_out > 10;
|
// Don't mark dataclass-like classes as critical — they're just data containers
|
||||||
|
let exceeds_threshold = *fan_in > self.config.thresholds.critical_fan_in
|
||||||
|
|| *fan_out > self.config.thresholds.critical_fan_out;
|
||||||
|
symbol.metrics.is_critical = exceeds_threshold && !dataclass_ids.contains(symbol_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
//! Markdown renderer for ArchDoc
|
//! Markdown renderer for WTIsMyCode
|
||||||
//!
|
//!
|
||||||
//! This module handles generating Markdown documentation from the project model
|
//! This module handles generating Markdown documentation from the project model
|
||||||
//! using templates.
|
//! using templates.
|
||||||
|
|
||||||
use crate::model::ProjectModel;
|
use crate::config::Config;
|
||||||
|
use crate::cycle_detector;
|
||||||
|
use crate::model::{ProjectModel, SymbolKind};
|
||||||
|
use chrono::Utc;
|
||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
|
|
||||||
fn sanitize_for_link(filename: &str) -> String {
|
fn sanitize_for_link(filename: &str) -> String {
|
||||||
filename
|
let cleaned = filename.strip_prefix("./").unwrap_or(filename);
|
||||||
.chars()
|
cleaned.replace('/', "__")
|
||||||
.map(|c| match c {
|
|
||||||
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
|
|
||||||
c => c,
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
@@ -67,7 +65,7 @@ impl Renderer {
|
|||||||
## Document metadata
|
## Document metadata
|
||||||
- **Created:** {{{created_date}}}
|
- **Created:** {{{created_date}}}
|
||||||
- **Updated:** {{{updated_date}}}
|
- **Updated:** {{{updated_date}}}
|
||||||
- **Generated by:** archdoc (cli) v0.1
|
- **Generated by:** wtismycode (cli) v0.1
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -89,6 +87,16 @@ impl Renderer {
|
|||||||
{{#each queue_integrations}}
|
{{#each queue_integrations}}
|
||||||
- {{{this}}}
|
- {{{this}}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
|
### Storage Integrations
|
||||||
|
{{#each storage_integrations}}
|
||||||
|
- {{{this}}}
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
### AI/ML Integrations
|
||||||
|
{{#each ai_integrations}}
|
||||||
|
- {{{this}}}
|
||||||
|
{{/each}}
|
||||||
<!-- ARCHDOC:END section=integrations -->
|
<!-- ARCHDOC:END section=integrations -->
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -224,6 +232,20 @@ impl Renderer {
|
|||||||
{{/each}}
|
{{/each}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if has_storage_integrations}}
|
||||||
|
### Storage Integrations
|
||||||
|
{{#each storage_symbols}}
|
||||||
|
- {{{this}}}
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if has_ai_integrations}}
|
||||||
|
### AI/ML Integrations
|
||||||
|
{{#each ai_symbols}}
|
||||||
|
- {{{this}}}
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
{{#each usage_examples}}
|
{{#each usage_examples}}
|
||||||
@@ -235,11 +257,13 @@ impl Renderer {
|
|||||||
"#
|
"#
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_architecture_md(&self, model: &ProjectModel) -> Result<String, anyhow::Error> {
|
pub fn render_architecture_md(&self, model: &ProjectModel, config: Option<&Config>) -> Result<String, anyhow::Error> {
|
||||||
// Collect integration information
|
// Collect integration information
|
||||||
let mut db_integrations = Vec::new();
|
let mut db_integrations = Vec::new();
|
||||||
let mut http_integrations = Vec::new();
|
let mut http_integrations = Vec::new();
|
||||||
let mut queue_integrations = Vec::new();
|
let mut queue_integrations = Vec::new();
|
||||||
|
let mut storage_integrations = Vec::new();
|
||||||
|
let mut ai_integrations = Vec::new();
|
||||||
|
|
||||||
for (symbol_id, symbol) in &model.symbols {
|
for (symbol_id, symbol) in &model.symbols {
|
||||||
if symbol.integrations_flags.db {
|
if symbol.integrations_flags.db {
|
||||||
@@ -251,21 +275,161 @@ impl Renderer {
|
|||||||
if symbol.integrations_flags.queue {
|
if symbol.integrations_flags.queue {
|
||||||
queue_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
queue_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||||
}
|
}
|
||||||
|
if symbol.integrations_flags.storage {
|
||||||
|
storage_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||||
}
|
}
|
||||||
|
if symbol.integrations_flags.ai {
|
||||||
|
ai_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine project name: config > pyproject.toml > directory name > fallback
|
||||||
|
let project_name = config
|
||||||
|
.and_then(|c| {
|
||||||
|
if c.project.name.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(c.project.name.clone())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
// Try pyproject.toml
|
||||||
|
config.and_then(|c| {
|
||||||
|
let pyproject_path = std::path::Path::new(&c.project.root).join("pyproject.toml");
|
||||||
|
std::fs::read_to_string(&pyproject_path).ok().and_then(|content| {
|
||||||
|
// Simple TOML parsing for [project] name = "..."
|
||||||
|
let mut in_project = false;
|
||||||
|
for line in content.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed == "[project]" {
|
||||||
|
in_project = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if trimmed.starts_with('[') {
|
||||||
|
in_project = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if in_project && trimmed.starts_with("name") {
|
||||||
|
if let Some(val) = trimmed.split('=').nth(1) {
|
||||||
|
let name = val.trim().trim_matches('"').trim_matches('\'');
|
||||||
|
if !name.is_empty() {
|
||||||
|
return Some(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
config.map(|c| {
|
||||||
|
std::path::Path::new(&c.project.root)
|
||||||
|
.canonicalize()
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
|
||||||
|
.unwrap_or_else(|| "Project".to_string())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "Project".to_string());
|
||||||
|
|
||||||
|
let today = Utc::now().format("%Y-%m-%d").to_string();
|
||||||
|
|
||||||
|
// Collect layout items grouped by top-level directory
|
||||||
|
let mut dir_files: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new();
|
||||||
|
for file_doc in model.files.values() {
|
||||||
|
let path = file_doc.path.strip_prefix("./").unwrap_or(&file_doc.path);
|
||||||
|
let top_dir = path.split('/').next().unwrap_or(path);
|
||||||
|
// If file is at root level (no '/'), use the filename itself
|
||||||
|
let top = if path.contains('/') {
|
||||||
|
format!("{}/", top_dir)
|
||||||
|
} else {
|
||||||
|
path.to_string()
|
||||||
|
};
|
||||||
|
dir_files.entry(top).or_default().push(path.to_string());
|
||||||
|
}
|
||||||
|
let mut layout_items = Vec::new();
|
||||||
|
for (dir, files) in &dir_files {
|
||||||
|
let file_count = files.len();
|
||||||
|
let purpose = if dir.ends_with('/') {
|
||||||
|
format!("{} files", file_count)
|
||||||
|
} else {
|
||||||
|
"Root file".to_string()
|
||||||
|
};
|
||||||
|
layout_items.push(serde_json::json!({
|
||||||
|
"path": dir,
|
||||||
|
"purpose": purpose,
|
||||||
|
"link": format!("docs/architecture/files/{}.md", sanitize_for_link(dir.trim_end_matches('/')))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect module items for template
|
||||||
|
let mut modules_list = Vec::new();
|
||||||
|
for (module_id, module) in &model.modules {
|
||||||
|
modules_list.push(serde_json::json!({
|
||||||
|
"name": module_id,
|
||||||
|
"symbol_count": module.symbols.len(),
|
||||||
|
"inbound_count": module.inbound_modules.len(),
|
||||||
|
"outbound_count": module.outbound_modules.len(),
|
||||||
|
"link": format!("docs/architecture/modules/{}.md", sanitize_for_link(module_id))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect critical points
|
||||||
|
let mut high_fan_in = Vec::new();
|
||||||
|
let mut high_fan_out = Vec::new();
|
||||||
|
for (symbol_id, symbol) in &model.symbols {
|
||||||
|
if symbol.metrics.fan_in > 5 {
|
||||||
|
high_fan_in.push(serde_json::json!({
|
||||||
|
"symbol": symbol_id,
|
||||||
|
"count": symbol.metrics.fan_in,
|
||||||
|
"critical": symbol.metrics.is_critical,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if symbol.metrics.fan_out > 5 {
|
||||||
|
high_fan_out.push(serde_json::json!({
|
||||||
|
"symbol": symbol_id,
|
||||||
|
"count": symbol.metrics.fan_out,
|
||||||
|
"critical": symbol.metrics.is_critical,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cycles: Vec<_> = cycle_detector::detect_cycles(model)
|
||||||
|
.iter()
|
||||||
|
.map(|cycle| {
|
||||||
|
serde_json::json!({
|
||||||
|
"cycle_path": format!("{} → {}", cycle.join(" → "), cycle.first().unwrap_or(&String::new()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Project statistics
|
||||||
|
let project_description = format!(
|
||||||
|
"Python project with {} modules, {} files, and {} symbols.",
|
||||||
|
model.modules.len(), model.files.len(), model.symbols.len()
|
||||||
|
);
|
||||||
|
|
||||||
// Prepare data for template
|
// Prepare data for template
|
||||||
let data = serde_json::json!({
|
let data = serde_json::json!({
|
||||||
"project_name": "New Project",
|
"project_name": project_name,
|
||||||
"project_description": "<FILL_MANUALLY: what this project does in 3–7 lines>",
|
"project_description": project_description,
|
||||||
"created_date": "2026-01-25",
|
"created_date": &today,
|
||||||
"updated_date": "2026-01-25",
|
"updated_date": &today,
|
||||||
"key_decisions": ["<FILL_MANUALLY>"],
|
"key_decisions": ["<FILL_MANUALLY>"],
|
||||||
"non_goals": ["<FILL_MANUALLY>"],
|
"non_goals": ["<FILL_MANUALLY>"],
|
||||||
"change_notes": ["<FILL_MANUALLY>"],
|
"change_notes": ["<FILL_MANUALLY>"],
|
||||||
"db_integrations": db_integrations,
|
"db_integrations": db_integrations,
|
||||||
"http_integrations": http_integrations,
|
"http_integrations": http_integrations,
|
||||||
"queue_integrations": queue_integrations,
|
"queue_integrations": queue_integrations,
|
||||||
// TODO: Fill with more actual data from model
|
"storage_integrations": storage_integrations,
|
||||||
|
"ai_integrations": ai_integrations,
|
||||||
|
"rails_summary": "\n\nNo tooling information available.\n",
|
||||||
|
"layout_items": layout_items,
|
||||||
|
"modules": modules_list,
|
||||||
|
"high_fan_in": high_fan_in,
|
||||||
|
"high_fan_out": high_fan_out,
|
||||||
|
"cycles": cycles,
|
||||||
});
|
});
|
||||||
|
|
||||||
self.templates.render("architecture_md", &data)
|
self.templates.render("architecture_md", &data)
|
||||||
@@ -297,6 +461,8 @@ impl Renderer {
|
|||||||
let mut db_symbols = Vec::new();
|
let mut db_symbols = Vec::new();
|
||||||
let mut http_symbols = Vec::new();
|
let mut http_symbols = Vec::new();
|
||||||
let mut queue_symbols = Vec::new();
|
let mut queue_symbols = Vec::new();
|
||||||
|
let mut storage_symbols = Vec::new();
|
||||||
|
let mut ai_symbols = Vec::new();
|
||||||
|
|
||||||
for symbol_id in &module.symbols {
|
for symbol_id in &module.symbols {
|
||||||
if let Some(symbol) = model.symbols.get(symbol_id) {
|
if let Some(symbol) = model.symbols.get(symbol_id) {
|
||||||
@@ -309,13 +475,83 @@ impl Renderer {
|
|||||||
if symbol.integrations_flags.queue {
|
if symbol.integrations_flags.queue {
|
||||||
queue_symbols.push(symbol.qualname.clone());
|
queue_symbols.push(symbol.qualname.clone());
|
||||||
}
|
}
|
||||||
|
if symbol.integrations_flags.storage {
|
||||||
|
storage_symbols.push(symbol.qualname.clone());
|
||||||
|
}
|
||||||
|
if symbol.integrations_flags.ai {
|
||||||
|
ai_symbols.push(symbol.qualname.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare usage examples (for now, just placeholders)
|
// Generate usage examples from public symbols
|
||||||
let usage_examples = vec![
|
let mut usage_examples = Vec::new();
|
||||||
"// Example usage of module functions\n// TODO: Add real usage examples based on module analysis".to_string()
|
for symbol_id in &module.symbols {
|
||||||
];
|
if let Some(symbol) = model.symbols.get(symbol_id) {
|
||||||
|
let short_name = symbol.qualname.rsplit('.').next().unwrap_or(&symbol.qualname);
|
||||||
|
match symbol.kind {
|
||||||
|
SymbolKind::Function | SymbolKind::AsyncFunction => {
|
||||||
|
// Extract args from signature: "def foo(a, b)" -> "a, b"
|
||||||
|
let args = symbol.signature
|
||||||
|
.find('(')
|
||||||
|
.and_then(|start| symbol.signature.rfind(')').map(|end| (start, end)))
|
||||||
|
.map(|(s, e)| &symbol.signature[s+1..e])
|
||||||
|
.unwrap_or("");
|
||||||
|
let clean_args = args.split(',')
|
||||||
|
.map(|a| a.split(':').next().unwrap_or("").trim())
|
||||||
|
.filter(|a| !a.is_empty() && *a != "self" && *a != "cls")
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
let example_args = if clean_args.is_empty() { String::new() } else {
|
||||||
|
clean_args.split(", ").map(|a| {
|
||||||
|
if a.starts_with('*') { "..." } else { a }
|
||||||
|
}).collect::<Vec<_>>().join(", ")
|
||||||
|
};
|
||||||
|
let prefix = if symbol.kind == SymbolKind::AsyncFunction { "await " } else { "" };
|
||||||
|
usage_examples.push(format!(
|
||||||
|
"from {} import {}\nresult = {}{}({})",
|
||||||
|
module_id, short_name, prefix, short_name, example_args
|
||||||
|
));
|
||||||
|
}
|
||||||
|
SymbolKind::Class => {
|
||||||
|
// Find __init__ method to get constructor args
|
||||||
|
let init_name = format!("{}.__init__", short_name);
|
||||||
|
let init_args = module.symbols.iter()
|
||||||
|
.find_map(|sid| {
|
||||||
|
model.symbols.get(sid).and_then(|s| {
|
||||||
|
if s.qualname == init_name || s.id == init_name {
|
||||||
|
// Extract args from __init__ signature
|
||||||
|
let args = s.signature
|
||||||
|
.find('(')
|
||||||
|
.and_then(|start| s.signature.rfind(')').map(|end| (start, end)))
|
||||||
|
.map(|(st, en)| &s.signature[st+1..en])
|
||||||
|
.unwrap_or("");
|
||||||
|
let clean = args.split(',')
|
||||||
|
.map(|a| a.split(':').next().unwrap_or("").split('=').next().unwrap_or("").trim())
|
||||||
|
.filter(|a| !a.is_empty() && *a != "self" && *a != "cls" && !a.starts_with('*'))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
Some(clean)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
usage_examples.push(format!(
|
||||||
|
"from {} import {}\ninstance = {}({})",
|
||||||
|
module_id, short_name, short_name, init_args
|
||||||
|
));
|
||||||
|
}
|
||||||
|
SymbolKind::Method => {
|
||||||
|
// Skip methods - they're shown via class usage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if usage_examples.is_empty() {
|
||||||
|
usage_examples.push(format!("import {}", module_id));
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare data for template
|
// Prepare data for template
|
||||||
let data = serde_json::json!({
|
let data = serde_json::json!({
|
||||||
@@ -328,9 +564,13 @@ impl Renderer {
|
|||||||
"has_db_integrations": !db_symbols.is_empty(),
|
"has_db_integrations": !db_symbols.is_empty(),
|
||||||
"has_http_integrations": !http_symbols.is_empty(),
|
"has_http_integrations": !http_symbols.is_empty(),
|
||||||
"has_queue_integrations": !queue_symbols.is_empty(),
|
"has_queue_integrations": !queue_symbols.is_empty(),
|
||||||
|
"has_storage_integrations": !storage_symbols.is_empty(),
|
||||||
|
"has_ai_integrations": !ai_symbols.is_empty(),
|
||||||
"db_symbols": db_symbols,
|
"db_symbols": db_symbols,
|
||||||
"http_symbols": http_symbols,
|
"http_symbols": http_symbols,
|
||||||
"queue_symbols": queue_symbols,
|
"queue_symbols": queue_symbols,
|
||||||
|
"storage_symbols": storage_symbols,
|
||||||
|
"ai_symbols": ai_symbols,
|
||||||
"usage_examples": usage_examples,
|
"usage_examples": usage_examples,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -343,6 +583,8 @@ impl Renderer {
|
|||||||
let mut db_integrations = Vec::new();
|
let mut db_integrations = Vec::new();
|
||||||
let mut http_integrations = Vec::new();
|
let mut http_integrations = Vec::new();
|
||||||
let mut queue_integrations = Vec::new();
|
let mut queue_integrations = Vec::new();
|
||||||
|
let mut storage_integrations = Vec::new();
|
||||||
|
let mut ai_integrations = Vec::new();
|
||||||
|
|
||||||
for (symbol_id, symbol) in &model.symbols {
|
for (symbol_id, symbol) in &model.symbols {
|
||||||
if symbol.integrations_flags.db {
|
if symbol.integrations_flags.db {
|
||||||
@@ -354,6 +596,12 @@ impl Renderer {
|
|||||||
if symbol.integrations_flags.queue {
|
if symbol.integrations_flags.queue {
|
||||||
queue_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
queue_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||||
}
|
}
|
||||||
|
if symbol.integrations_flags.storage {
|
||||||
|
storage_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||||
|
}
|
||||||
|
if symbol.integrations_flags.ai {
|
||||||
|
ai_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare data for integrations section
|
// Prepare data for integrations section
|
||||||
@@ -361,6 +609,8 @@ impl Renderer {
|
|||||||
"db_integrations": db_integrations,
|
"db_integrations": db_integrations,
|
||||||
"http_integrations": http_integrations,
|
"http_integrations": http_integrations,
|
||||||
"queue_integrations": queue_integrations,
|
"queue_integrations": queue_integrations,
|
||||||
|
"storage_integrations": storage_integrations,
|
||||||
|
"ai_integrations": ai_integrations,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a smaller template just for the integrations section
|
// Create a smaller template just for the integrations section
|
||||||
@@ -380,6 +630,16 @@ impl Renderer {
|
|||||||
{{#each queue_integrations}}
|
{{#each queue_integrations}}
|
||||||
- {{{this}}}
|
- {{{this}}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
|
### Storage Integrations
|
||||||
|
{{#each storage_integrations}}
|
||||||
|
- {{{this}}}
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
### AI/ML Integrations
|
||||||
|
{{#each ai_integrations}}
|
||||||
|
- {{{this}}}
|
||||||
|
{{/each}}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let mut handlebars = Handlebars::new();
|
let mut handlebars = Handlebars::new();
|
||||||
@@ -396,14 +656,30 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_layout_section(&self, model: &ProjectModel) -> Result<String, anyhow::Error> {
|
pub fn render_layout_section(&self, model: &ProjectModel) -> Result<String, anyhow::Error> {
|
||||||
// Collect layout information from files
|
// Collect layout items grouped by top-level directory
|
||||||
let mut layout_items = Vec::new();
|
let mut dir_files: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new();
|
||||||
|
|
||||||
for file_doc in model.files.values() {
|
for file_doc in model.files.values() {
|
||||||
|
let path = file_doc.path.strip_prefix("./").unwrap_or(&file_doc.path);
|
||||||
|
let top_dir = path.split('/').next().unwrap_or(path);
|
||||||
|
let top = if path.contains('/') {
|
||||||
|
format!("{}/", top_dir)
|
||||||
|
} else {
|
||||||
|
path.to_string()
|
||||||
|
};
|
||||||
|
dir_files.entry(top).or_default().push(path.to_string());
|
||||||
|
}
|
||||||
|
let mut layout_items = Vec::new();
|
||||||
|
for (dir, files) in &dir_files {
|
||||||
|
let file_count = files.len();
|
||||||
|
let purpose = if dir.ends_with('/') {
|
||||||
|
format!("{} files", file_count)
|
||||||
|
} else {
|
||||||
|
"Root file".to_string()
|
||||||
|
};
|
||||||
layout_items.push(serde_json::json!({
|
layout_items.push(serde_json::json!({
|
||||||
"path": file_doc.path,
|
"path": dir,
|
||||||
"purpose": "Source file",
|
"purpose": purpose,
|
||||||
"link": format!("docs/architecture/files/{}.md", sanitize_for_link(&file_doc.path))
|
"link": format!("docs/architecture/files/{}.md", sanitize_for_link(dir.trim_end_matches('/')))
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +769,14 @@ impl Renderer {
|
|||||||
let data = serde_json::json!({
|
let data = serde_json::json!({
|
||||||
"high_fan_in": high_fan_in,
|
"high_fan_in": high_fan_in,
|
||||||
"high_fan_out": high_fan_out,
|
"high_fan_out": high_fan_out,
|
||||||
"cycles": Vec::<String>::new(), // TODO: Implement cycle detection
|
"cycles": cycle_detector::detect_cycles(model)
|
||||||
|
.iter()
|
||||||
|
.map(|cycle| {
|
||||||
|
serde_json::json!({
|
||||||
|
"cycle_path": format!("{} → {}", cycle.join(" → "), cycle.first().unwrap_or(&String::new()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a smaller template just for the critical points section
|
// Create a smaller template just for the critical points section
|
||||||
@@ -515,7 +798,7 @@ impl Renderer {
|
|||||||
|
|
||||||
### Module Cycles
|
### Module Cycles
|
||||||
{{#each cycles}}
|
{{#each cycles}}
|
||||||
- {{{this}}}
|
- {{{cycle_path}}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
@@ -528,14 +811,30 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_layout_md(&self, model: &ProjectModel) -> Result<String, anyhow::Error> {
|
pub fn render_layout_md(&self, model: &ProjectModel) -> Result<String, anyhow::Error> {
|
||||||
// Collect layout information from files
|
// Collect layout items grouped by top-level directory
|
||||||
let mut layout_items = Vec::new();
|
let mut dir_files: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new();
|
||||||
|
|
||||||
for file_doc in model.files.values() {
|
for file_doc in model.files.values() {
|
||||||
|
let path = file_doc.path.strip_prefix("./").unwrap_or(&file_doc.path);
|
||||||
|
let top_dir = path.split('/').next().unwrap_or(path);
|
||||||
|
let top = if path.contains('/') {
|
||||||
|
format!("{}/", top_dir)
|
||||||
|
} else {
|
||||||
|
path.to_string()
|
||||||
|
};
|
||||||
|
dir_files.entry(top).or_default().push(path.to_string());
|
||||||
|
}
|
||||||
|
let mut layout_items = Vec::new();
|
||||||
|
for (dir, files) in &dir_files {
|
||||||
|
let file_count = files.len();
|
||||||
|
let purpose = if dir.ends_with('/') {
|
||||||
|
format!("{} files", file_count)
|
||||||
|
} else {
|
||||||
|
"Root file".to_string()
|
||||||
|
};
|
||||||
layout_items.push(serde_json::json!({
|
layout_items.push(serde_json::json!({
|
||||||
"path": file_doc.path,
|
"path": dir,
|
||||||
"purpose": "Source file",
|
"purpose": purpose,
|
||||||
"link": format!("files/{}.md", sanitize_for_link(&file_doc.path))
|
"link": format!("files/{}.md", sanitize_for_link(dir.trim_end_matches('/')))
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,6 +889,8 @@ impl Renderer {
|
|||||||
"http": symbol.integrations_flags.http,
|
"http": symbol.integrations_flags.http,
|
||||||
"db": symbol.integrations_flags.db,
|
"db": symbol.integrations_flags.db,
|
||||||
"queue": symbol.integrations_flags.queue,
|
"queue": symbol.integrations_flags.queue,
|
||||||
|
"storage": symbol.integrations_flags.storage,
|
||||||
|
"ai": symbol.integrations_flags.ai,
|
||||||
},
|
},
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"fan_in": symbol.metrics.fan_in,
|
"fan_in": symbol.metrics.fan_in,
|
||||||
@@ -632,6 +933,8 @@ impl Renderer {
|
|||||||
- HTTP: {{#if integrations.http}}yes{{else}}no{{/if}}
|
- HTTP: {{#if integrations.http}}yes{{else}}no{{/if}}
|
||||||
- DB: {{#if integrations.db}}yes{{else}}no{{/if}}
|
- DB: {{#if integrations.db}}yes{{else}}no{{/if}}
|
||||||
- Queue/Tasks: {{#if integrations.queue}}yes{{else}}no{{/if}}
|
- Queue/Tasks: {{#if integrations.queue}}yes{{else}}no{{/if}}
|
||||||
|
- Storage: {{#if integrations.storage}}yes{{else}}no{{/if}}
|
||||||
|
- AI/ML: {{#if integrations.ai}}yes{{else}}no{{/if}}
|
||||||
<!-- ARCHDOC:END section=integrations -->
|
<!-- ARCHDOC:END section=integrations -->
|
||||||
|
|
||||||
#### Risk / impact
|
#### Risk / impact
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
//! File scanner for ArchDoc
|
//! File scanner for WTIsMyCode
|
||||||
//!
|
//!
|
||||||
//! This module handles scanning the file system for Python files according to
|
//! This module handles scanning the file system for Python files according to
|
||||||
//! the configuration settings.
|
//! the configuration settings.
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::errors::ArchDocError;
|
use crate::errors::WTIsMyCodeError;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
@@ -17,17 +17,17 @@ impl FileScanner {
|
|||||||
Self { config }
|
Self { config }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scan_python_files(&self, root: &Path) -> Result<Vec<PathBuf>, ArchDocError> {
|
pub fn scan_python_files(&self, root: &Path) -> Result<Vec<PathBuf>, WTIsMyCodeError> {
|
||||||
// Check if root directory exists
|
// Check if root directory exists
|
||||||
if !root.exists() {
|
if !root.exists() {
|
||||||
return Err(ArchDocError::Io(std::io::Error::new(
|
return Err(WTIsMyCodeError::Io(std::io::Error::new(
|
||||||
std::io::ErrorKind::NotFound,
|
std::io::ErrorKind::NotFound,
|
||||||
format!("Root directory does not exist: {}", root.display())
|
format!("Root directory does not exist: {}", root.display())
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !root.is_dir() {
|
if !root.is_dir() {
|
||||||
return Err(ArchDocError::Io(std::io::Error::new(
|
return Err(WTIsMyCodeError::Io(std::io::Error::new(
|
||||||
std::io::ErrorKind::InvalidInput,
|
std::io::ErrorKind::InvalidInput,
|
||||||
format!("Root path is not a directory: {}", root.display())
|
format!("Root path is not a directory: {}", root.display())
|
||||||
)));
|
)));
|
||||||
@@ -41,7 +41,7 @@ impl FileScanner {
|
|||||||
.into_iter() {
|
.into_iter() {
|
||||||
|
|
||||||
let entry = entry.map_err(|e| {
|
let entry = entry.map_err(|e| {
|
||||||
ArchDocError::Io(std::io::Error::other(
|
WTIsMyCodeError::Io(std::io::Error::other(
|
||||||
format!("Failed to read directory entry: {}", e)
|
format!("Failed to read directory entry: {}", e)
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
//! Diff-aware file writer for ArchDoc
|
//! Diff-aware file writer for WTIsMyCode
|
||||||
//!
|
//!
|
||||||
//! This module handles writing generated documentation to files while preserving
|
//! This module handles writing generated documentation to files while preserving
|
||||||
//! manual content and only updating generated sections.
|
//! manual content and only updating generated sections.
|
||||||
|
|
||||||
use crate::errors::ArchDocError;
|
use crate::errors::WTIsMyCodeError;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
@@ -42,17 +42,17 @@ impl DiffAwareWriter {
|
|||||||
file_path: &Path,
|
file_path: &Path,
|
||||||
generated_content: &str,
|
generated_content: &str,
|
||||||
section_name: &str,
|
section_name: &str,
|
||||||
) -> Result<(), ArchDocError> {
|
) -> Result<(), WTIsMyCodeError> {
|
||||||
// Read existing file
|
// Read existing file
|
||||||
let existing_content = if file_path.exists() {
|
let existing_content = if file_path.exists() {
|
||||||
fs::read_to_string(file_path)
|
fs::read_to_string(file_path)
|
||||||
.map_err(ArchDocError::Io)?
|
.map_err(WTIsMyCodeError::Io)?
|
||||||
} else {
|
} else {
|
||||||
// Create new file with template
|
// Create new file with template
|
||||||
let template_content = self.create_template_file(file_path, section_name)?;
|
let template_content = self.create_template_file(file_path, section_name)?;
|
||||||
// Write template to file
|
// Write template to file
|
||||||
fs::write(file_path, &template_content)
|
fs::write(file_path, &template_content)
|
||||||
.map_err(ArchDocError::Io)?;
|
.map_err(WTIsMyCodeError::Io)?;
|
||||||
template_content
|
template_content
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,17 +70,13 @@ impl DiffAwareWriter {
|
|||||||
// Check if content has changed
|
// Check if content has changed
|
||||||
let content_changed = existing_content != new_content;
|
let content_changed = existing_content != new_content;
|
||||||
|
|
||||||
// Write updated content
|
// Only write if content actually changed (optimization)
|
||||||
if content_changed {
|
if content_changed {
|
||||||
let updated_content = self.update_timestamp(new_content)?;
|
let updated_content = self.update_timestamp(new_content)?;
|
||||||
fs::write(file_path, updated_content)
|
fs::write(file_path, updated_content)
|
||||||
.map_err(ArchDocError::Io)?;
|
.map_err(WTIsMyCodeError::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(ArchDocError::Io)?;
|
|
||||||
}
|
}
|
||||||
|
// If not changed, skip writing entirely
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -91,16 +87,16 @@ impl DiffAwareWriter {
|
|||||||
file_path: &Path,
|
file_path: &Path,
|
||||||
symbol_id: &str,
|
symbol_id: &str,
|
||||||
generated_content: &str,
|
generated_content: &str,
|
||||||
) -> Result<(), ArchDocError> {
|
) -> Result<(), WTIsMyCodeError> {
|
||||||
// Read existing file
|
// Read existing file
|
||||||
let existing_content = if file_path.exists() {
|
let existing_content = if file_path.exists() {
|
||||||
fs::read_to_string(file_path)
|
fs::read_to_string(file_path)
|
||||||
.map_err(ArchDocError::Io)?
|
.map_err(WTIsMyCodeError::Io)?
|
||||||
} else {
|
} else {
|
||||||
// If file doesn't exist, create it with a basic template
|
// If file doesn't exist, create it with a basic template
|
||||||
let template_content = self.create_template_file(file_path, "symbol")?;
|
let template_content = self.create_template_file(file_path, "symbol")?;
|
||||||
fs::write(file_path, &template_content)
|
fs::write(file_path, &template_content)
|
||||||
.map_err(ArchDocError::Io)?;
|
.map_err(WTIsMyCodeError::Io)?;
|
||||||
template_content
|
template_content
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,17 +114,13 @@ impl DiffAwareWriter {
|
|||||||
// Check if content has changed
|
// Check if content has changed
|
||||||
let content_changed = existing_content != new_content;
|
let content_changed = existing_content != new_content;
|
||||||
|
|
||||||
// Write updated content
|
// Only write if content actually changed (optimization)
|
||||||
if content_changed {
|
if content_changed {
|
||||||
let updated_content = self.update_timestamp(new_content)?;
|
let updated_content = self.update_timestamp(new_content)?;
|
||||||
fs::write(file_path, updated_content)
|
fs::write(file_path, updated_content)
|
||||||
.map_err(ArchDocError::Io)?;
|
.map_err(WTIsMyCodeError::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(ArchDocError::Io)?;
|
|
||||||
}
|
}
|
||||||
|
// If not changed, skip writing entirely
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Warning: No symbol marker found for {} in {}", symbol_id, file_path.display());
|
eprintln!("Warning: No symbol marker found for {} in {}", symbol_id, file_path.display());
|
||||||
}
|
}
|
||||||
@@ -136,7 +128,7 @@ impl DiffAwareWriter {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_section_markers(&self, content: &str, section_name: &str) -> Result<Vec<SectionMarker>, ArchDocError> {
|
fn find_section_markers(&self, content: &str, section_name: &str) -> Result<Vec<SectionMarker>, WTIsMyCodeError> {
|
||||||
let begin_marker = format!("<!-- ARCHDOC:BEGIN section={} -->", section_name);
|
let begin_marker = format!("<!-- ARCHDOC:BEGIN section={} -->", section_name);
|
||||||
let end_marker = format!("<!-- ARCHDOC:END section={} -->", section_name);
|
let end_marker = format!("<!-- ARCHDOC:END section={} -->", section_name);
|
||||||
|
|
||||||
@@ -163,7 +155,7 @@ impl DiffAwareWriter {
|
|||||||
Ok(markers)
|
Ok(markers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_symbol_markers(&self, content: &str, symbol_id: &str) -> Result<Vec<SymbolMarker>, ArchDocError> {
|
fn find_symbol_markers(&self, content: &str, symbol_id: &str) -> Result<Vec<SymbolMarker>, WTIsMyCodeError> {
|
||||||
let begin_marker = format!("<!-- ARCHDOC:BEGIN symbol id={} -->", symbol_id);
|
let begin_marker = format!("<!-- ARCHDOC:BEGIN symbol id={} -->", symbol_id);
|
||||||
let end_marker = format!("<!-- ARCHDOC:END symbol id={} -->", symbol_id);
|
let end_marker = format!("<!-- ARCHDOC:END symbol id={} -->", symbol_id);
|
||||||
|
|
||||||
@@ -195,7 +187,7 @@ impl DiffAwareWriter {
|
|||||||
content: &str,
|
content: &str,
|
||||||
marker: &SectionMarker,
|
marker: &SectionMarker,
|
||||||
new_content: &str,
|
new_content: &str,
|
||||||
) -> Result<String, ArchDocError> {
|
) -> Result<String, WTIsMyCodeError> {
|
||||||
let before = &content[..marker.start_pos];
|
let before = &content[..marker.start_pos];
|
||||||
let after = &content[marker.end_pos..];
|
let after = &content[marker.end_pos..];
|
||||||
|
|
||||||
@@ -213,7 +205,7 @@ impl DiffAwareWriter {
|
|||||||
content: &str,
|
content: &str,
|
||||||
marker: &SymbolMarker,
|
marker: &SymbolMarker,
|
||||||
new_content: &str,
|
new_content: &str,
|
||||||
) -> Result<String, ArchDocError> {
|
) -> Result<String, WTIsMyCodeError> {
|
||||||
let before = &content[..marker.start_pos];
|
let before = &content[..marker.start_pos];
|
||||||
let after = &content[marker.end_pos..];
|
let after = &content[marker.end_pos..];
|
||||||
|
|
||||||
@@ -226,7 +218,7 @@ impl DiffAwareWriter {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_timestamp(&self, content: String) -> Result<String, ArchDocError> {
|
fn update_timestamp(&self, content: String) -> Result<String, WTIsMyCodeError> {
|
||||||
// Update the "Updated" field in the document metadata section
|
// Update the "Updated" field in the document metadata section
|
||||||
// Find the metadata section and update the timestamp
|
// Find the metadata section and update the timestamp
|
||||||
let today = Utc::now().format("%Y-%m-%d").to_string();
|
let today = Utc::now().format("%Y-%m-%d").to_string();
|
||||||
@@ -246,7 +238,7 @@ impl DiffAwareWriter {
|
|||||||
Ok(updated_lines.join("\n"))
|
Ok(updated_lines.join("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_template_file(&self, _file_path: &Path, template_type: &str) -> Result<String, ArchDocError> {
|
fn create_template_file(&self, _file_path: &Path, template_type: &str) -> Result<String, WTIsMyCodeError> {
|
||||||
// Create file with appropriate template based on type
|
// Create file with appropriate template based on type
|
||||||
match template_type {
|
match template_type {
|
||||||
"architecture" => {
|
"architecture" => {
|
||||||
@@ -269,7 +261,7 @@ impl DiffAwareWriter {
|
|||||||
## Document metadata
|
## Document metadata
|
||||||
- **Created:** <AUTO_ON_INIT: YYYY-MM-DD>
|
- **Created:** <AUTO_ON_INIT: YYYY-MM-DD>
|
||||||
- **Updated:** <AUTO_ON_CHANGE: YYYY-MM-DD>
|
- **Updated:** <AUTO_ON_CHANGE: YYYY-MM-DD>
|
||||||
- **Generated by:** archdoc (cli) v0.1
|
- **Generated by:** wtismycode (cli) v0.1
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
//! Caching tests for ArchDoc
|
//! Caching tests for WTIsMyCode
|
||||||
//!
|
//!
|
||||||
//! These tests verify that the caching functionality works correctly.
|
//! These tests verify that the caching functionality works correctly.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use archdoc_core::{Config, python_analyzer::PythonAnalyzer};
|
use wtismycode_core::{Config, python_analyzer::PythonAnalyzer};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cache_store_and_retrieve() {
|
fn test_cache_store_and_retrieve() {
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
//! Enhanced analysis tests for ArchDoc
|
//! Enhanced analysis tests for WTIsMyCode
|
||||||
//!
|
//!
|
||||||
//! These tests verify that the enhanced analysis functionality works correctly
|
//! These tests verify that the enhanced analysis functionality works correctly
|
||||||
//! with complex code that includes integrations, calls, and docstrings.
|
//! with complex code that includes integrations, calls, and docstrings.
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use archdoc_core::{Config, scanner::FileScanner, python_analyzer::PythonAnalyzer};
|
use wtismycode_core::{Config, scanner::FileScanner, python_analyzer::PythonAnalyzer};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_enhanced_analysis_with_integrations() {
|
fn test_enhanced_analysis_with_integrations() {
|
||||||
@@ -15,8 +15,8 @@ fn test_enhanced_analysis_with_integrations() {
|
|||||||
|
|
||||||
// Try different paths for the config file
|
// Try different paths for the config file
|
||||||
let possible_paths = [
|
let possible_paths = [
|
||||||
"tests/golden/test_project/archdoc.toml",
|
"tests/golden/test_project/wtismycode.toml",
|
||||||
"../tests/golden/test_project/archdoc.toml",
|
"../tests/golden/test_project/wtismycode.toml",
|
||||||
];
|
];
|
||||||
|
|
||||||
let config_path = possible_paths.iter().find(|&path| {
|
let config_path = possible_paths.iter().find(|&path| {
|
||||||
@@ -98,19 +98,19 @@ fn test_enhanced_analysis_with_integrations() {
|
|||||||
assert!(found_advanced_module);
|
assert!(found_advanced_module);
|
||||||
|
|
||||||
// Check that we found the UserService class with DB integration
|
// Check that we found the UserService class with DB integration
|
||||||
let user_service_symbol = project_model.symbols.values().find(|s| s.id == "UserService");
|
let user_service_symbol = project_model.symbols.values().find(|s| s.id.ends_with("::UserService"));
|
||||||
assert!(user_service_symbol.is_some());
|
assert!(user_service_symbol.is_some());
|
||||||
assert_eq!(user_service_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Class);
|
assert_eq!(user_service_symbol.unwrap().kind, wtismycode_core::model::SymbolKind::Class);
|
||||||
|
|
||||||
// Check that we found the NotificationService class with queue integration
|
// Check that we found the NotificationService class with queue integration
|
||||||
let notification_service_symbol = project_model.symbols.values().find(|s| s.id == "NotificationService");
|
let notification_service_symbol = project_model.symbols.values().find(|s| s.id.ends_with("::NotificationService"));
|
||||||
assert!(notification_service_symbol.is_some());
|
assert!(notification_service_symbol.is_some());
|
||||||
assert_eq!(notification_service_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Class);
|
assert_eq!(notification_service_symbol.unwrap().kind, wtismycode_core::model::SymbolKind::Class);
|
||||||
|
|
||||||
// Check that we found the fetch_external_user_data function with HTTP integration
|
// Check that we found the fetch_external_user_data function with HTTP integration
|
||||||
let fetch_external_user_data_symbol = project_model.symbols.values().find(|s| s.id == "fetch_external_user_data");
|
let fetch_external_user_data_symbol = project_model.symbols.values().find(|s| s.id.ends_with("::fetch_external_user_data"));
|
||||||
assert!(fetch_external_user_data_symbol.is_some());
|
assert!(fetch_external_user_data_symbol.is_some());
|
||||||
assert_eq!(fetch_external_user_data_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Function);
|
assert_eq!(fetch_external_user_data_symbol.unwrap().kind, wtismycode_core::model::SymbolKind::Function);
|
||||||
|
|
||||||
// Check file imports
|
// Check file imports
|
||||||
let mut found_advanced_file = false;
|
let mut found_advanced_file = false;
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
//! Error handling tests for ArchDoc
|
//! Error handling tests for WTIsMyCode
|
||||||
//!
|
//!
|
||||||
//! These tests verify that ArchDoc properly handles various error conditions
|
//! These tests verify that WTIsMyCode properly handles various error conditions
|
||||||
//! and edge cases.
|
//! and edge cases.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use archdoc_core::{Config, scanner::FileScanner, python_analyzer::PythonAnalyzer};
|
use wtismycode_core::{Config, scanner::FileScanner, python_analyzer::PythonAnalyzer};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scanner_nonexistent_directory() {
|
fn test_scanner_nonexistent_directory() {
|
||||||
@@ -19,7 +19,7 @@ fn test_scanner_nonexistent_directory() {
|
|||||||
|
|
||||||
// Check that we get an IO error
|
// Check that we get an IO error
|
||||||
match result.unwrap_err() {
|
match result.unwrap_err() {
|
||||||
archdoc_core::errors::ArchDocError::Io(_) => {},
|
wtismycode_core::errors::WTIsMyCodeError::Io(_) => {},
|
||||||
_ => panic!("Expected IO error"),
|
_ => panic!("Expected IO error"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ fn test_scanner_file_instead_of_directory() {
|
|||||||
|
|
||||||
// Check that we get an IO error
|
// Check that we get an IO error
|
||||||
match result.unwrap_err() {
|
match result.unwrap_err() {
|
||||||
archdoc_core::errors::ArchDocError::Io(_) => {},
|
wtismycode_core::errors::WTIsMyCodeError::Io(_) => {},
|
||||||
_ => panic!("Expected IO error"),
|
_ => panic!("Expected IO error"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ fn test_analyzer_nonexistent_file() {
|
|||||||
|
|
||||||
// Check that we get an IO error
|
// Check that we get an IO error
|
||||||
match result.unwrap_err() {
|
match result.unwrap_err() {
|
||||||
archdoc_core::errors::ArchDocError::Io(_) => {},
|
wtismycode_core::errors::WTIsMyCodeError::Io(_) => {},
|
||||||
_ => panic!("Expected IO error"),
|
_ => panic!("Expected IO error"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ fn test_analyzer_invalid_python_syntax() {
|
|||||||
|
|
||||||
// Check that we get a parse error
|
// Check that we get a parse error
|
||||||
match result.unwrap_err() {
|
match result.unwrap_err() {
|
||||||
archdoc_core::errors::ArchDocError::ParseError { .. } => {},
|
wtismycode_core::errors::WTIsMyCodeError::ParseError { .. } => {},
|
||||||
_ => panic!("Expected parse error"),
|
_ => panic!("Expected parse error"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
157
wtismycode-core/tests/full_pipeline.rs
Normal file
157
wtismycode-core/tests/full_pipeline.rs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
//! Full pipeline integration tests for WTIsMyCode
|
||||||
|
//!
|
||||||
|
//! Tests the complete scan → analyze → render pipeline using test-project/.
|
||||||
|
|
||||||
|
use wtismycode_core::config::Config;
|
||||||
|
use wtismycode_core::cycle_detector;
|
||||||
|
use wtismycode_core::model::{Module, ProjectModel};
|
||||||
|
use wtismycode_core::renderer::Renderer;
|
||||||
|
use wtismycode_core::scanner::FileScanner;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_load_and_validate() {
|
||||||
|
let config_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.join("test-project/wtismycode.toml");
|
||||||
|
|
||||||
|
let config = Config::load_from_file(&config_path).expect("Failed to load config");
|
||||||
|
assert_eq!(config.project.language, "python");
|
||||||
|
assert!(!config.scan.include.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_validate_on_test_project() {
|
||||||
|
let config_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.join("test-project/wtismycode.toml");
|
||||||
|
|
||||||
|
let mut config = Config::load_from_file(&config_path).expect("Failed to load config");
|
||||||
|
// Set root to actual test-project path so validation passes
|
||||||
|
config.project.root = config_path.parent().unwrap().to_string_lossy().to_string();
|
||||||
|
assert!(config.validate().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_validate_rejects_bad_language() {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.project.language = "java".to_string();
|
||||||
|
assert!(config.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_scan_test_project() {
|
||||||
|
let test_project = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.join("test-project");
|
||||||
|
|
||||||
|
let config_path = test_project.join("wtismycode.toml");
|
||||||
|
let mut config = Config::load_from_file(&config_path).expect("Failed to load config");
|
||||||
|
config.project.root = test_project.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let scanner = FileScanner::new(config);
|
||||||
|
let files = scanner.scan_python_files(&test_project).expect("Scan should succeed");
|
||||||
|
assert!(!files.is_empty(), "Should find Python files in test-project");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cycle_detection_with_known_cycles() {
|
||||||
|
let mut model = ProjectModel::new();
|
||||||
|
|
||||||
|
// Create a known cycle: a → b → c → a
|
||||||
|
model.modules.insert(
|
||||||
|
"mod_a".into(),
|
||||||
|
Module {
|
||||||
|
id: "mod_a".into(),
|
||||||
|
path: "a.py".into(),
|
||||||
|
files: vec![],
|
||||||
|
doc_summary: None,
|
||||||
|
outbound_modules: vec!["mod_b".into()],
|
||||||
|
inbound_modules: vec!["mod_c".into()],
|
||||||
|
symbols: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
model.modules.insert(
|
||||||
|
"mod_b".into(),
|
||||||
|
Module {
|
||||||
|
id: "mod_b".into(),
|
||||||
|
path: "b.py".into(),
|
||||||
|
files: vec![],
|
||||||
|
doc_summary: None,
|
||||||
|
outbound_modules: vec!["mod_c".into()],
|
||||||
|
inbound_modules: vec!["mod_a".into()],
|
||||||
|
symbols: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
model.modules.insert(
|
||||||
|
"mod_c".into(),
|
||||||
|
Module {
|
||||||
|
id: "mod_c".into(),
|
||||||
|
path: "c.py".into(),
|
||||||
|
files: vec![],
|
||||||
|
doc_summary: None,
|
||||||
|
outbound_modules: vec!["mod_a".into()],
|
||||||
|
inbound_modules: vec!["mod_b".into()],
|
||||||
|
symbols: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let cycles = cycle_detector::detect_cycles(&model);
|
||||||
|
assert_eq!(cycles.len(), 1, "Should detect exactly one cycle");
|
||||||
|
assert_eq!(cycles[0].len(), 3, "Cycle should have 3 modules");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cycle_detection_no_cycles() {
|
||||||
|
let mut model = ProjectModel::new();
|
||||||
|
|
||||||
|
model.modules.insert(
|
||||||
|
"mod_a".into(),
|
||||||
|
Module {
|
||||||
|
id: "mod_a".into(),
|
||||||
|
path: "a.py".into(),
|
||||||
|
files: vec![],
|
||||||
|
doc_summary: None,
|
||||||
|
outbound_modules: vec!["mod_b".into()],
|
||||||
|
inbound_modules: vec![],
|
||||||
|
symbols: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
model.modules.insert(
|
||||||
|
"mod_b".into(),
|
||||||
|
Module {
|
||||||
|
id: "mod_b".into(),
|
||||||
|
path: "b.py".into(),
|
||||||
|
files: vec![],
|
||||||
|
doc_summary: None,
|
||||||
|
outbound_modules: vec![],
|
||||||
|
inbound_modules: vec!["mod_a".into()],
|
||||||
|
symbols: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let cycles = cycle_detector::detect_cycles(&model);
|
||||||
|
assert!(cycles.is_empty(), "Should detect no cycles in DAG");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_renderer_produces_output() {
|
||||||
|
let config = Config::default();
|
||||||
|
let model = ProjectModel::new();
|
||||||
|
let renderer = Renderer::new();
|
||||||
|
let result = renderer.render_architecture_md(&model, None);
|
||||||
|
assert!(result.is_ok(), "Renderer should produce output for empty model");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_duration_values() {
|
||||||
|
use wtismycode_core::config::{parse_duration, parse_file_size};
|
||||||
|
|
||||||
|
assert_eq!(parse_duration("24h").unwrap(), 86400);
|
||||||
|
assert_eq!(parse_duration("7d").unwrap(), 604800);
|
||||||
|
assert_eq!(parse_file_size("10MB").unwrap(), 10 * 1024 * 1024);
|
||||||
|
assert_eq!(parse_file_size("1GB").unwrap(), 1024 * 1024 * 1024);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Golden tests for ArchDoc
|
//! Golden tests for WTIsMyCode
|
||||||
//!
|
//!
|
||||||
//! These tests generate documentation for test projects and compare the output
|
//! These tests generate documentation for test projects and compare the output
|
||||||
//! with expected "golden" files to ensure consistency.
|
//! with expected "golden" files to ensure consistency.
|
||||||
@@ -7,7 +7,7 @@ mod test_utils;
|
|||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use archdoc_core::{Config, scanner::FileScanner, python_analyzer::PythonAnalyzer};
|
use wtismycode_core::{Config, scanner::FileScanner, python_analyzer::PythonAnalyzer};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_simple_project_generation() {
|
fn test_simple_project_generation() {
|
||||||
@@ -17,8 +17,8 @@ fn test_simple_project_generation() {
|
|||||||
|
|
||||||
// Try different paths for the config file
|
// Try different paths for the config file
|
||||||
let possible_paths = [
|
let possible_paths = [
|
||||||
"tests/golden/test_project/archdoc.toml",
|
"tests/golden/test_project/wtismycode.toml",
|
||||||
"../tests/golden/test_project/archdoc.toml",
|
"../tests/golden/test_project/wtismycode.toml",
|
||||||
];
|
];
|
||||||
|
|
||||||
let config_path = possible_paths.iter().find(|&path| {
|
let config_path = possible_paths.iter().find(|&path| {
|
||||||
@@ -90,14 +90,14 @@ fn test_simple_project_generation() {
|
|||||||
assert!(found_example_module);
|
assert!(found_example_module);
|
||||||
|
|
||||||
// Check that we found the Calculator class
|
// Check that we found the Calculator class
|
||||||
let calculator_symbol = project_model.symbols.values().find(|s| s.id == "Calculator");
|
let calculator_symbol = project_model.symbols.values().find(|s| s.id.ends_with("::Calculator"));
|
||||||
assert!(calculator_symbol.is_some());
|
assert!(calculator_symbol.is_some());
|
||||||
assert_eq!(calculator_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Class);
|
assert_eq!(calculator_symbol.unwrap().kind, wtismycode_core::model::SymbolKind::Class);
|
||||||
|
|
||||||
// Check that we found the process_numbers function
|
// Check that we found the process_numbers function
|
||||||
let process_numbers_symbol = project_model.symbols.values().find(|s| s.id == "process_numbers");
|
let process_numbers_symbol = project_model.symbols.values().find(|s| s.id.ends_with("::process_numbers"));
|
||||||
assert!(process_numbers_symbol.is_some());
|
assert!(process_numbers_symbol.is_some());
|
||||||
assert_eq!(process_numbers_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Function);
|
assert_eq!(process_numbers_symbol.unwrap().kind, wtismycode_core::model::SymbolKind::Function);
|
||||||
|
|
||||||
// Check file imports
|
// Check file imports
|
||||||
assert!(!project_model.files.is_empty());
|
assert!(!project_model.files.is_empty());
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
## Document metadata
|
## Document metadata
|
||||||
- **Created:** 2026-01-25
|
- **Created:** 2026-01-25
|
||||||
- **Updated:** 2026-01-25
|
- **Updated:** 2026-01-25
|
||||||
- **Generated by:** archdoc (cli) v0.1
|
- **Generated by:** wtismycode (cli) v0.1
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,10 +53,10 @@ description_max_length = 200
|
|||||||
|
|
||||||
[logging]
|
[logging]
|
||||||
level = "info"
|
level = "info"
|
||||||
file = "archdoc.log"
|
file = "wtismycode.log"
|
||||||
format = "compact"
|
format = "compact"
|
||||||
|
|
||||||
[caching]
|
[caching]
|
||||||
enabled = true
|
enabled = true
|
||||||
cache_dir = ".archdoc/cache"
|
cache_dir = ".wtismycode/cache"
|
||||||
max_cache_age = "24h"
|
max_cache_age = "24h"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user