agentmd-gen 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentmd_gen-0.2.0/.github/workflows/ci.yml +27 -0
- agentmd_gen-0.2.0/.gitignore +6 -0
- agentmd_gen-0.2.0/LICENSE +21 -0
- agentmd_gen-0.2.0/PKG-INFO +137 -0
- agentmd_gen-0.2.0/README.md +119 -0
- agentmd_gen-0.2.0/agentmd/__init__.py +4 -0
- agentmd_gen-0.2.0/agentmd/analyzer.py +303 -0
- agentmd_gen-0.2.0/agentmd/cli.py +304 -0
- agentmd_gen-0.2.0/agentmd/detectors/__init__.py +23 -0
- agentmd_gen-0.2.0/agentmd/detectors/ci.py +28 -0
- agentmd_gen-0.2.0/agentmd/detectors/common.py +56 -0
- agentmd_gen-0.2.0/agentmd/detectors/context_completeness.py +325 -0
- agentmd_gen-0.2.0/agentmd/detectors/framework.py +111 -0
- agentmd_gen-0.2.0/agentmd/detectors/go.py +127 -0
- agentmd_gen-0.2.0/agentmd/detectors/language.py +48 -0
- agentmd_gen-0.2.0/agentmd/detectors/lint.py +70 -0
- agentmd_gen-0.2.0/agentmd/detectors/package_manager.py +60 -0
- agentmd_gen-0.2.0/agentmd/detectors/rust.py +123 -0
- agentmd_gen-0.2.0/agentmd/detectors/swift.py +111 -0
- agentmd_gen-0.2.0/agentmd/detectors/test_runner.py +80 -0
- agentmd_gen-0.2.0/agentmd/generators/__init__.py +22 -0
- agentmd_gen-0.2.0/agentmd/generators/base.py +369 -0
- agentmd_gen-0.2.0/agentmd/generators/claude.py +196 -0
- agentmd_gen-0.2.0/agentmd/generators/codex.py +167 -0
- agentmd_gen-0.2.0/agentmd/generators/copilot.py +230 -0
- agentmd_gen-0.2.0/agentmd/generators/cursor.py +229 -0
- agentmd_gen-0.2.0/agentmd/scorer.py +104 -0
- agentmd_gen-0.2.0/agentmd/types.py +97 -0
- agentmd_gen-0.2.0/all-day-build-contract-agentmd-v0.1.md +174 -0
- agentmd_gen-0.2.0/all-day-build-contract-agentmd-v0.2.md +120 -0
- agentmd_gen-0.2.0/progress-log.md +73 -0
- agentmd_gen-0.2.0/pyproject.toml +59 -0
- agentmd_gen-0.2.0/tests/unit/test_analyzer.py +47 -0
- agentmd_gen-0.2.0/tests/unit/test_ci_detector.py +16 -0
- agentmd_gen-0.2.0/tests/unit/test_cli.py +217 -0
- agentmd_gen-0.2.0/tests/unit/test_cli_json.py +267 -0
- agentmd_gen-0.2.0/tests/unit/test_framework_detector.py +35 -0
- agentmd_gen-0.2.0/tests/unit/test_generator_languages.py +569 -0
- agentmd_gen-0.2.0/tests/unit/test_generators.py +301 -0
- agentmd_gen-0.2.0/tests/unit/test_go_detector.py +269 -0
- agentmd_gen-0.2.0/tests/unit/test_language_detector.py +34 -0
- agentmd_gen-0.2.0/tests/unit/test_lint_detector.py +26 -0
- agentmd_gen-0.2.0/tests/unit/test_package_manager_detector.py +27 -0
- agentmd_gen-0.2.0/tests/unit/test_rust_detector.py +309 -0
- agentmd_gen-0.2.0/tests/unit/test_scorer.py +236 -0
- agentmd_gen-0.2.0/tests/unit/test_scorer_false_positives.py +215 -0
- agentmd_gen-0.2.0/tests/unit/test_swift_detector.py +190 -0
- agentmd_gen-0.2.0/tests/unit/test_test_runner_detector.py +37 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: pip install -e ".[dev]"
|
|
25
|
+
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: pytest tests/unit -q
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mikiships
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentmd-gen
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Analyze codebases and generate optimized context files for AI coding agents (Claude Code, Codex, Cursor, Copilot)
|
|
5
|
+
Author: mikiships
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Software Development
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: typer>=0.16.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest>=8.2.0; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# agentmd
|
|
20
|
+
|
|
21
|
+
agentmd analyzes your codebase and generates optimized context files for AI coding agents. Point it at any Python, Swift/Xcode, Rust, Go, TypeScript, or multi-language project and it produces ready-to-use `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, or Copilot instruction files — scored and ranked so your agent starts with the best possible picture of your project.
|
|
22
|
+
|
|
23
|
+
## What's New in 0.2.0
|
|
24
|
+
|
|
25
|
+
- **Multi-language support** — full detection and generation for Python, Swift/Xcode, Rust, and Go projects (see [Supported Languages](#supported-languages))
|
|
26
|
+
- **`--json` flag** — all commands now support structured JSON output for scripting and CI integration
|
|
27
|
+
- **Freshness scoring fixes** — eliminated false positives in freshness scoring that penalized projects using current stable versions
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install agentmd
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### scan — inspect a project
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
agentmd scan # scan current directory
|
|
41
|
+
agentmd scan ~/repos/myapp # scan a specific path
|
|
42
|
+
agentmd scan --json # output as JSON
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Prints detected languages, frameworks, package managers, test runners, linters, CI systems, and existing context files.
|
|
46
|
+
|
|
47
|
+
### generate — create agent context files
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
agentmd generate # generate for all supported agents
|
|
51
|
+
agentmd generate --agent claude # Claude Code (CLAUDE.md)
|
|
52
|
+
agentmd generate --agent codex # OpenAI Codex (AGENTS.md)
|
|
53
|
+
agentmd generate --agent cursor # Cursor (.cursorrules)
|
|
54
|
+
agentmd generate --agent copilot # GitHub Copilot (.github/copilot-instructions.md)
|
|
55
|
+
agentmd generate --out ./docs/ # write to a custom directory
|
|
56
|
+
agentmd generate --json # output generated content as JSON
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### score — evaluate existing context files
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
agentmd score # score all context files in cwd
|
|
63
|
+
agentmd score CLAUDE.md # score a specific file
|
|
64
|
+
agentmd score --json # output scores as JSON
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Outputs a score (0–100) broken down by dimension.
|
|
68
|
+
|
|
69
|
+
**Example JSON output:**
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"file": "CLAUDE.md",
|
|
74
|
+
"total": 84,
|
|
75
|
+
"dimensions": {
|
|
76
|
+
"completeness": 18,
|
|
77
|
+
"specificity": 17,
|
|
78
|
+
"clarity": 16,
|
|
79
|
+
"agent_awareness": 18,
|
|
80
|
+
"freshness": 15
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### diff — compare context files
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
agentmd diff CLAUDE.md AGENTS.md # side-by-side diff of two context files
|
|
89
|
+
agentmd diff --agent claude # diff current file vs freshly generated output
|
|
90
|
+
agentmd diff --json # output diff as JSON
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Supported Agents
|
|
94
|
+
|
|
95
|
+
| Agent | Output file |
|
|
96
|
+
|-------|-------------|
|
|
97
|
+
| Claude Code | `CLAUDE.md` |
|
|
98
|
+
| OpenAI Codex | `AGENTS.md` |
|
|
99
|
+
| Cursor | `.cursorrules` |
|
|
100
|
+
| GitHub Copilot | `.github/copilot-instructions.md` |
|
|
101
|
+
|
|
102
|
+
## Supported Languages
|
|
103
|
+
|
|
104
|
+
| Language | Detection | Generators | What's detected |
|
|
105
|
+
|----------|-----------|------------|-----------------|
|
|
106
|
+
| **Python** | `requirements.txt`, `pyproject.toml`, `setup.py`, `Pipfile` | All agents | Frameworks (Django, Flask, FastAPI, Starlette, Litestar), pytest, ruff/flake8/mypy, GitHub Actions |
|
|
107
|
+
| **Swift/Xcode** | `.xcodeproj`, `Package.swift` | All agents | SwiftUI/UIKit/AppKit targets, SwiftLint, xcodebuild CI, Swift Package Manager |
|
|
108
|
+
| **Rust** | `Cargo.toml` | All agents | tokio, actix-web, serde, axum, clap, and other common crates; clippy/rustfmt, cargo-based CI |
|
|
109
|
+
| **Go** | `go.mod` | All agents | gin, echo, fiber, cobra, and other common modules; golangci-lint, go test CI |
|
|
110
|
+
|
|
111
|
+
## How Scoring Works
|
|
112
|
+
|
|
113
|
+
Each context file is evaluated on five dimensions (total: 100 points):
|
|
114
|
+
|
|
115
|
+
| Dimension | Points | What it measures |
|
|
116
|
+
|-----------|--------|------------------|
|
|
117
|
+
| **Completeness** | 20 | All key project facts present (languages, stack, test commands) |
|
|
118
|
+
| **Specificity** | 20 | Concrete details vs. generic boilerplate |
|
|
119
|
+
| **Clarity** | 20 | Readable structure, scannable headings, no walls of text |
|
|
120
|
+
| **Agent-awareness** | 20 | Instructions tailored to the target agent's strengths and quirks |
|
|
121
|
+
| **Freshness** | 20 | Content reflects the current state of the codebase (no stale info) |
|
|
122
|
+
|
|
123
|
+
**Note on freshness scoring (v0.2.0):** Earlier versions could false-positive on freshness — penalizing files that referenced current stable versions or recent stable APIs. This has been corrected. The freshness dimension now only flags genuinely stale references (deprecated packages, EOL runtime versions, removed APIs).
|
|
124
|
+
|
|
125
|
+
Run `agentmd score` after generating to see where your files land and what to improve.
|
|
126
|
+
|
|
127
|
+
## Contributing
|
|
128
|
+
|
|
129
|
+
1. Fork the repo and create a branch
|
|
130
|
+
2. `pip install -e ".[dev]"` to get dev dependencies
|
|
131
|
+
3. Write tests in `tests/unit/`
|
|
132
|
+
4. `pytest tests/unit -q` must pass
|
|
133
|
+
5. Open a PR — CI runs on Python 3.10–3.13
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT © 2026 mikiships
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# agentmd
|
|
2
|
+
|
|
3
|
+
agentmd analyzes your codebase and generates optimized context files for AI coding agents. Point it at any Python, Swift/Xcode, Rust, Go, TypeScript, or multi-language project and it produces ready-to-use `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, or Copilot instruction files — scored and ranked so your agent starts with the best possible picture of your project.
|
|
4
|
+
|
|
5
|
+
## What's New in 0.2.0
|
|
6
|
+
|
|
7
|
+
- **Multi-language support** — full detection and generation for Python, Swift/Xcode, Rust, and Go projects (see [Supported Languages](#supported-languages))
|
|
8
|
+
- **`--json` flag** — all commands now support structured JSON output for scripting and CI integration
|
|
9
|
+
- **Freshness scoring fixes** — eliminated false positives in freshness scoring that penalized projects using current stable versions
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install agentmd
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### scan — inspect a project
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
agentmd scan # scan current directory
|
|
23
|
+
agentmd scan ~/repos/myapp # scan a specific path
|
|
24
|
+
agentmd scan --json # output as JSON
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Prints detected languages, frameworks, package managers, test runners, linters, CI systems, and existing context files.
|
|
28
|
+
|
|
29
|
+
### generate — create agent context files
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
agentmd generate # generate for all supported agents
|
|
33
|
+
agentmd generate --agent claude # Claude Code (CLAUDE.md)
|
|
34
|
+
agentmd generate --agent codex # OpenAI Codex (AGENTS.md)
|
|
35
|
+
agentmd generate --agent cursor # Cursor (.cursorrules)
|
|
36
|
+
agentmd generate --agent copilot # GitHub Copilot (.github/copilot-instructions.md)
|
|
37
|
+
agentmd generate --out ./docs/ # write to a custom directory
|
|
38
|
+
agentmd generate --json # output generated content as JSON
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### score — evaluate existing context files
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
agentmd score # score all context files in cwd
|
|
45
|
+
agentmd score CLAUDE.md # score a specific file
|
|
46
|
+
agentmd score --json # output scores as JSON
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Outputs a score (0–100) broken down by dimension.
|
|
50
|
+
|
|
51
|
+
**Example JSON output:**
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"file": "CLAUDE.md",
|
|
56
|
+
"total": 84,
|
|
57
|
+
"dimensions": {
|
|
58
|
+
"completeness": 18,
|
|
59
|
+
"specificity": 17,
|
|
60
|
+
"clarity": 16,
|
|
61
|
+
"agent_awareness": 18,
|
|
62
|
+
"freshness": 15
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### diff — compare context files
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
agentmd diff CLAUDE.md AGENTS.md # side-by-side diff of two context files
|
|
71
|
+
agentmd diff --agent claude # diff current file vs freshly generated output
|
|
72
|
+
agentmd diff --json # output diff as JSON
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Supported Agents
|
|
76
|
+
|
|
77
|
+
| Agent | Output file |
|
|
78
|
+
|-------|-------------|
|
|
79
|
+
| Claude Code | `CLAUDE.md` |
|
|
80
|
+
| OpenAI Codex | `AGENTS.md` |
|
|
81
|
+
| Cursor | `.cursorrules` |
|
|
82
|
+
| GitHub Copilot | `.github/copilot-instructions.md` |
|
|
83
|
+
|
|
84
|
+
## Supported Languages
|
|
85
|
+
|
|
86
|
+
| Language | Detection | Generators | What's detected |
|
|
87
|
+
|----------|-----------|------------|-----------------|
|
|
88
|
+
| **Python** | `requirements.txt`, `pyproject.toml`, `setup.py`, `Pipfile` | All agents | Frameworks (Django, Flask, FastAPI, Starlette, Litestar), pytest, ruff/flake8/mypy, GitHub Actions |
|
|
89
|
+
| **Swift/Xcode** | `.xcodeproj`, `Package.swift` | All agents | SwiftUI/UIKit/AppKit targets, SwiftLint, xcodebuild CI, Swift Package Manager |
|
|
90
|
+
| **Rust** | `Cargo.toml` | All agents | tokio, actix-web, serde, axum, clap, and other common crates; clippy/rustfmt, cargo-based CI |
|
|
91
|
+
| **Go** | `go.mod` | All agents | gin, echo, fiber, cobra, and other common modules; golangci-lint, go test CI |
|
|
92
|
+
|
|
93
|
+
## How Scoring Works
|
|
94
|
+
|
|
95
|
+
Each context file is evaluated on five dimensions (total: 100 points):
|
|
96
|
+
|
|
97
|
+
| Dimension | Points | What it measures |
|
|
98
|
+
|-----------|--------|------------------|
|
|
99
|
+
| **Completeness** | 20 | All key project facts present (languages, stack, test commands) |
|
|
100
|
+
| **Specificity** | 20 | Concrete details vs. generic boilerplate |
|
|
101
|
+
| **Clarity** | 20 | Readable structure, scannable headings, no walls of text |
|
|
102
|
+
| **Agent-awareness** | 20 | Instructions tailored to the target agent's strengths and quirks |
|
|
103
|
+
| **Freshness** | 20 | Content reflects the current state of the codebase (no stale info) |
|
|
104
|
+
|
|
105
|
+
**Note on freshness scoring (v0.2.0):** Earlier versions could false-positive on freshness — penalizing files that referenced current stable versions or recent stable APIs. This has been corrected. The freshness dimension now only flags genuinely stale references (deprecated packages, EOL runtime versions, removed APIs).
|
|
106
|
+
|
|
107
|
+
Run `agentmd score` after generating to see where your files land and what to improve.
|
|
108
|
+
|
|
109
|
+
## Contributing
|
|
110
|
+
|
|
111
|
+
1. Fork the repo and create a branch
|
|
112
|
+
2. `pip install -e ".[dev]"` to get dev dependencies
|
|
113
|
+
3. Write tests in `tests/unit/`
|
|
114
|
+
4. `pytest tests/unit -q` must pass
|
|
115
|
+
5. Open a PR — CI runs on Python 3.10–3.13
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT © 2026 mikiships
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""Project analysis engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from agentmd.detectors import (
|
|
10
|
+
detect_ci_systems,
|
|
11
|
+
detect_frameworks,
|
|
12
|
+
detect_go_project,
|
|
13
|
+
detect_languages,
|
|
14
|
+
detect_linters,
|
|
15
|
+
detect_package_managers,
|
|
16
|
+
detect_rust_project,
|
|
17
|
+
detect_swift_project,
|
|
18
|
+
detect_test_runners,
|
|
19
|
+
)
|
|
20
|
+
from agentmd.detectors.common import collect_project_files, read_text, top_ranked
|
|
21
|
+
from agentmd.types import (
|
|
22
|
+
DirectoryStructure,
|
|
23
|
+
ExistingContextFile,
|
|
24
|
+
GitHistorySummary,
|
|
25
|
+
ProjectAnalysis,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
CONTEXT_FILE_NAMES = ["CLAUDE.md", "AGENTS.md", ".cursorrules", "copilot-instructions.md"]
|
|
29
|
+
|
|
30
|
+
CONTEXT_MARKERS: dict[str, list[str]] = {
|
|
31
|
+
"CLAUDE.md": ["/init", "/review", "/compact", "claude"],
|
|
32
|
+
"AGENTS.md": ["sandbox", "approval", "codex", "apply_patch"],
|
|
33
|
+
".cursorrules": ["cursor", "rules", "always"],
|
|
34
|
+
"copilot-instructions.md": ["copilot", "instruction", "github"],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
SOURCE_DIR_CANDIDATES = {
|
|
38
|
+
"src",
|
|
39
|
+
"app",
|
|
40
|
+
"apps",
|
|
41
|
+
"lib",
|
|
42
|
+
"libs",
|
|
43
|
+
"services",
|
|
44
|
+
"server",
|
|
45
|
+
"client",
|
|
46
|
+
"backend",
|
|
47
|
+
"frontend",
|
|
48
|
+
"pkg",
|
|
49
|
+
"cmd",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
TEST_DIR_CANDIDATES = {"tests", "test", "spec", "__tests__"}
|
|
53
|
+
MONOREPO_DIR_HINTS = {"packages", "apps", "services"}
|
|
54
|
+
MANIFEST_FILE_NAMES = {
|
|
55
|
+
"pyproject.toml",
|
|
56
|
+
"requirements.txt",
|
|
57
|
+
"setup.py",
|
|
58
|
+
"package.json",
|
|
59
|
+
"Cargo.toml",
|
|
60
|
+
"go.mod",
|
|
61
|
+
"Gemfile",
|
|
62
|
+
"pom.xml",
|
|
63
|
+
"build.gradle",
|
|
64
|
+
"build.gradle.kts",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ProjectAnalyzer:
|
|
69
|
+
"""Analyze codebase structure, tools, and existing context files."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, max_git_commits: int = 100) -> None:
|
|
72
|
+
self.max_git_commits = max_git_commits
|
|
73
|
+
|
|
74
|
+
def analyze(self, path: str | Path = ".") -> ProjectAnalysis:
|
|
75
|
+
"""Run full project analysis for a path."""
|
|
76
|
+
root = Path(path).resolve()
|
|
77
|
+
files = collect_project_files(root)
|
|
78
|
+
|
|
79
|
+
language_findings = detect_languages(root, files)
|
|
80
|
+
package_manager_findings = detect_package_managers(root, files)
|
|
81
|
+
framework_findings = detect_frameworks(root, files)
|
|
82
|
+
test_runner_findings = detect_test_runners(root, files)
|
|
83
|
+
linter_findings = detect_linters(root, files)
|
|
84
|
+
ci_findings = detect_ci_systems(root, files)
|
|
85
|
+
swift_findings = detect_swift_project(root, files)
|
|
86
|
+
rust_findings = detect_rust_project(root, files)
|
|
87
|
+
go_findings = detect_go_project(root, files)
|
|
88
|
+
|
|
89
|
+
directory_structure = self._analyze_directory_structure(root, files)
|
|
90
|
+
git_history = self._analyze_git_history(root)
|
|
91
|
+
context_files = self._detect_existing_context_files(root)
|
|
92
|
+
|
|
93
|
+
detection_reasons: dict[str, dict[str, list[str]]] = {
|
|
94
|
+
"languages": language_findings.evidence,
|
|
95
|
+
"package_managers": package_manager_findings.evidence,
|
|
96
|
+
"frameworks": framework_findings.evidence,
|
|
97
|
+
"test_runners": test_runner_findings.evidence,
|
|
98
|
+
"linters": linter_findings.evidence,
|
|
99
|
+
"ci_systems": ci_findings.evidence,
|
|
100
|
+
"swift": swift_findings.evidence,
|
|
101
|
+
"rust": rust_findings.evidence,
|
|
102
|
+
"go": go_findings.evidence,
|
|
103
|
+
"directory_structure": {
|
|
104
|
+
"source_directories": directory_structure.source_directories,
|
|
105
|
+
"test_directories": directory_structure.test_directories,
|
|
106
|
+
"monorepo_indicators": directory_structure.monorepo_indicators,
|
|
107
|
+
},
|
|
108
|
+
"existing_context_files": {
|
|
109
|
+
item.name: [item.path] for item in context_files if item.present
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return ProjectAnalysis(
|
|
114
|
+
root_path=str(root),
|
|
115
|
+
languages=language_findings.values,
|
|
116
|
+
package_managers=package_manager_findings.values,
|
|
117
|
+
frameworks=framework_findings.values,
|
|
118
|
+
test_runners=test_runner_findings.values,
|
|
119
|
+
linters=linter_findings.values,
|
|
120
|
+
ci_systems=ci_findings.values,
|
|
121
|
+
swift_components=swift_findings.values,
|
|
122
|
+
rust_components=rust_findings.values,
|
|
123
|
+
go_components=go_findings.values,
|
|
124
|
+
directory_structure=directory_structure,
|
|
125
|
+
git_history=git_history,
|
|
126
|
+
existing_context_files=context_files,
|
|
127
|
+
detection_reasons=detection_reasons,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def _analyze_directory_structure(self, root: Path, files: list[Path]) -> DirectoryStructure:
|
|
131
|
+
top_level_dirs = sorted({path.parts[0] for path in files if len(path.parts) > 1})
|
|
132
|
+
top_level_files = sorted({path.name for path in files if len(path.parts) == 1})
|
|
133
|
+
|
|
134
|
+
source_directories = sorted(
|
|
135
|
+
{directory for directory in top_level_dirs if directory.lower() in SOURCE_DIR_CANDIDATES}
|
|
136
|
+
)
|
|
137
|
+
test_directories = sorted(
|
|
138
|
+
{directory for directory in top_level_dirs if directory.lower() in TEST_DIR_CANDIDATES}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
monorepo_indicators = self._monorepo_indicators(root, files)
|
|
142
|
+
return DirectoryStructure(
|
|
143
|
+
top_level_directories=top_level_dirs,
|
|
144
|
+
top_level_files=top_level_files,
|
|
145
|
+
source_directories=source_directories,
|
|
146
|
+
test_directories=test_directories,
|
|
147
|
+
is_monorepo=bool(monorepo_indicators),
|
|
148
|
+
monorepo_indicators=monorepo_indicators,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def _monorepo_indicators(self, root: Path, files: list[Path]) -> list[str]:
|
|
152
|
+
indicators: list[str] = []
|
|
153
|
+
file_names = {path.name for path in files}
|
|
154
|
+
|
|
155
|
+
if "pnpm-workspace.yaml" in file_names:
|
|
156
|
+
indicators.append("pnpm workspace file present")
|
|
157
|
+
|
|
158
|
+
package_json = root / "package.json"
|
|
159
|
+
if package_json.exists():
|
|
160
|
+
package_json_content = read_text(package_json, max_chars=25000)
|
|
161
|
+
if "\"workspaces\"" in package_json_content:
|
|
162
|
+
indicators.append("package.json defines workspaces")
|
|
163
|
+
|
|
164
|
+
manifest_dirs = {
|
|
165
|
+
path.parts[0]
|
|
166
|
+
for path in files
|
|
167
|
+
if path.name in MANIFEST_FILE_NAMES and len(path.parts) > 1
|
|
168
|
+
}
|
|
169
|
+
if len(manifest_dirs) > 1:
|
|
170
|
+
indicators.append("multiple top-level directories contain package manifests")
|
|
171
|
+
|
|
172
|
+
for hinted_dir in MONOREPO_DIR_HINTS:
|
|
173
|
+
candidate = root / hinted_dir
|
|
174
|
+
if not candidate.exists() or not candidate.is_dir():
|
|
175
|
+
continue
|
|
176
|
+
child_count = 0
|
|
177
|
+
for child in candidate.iterdir():
|
|
178
|
+
if not child.is_dir():
|
|
179
|
+
continue
|
|
180
|
+
if any((child / manifest).exists() for manifest in MANIFEST_FILE_NAMES):
|
|
181
|
+
child_count += 1
|
|
182
|
+
if child_count >= 2:
|
|
183
|
+
indicators.append(f"{hinted_dir}/ has {child_count} package subprojects")
|
|
184
|
+
|
|
185
|
+
return sorted(set(indicators))
|
|
186
|
+
|
|
187
|
+
def _analyze_git_history(self, root: Path) -> GitHistorySummary:
|
|
188
|
+
if not (root / ".git").exists():
|
|
189
|
+
return GitHistorySummary()
|
|
190
|
+
|
|
191
|
+
inside_git = subprocess.run(
|
|
192
|
+
["git", "-C", str(root), "rev-parse", "--is-inside-work-tree"],
|
|
193
|
+
check=False,
|
|
194
|
+
capture_output=True,
|
|
195
|
+
text=True,
|
|
196
|
+
)
|
|
197
|
+
if inside_git.returncode != 0 or inside_git.stdout.strip().lower() != "true":
|
|
198
|
+
return GitHistorySummary()
|
|
199
|
+
|
|
200
|
+
log_result = subprocess.run(
|
|
201
|
+
[
|
|
202
|
+
"git",
|
|
203
|
+
"-C",
|
|
204
|
+
str(root),
|
|
205
|
+
"log",
|
|
206
|
+
f"-n{self.max_git_commits}",
|
|
207
|
+
"--pretty=format:__COMMIT__%s",
|
|
208
|
+
"--name-only",
|
|
209
|
+
],
|
|
210
|
+
check=False,
|
|
211
|
+
capture_output=True,
|
|
212
|
+
text=True,
|
|
213
|
+
)
|
|
214
|
+
if log_result.returncode != 0:
|
|
215
|
+
return GitHistorySummary()
|
|
216
|
+
|
|
217
|
+
extension_counter: Counter[str] = Counter()
|
|
218
|
+
directory_counter: Counter[str] = Counter()
|
|
219
|
+
message_prefix_counter: Counter[str] = Counter()
|
|
220
|
+
commit_count = 0
|
|
221
|
+
|
|
222
|
+
for line in log_result.stdout.splitlines():
|
|
223
|
+
if not line:
|
|
224
|
+
continue
|
|
225
|
+
if line.startswith("__COMMIT__"):
|
|
226
|
+
commit_count += 1
|
|
227
|
+
message = line.replace("__COMMIT__", "", 1).strip()
|
|
228
|
+
if not message:
|
|
229
|
+
continue
|
|
230
|
+
if ":" in message:
|
|
231
|
+
prefix = message.split(":", 1)[0].strip().lower()
|
|
232
|
+
else:
|
|
233
|
+
prefix = message.split()[0].strip().lower()
|
|
234
|
+
if prefix:
|
|
235
|
+
message_prefix_counter[prefix] += 1
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
changed_path = Path(line.strip())
|
|
239
|
+
if changed_path.suffix:
|
|
240
|
+
extension_counter[changed_path.suffix.lower()] += 1
|
|
241
|
+
else:
|
|
242
|
+
extension_counter["[no_ext]"] += 1
|
|
243
|
+
if changed_path.parts:
|
|
244
|
+
directory_counter[changed_path.parts[0]] += 1
|
|
245
|
+
|
|
246
|
+
return GitHistorySummary(
|
|
247
|
+
commit_count=commit_count,
|
|
248
|
+
common_file_extensions=top_ranked(extension_counter),
|
|
249
|
+
common_directories=top_ranked(directory_counter),
|
|
250
|
+
common_message_prefixes=top_ranked(message_prefix_counter),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def _detect_existing_context_files(self, root: Path) -> list[ExistingContextFile]:
|
|
254
|
+
records: list[ExistingContextFile] = []
|
|
255
|
+
for file_name in CONTEXT_FILE_NAMES:
|
|
256
|
+
full_path = root / file_name
|
|
257
|
+
relative_path = file_name
|
|
258
|
+
if not full_path.exists():
|
|
259
|
+
records.append(
|
|
260
|
+
ExistingContextFile(name=file_name, path=relative_path, present=False)
|
|
261
|
+
)
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
content = read_text(full_path, max_chars=50000)
|
|
265
|
+
lines = content.splitlines()
|
|
266
|
+
first_heading = self._first_heading(lines)
|
|
267
|
+
markers = self._detect_markers(file_name, content)
|
|
268
|
+
records.append(
|
|
269
|
+
ExistingContextFile(
|
|
270
|
+
name=file_name,
|
|
271
|
+
path=relative_path,
|
|
272
|
+
present=True,
|
|
273
|
+
line_count=len(lines),
|
|
274
|
+
first_heading=first_heading,
|
|
275
|
+
agent_markers=markers,
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
return records
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def _first_heading(lines: list[str]) -> str | None:
|
|
282
|
+
for line in lines:
|
|
283
|
+
stripped = line.strip()
|
|
284
|
+
if not stripped:
|
|
285
|
+
continue
|
|
286
|
+
if stripped.startswith("#"):
|
|
287
|
+
return stripped
|
|
288
|
+
return stripped[:120]
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
@staticmethod
|
|
292
|
+
def _detect_markers(file_name: str, content: str) -> list[str]:
|
|
293
|
+
lowered = content.lower()
|
|
294
|
+
markers = []
|
|
295
|
+
for marker in CONTEXT_MARKERS.get(file_name, []):
|
|
296
|
+
if marker.lower() in lowered:
|
|
297
|
+
markers.append(marker)
|
|
298
|
+
return sorted(set(markers))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def analyze_project(path: str | Path = ".") -> ProjectAnalysis:
|
|
302
|
+
"""Convenience wrapper for one-shot analysis."""
|
|
303
|
+
return ProjectAnalyzer().analyze(path)
|