codedocent 0.3.0__tar.gz → 0.4.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.
- {codedocent-0.3.0 → codedocent-0.4.0}/PKG-INFO +23 -12
- {codedocent-0.3.0 → codedocent-0.4.0}/README.md +19 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/quality.py +6 -54
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/server.py +18 -2
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/templates/interactive.html +9 -11
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent.egg-info/PKG-INFO +23 -12
- {codedocent-0.3.0 → codedocent-0.4.0}/pyproject.toml +2 -2
- {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_analyzer.py +68 -141
- {codedocent-0.3.0 → codedocent-0.4.0}/LICENSE +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/__init__.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/__main__.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/analyzer.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/cli.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/editor.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/gui.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/ollama_utils.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/parser.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/renderer.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/scanner.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/templates/base.html +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent.egg-info/SOURCES.txt +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent.egg-info/dependency_links.txt +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent.egg-info/entry_points.txt +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent.egg-info/requires.txt +4 -4
- {codedocent-0.3.0 → codedocent-0.4.0}/codedocent.egg-info/top_level.txt +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/setup.cfg +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_cli.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_editor.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_gui.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_parser.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_renderer.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_scanner.py +0 -0
- {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_server.py +0 -0
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: codedocent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Code visualization for non-programmers
|
|
5
|
-
License
|
|
5
|
+
License: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Requires-Dist: tree-sitter>=0.23
|
|
10
|
-
Requires-Dist: tree-sitter-language-pack>=0.13
|
|
11
|
-
Requires-Dist: radon>=6.0
|
|
12
|
-
Requires-Dist: pathspec>=0.11
|
|
13
|
-
Requires-Dist: jinja2>=3.1
|
|
14
|
-
Requires-Dist: ollama>=0.4
|
|
15
8
|
Provides-Extra: dev
|
|
16
|
-
|
|
17
|
-
Dynamic: license-file
|
|
9
|
+
License-File: LICENSE
|
|
18
10
|
|
|
19
11
|
# codedocent
|
|
20
12
|
|
|
@@ -24,6 +16,21 @@ Dynamic: license-file
|
|
|
24
16
|
|
|
25
17
|
A docent is a guide who explains things to people who aren't experts. Codedocent does that for code.
|
|
26
18
|
|
|
19
|
+
## The problem
|
|
20
|
+
|
|
21
|
+
You're staring at a codebase you didn't write — maybe thousands of files across dozens of directories — and you need to understand what it does. Reading every file isn't realistic. You need a way to visualize the code structure, get a high-level map of what's where, and drill into the parts that matter without losing context.
|
|
22
|
+
|
|
23
|
+
Codedocent parses the codebase into a navigable, visual block structure and explains each piece in plain English. It's an AI code analysis tool that runs entirely on your machine — no API keys, no cloud, no data leaving your laptop. Point it at any codebase and get a structural overview you can explore interactively, understand quickly, and share as a static HTML file.
|
|
24
|
+
|
|
25
|
+
## Who this is for
|
|
26
|
+
|
|
27
|
+
- **Developers onboarding onto an unfamiliar codebase** — get oriented in minutes instead of days
|
|
28
|
+
- **Non-programmers** (managers, designers, PMs) who need to understand what code does without reading it
|
|
29
|
+
- **Solo developers inheriting legacy code** — map out the structure before making changes
|
|
30
|
+
- **Code reviewers** who want a high-level overview before diving into details
|
|
31
|
+
- **Security reviewers** who need a structural map of an application
|
|
32
|
+
- **Students** learning to read and navigate real-world codebases
|
|
33
|
+
|
|
27
34
|
## What you see
|
|
28
35
|
|
|
29
36
|
Nested, color-coded blocks representing directories, files, classes, and functions — the entire structure of a codebase laid out visually. Each block shows a plain English summary, a pseudocode translation, and quality warnings (green/yellow/red). Click any block to drill down; breadcrumbs navigate you back up. You can export code from any block or paste replacement code back into the source file. All AI runs locally through Ollama — nothing leaves your machine.
|
|
@@ -49,6 +56,10 @@ codedocent --gui # graphical launcher
|
|
|
49
56
|
|
|
50
57
|
Parses code structure with tree-sitter, scores quality with static analysis, and sends individual blocks to a local Ollama model for plain English summaries and pseudocode. Interactive mode analyzes on click — typically 1-2 seconds per block. Full mode analyzes everything upfront into a self-contained HTML file you can share.
|
|
51
58
|
|
|
59
|
+
## Why local
|
|
60
|
+
|
|
61
|
+
All AI processing runs through Ollama on your machine. Your code is never uploaded, transmitted, or stored anywhere external. No API keys, no accounts, no cloud services. This matters when you're working with proprietary code, client projects, or anything you can't share — codedocent works fully air-gapped. The `--no-ai` mode removes the AI dependency entirely while keeping the structural visualization and quality scoring.
|
|
62
|
+
|
|
52
63
|
## Supported languages
|
|
53
64
|
|
|
54
65
|
Full AST parsing for Python and JavaScript/TypeScript (functions, classes, methods, imports). File-level detection for 23 extensions including C, C++, Rust, Go, Java, Ruby, PHP, Swift, Kotlin, Scala, HTML, CSS, and config formats.
|
|
@@ -6,6 +6,21 @@
|
|
|
6
6
|
|
|
7
7
|
A docent is a guide who explains things to people who aren't experts. Codedocent does that for code.
|
|
8
8
|
|
|
9
|
+
## The problem
|
|
10
|
+
|
|
11
|
+
You're staring at a codebase you didn't write — maybe thousands of files across dozens of directories — and you need to understand what it does. Reading every file isn't realistic. You need a way to visualize the code structure, get a high-level map of what's where, and drill into the parts that matter without losing context.
|
|
12
|
+
|
|
13
|
+
Codedocent parses the codebase into a navigable, visual block structure and explains each piece in plain English. It's an AI code analysis tool that runs entirely on your machine — no API keys, no cloud, no data leaving your laptop. Point it at any codebase and get a structural overview you can explore interactively, understand quickly, and share as a static HTML file.
|
|
14
|
+
|
|
15
|
+
## Who this is for
|
|
16
|
+
|
|
17
|
+
- **Developers onboarding onto an unfamiliar codebase** — get oriented in minutes instead of days
|
|
18
|
+
- **Non-programmers** (managers, designers, PMs) who need to understand what code does without reading it
|
|
19
|
+
- **Solo developers inheriting legacy code** — map out the structure before making changes
|
|
20
|
+
- **Code reviewers** who want a high-level overview before diving into details
|
|
21
|
+
- **Security reviewers** who need a structural map of an application
|
|
22
|
+
- **Students** learning to read and navigate real-world codebases
|
|
23
|
+
|
|
9
24
|
## What you see
|
|
10
25
|
|
|
11
26
|
Nested, color-coded blocks representing directories, files, classes, and functions — the entire structure of a codebase laid out visually. Each block shows a plain English summary, a pseudocode translation, and quality warnings (green/yellow/red). Click any block to drill down; breadcrumbs navigate you back up. You can export code from any block or paste replacement code back into the source file. All AI runs locally through Ollama — nothing leaves your machine.
|
|
@@ -31,6 +46,10 @@ codedocent --gui # graphical launcher
|
|
|
31
46
|
|
|
32
47
|
Parses code structure with tree-sitter, scores quality with static analysis, and sends individual blocks to a local Ollama model for plain English summaries and pseudocode. Interactive mode analyzes on click — typically 1-2 seconds per block. Full mode analyzes everything upfront into a self-contained HTML file you can share.
|
|
33
48
|
|
|
49
|
+
## Why local
|
|
50
|
+
|
|
51
|
+
All AI processing runs through Ollama on your machine. Your code is never uploaded, transmitted, or stored anywhere external. No API keys, no accounts, no cloud services. This matters when you're working with proprietary code, client projects, or anything you can't share — codedocent works fully air-gapped. The `--no-ai` mode removes the AI dependency entirely while keeping the structural visualization and quality scoring.
|
|
52
|
+
|
|
34
53
|
## Supported languages
|
|
35
54
|
|
|
36
55
|
Full AST parsing for Python and JavaScript/TypeScript (functions, classes, methods, imports). File-level detection for 23 extensions including C, C++, Rust, Go, Java, Ruby, PHP, Swift, Kotlin, Scala, HTML, CSS, and config formats.
|
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
"""Quality scoring: complexity
|
|
1
|
+
"""Quality scoring: complexity and parameter checks."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import os
|
|
6
|
-
|
|
7
5
|
from codedocent.parser import CodeNode
|
|
8
6
|
|
|
9
|
-
# Quality scoring thresholds: (yellow_threshold, red_threshold)
|
|
10
|
-
# yellow = "complex", red = "warning"
|
|
11
|
-
LINE_THRESHOLDS: dict[str, tuple[int, int]] = {
|
|
12
|
-
"function": (50, 100),
|
|
13
|
-
"method": (50, 100),
|
|
14
|
-
"file": (500, 1000),
|
|
15
|
-
"class": (300, 600),
|
|
16
|
-
}
|
|
17
7
|
PARAM_THRESHOLD = 5
|
|
18
8
|
|
|
19
9
|
|
|
@@ -82,17 +72,17 @@ def _score_radon(node: CodeNode) -> tuple[str, str | None]:
|
|
|
82
72
|
if blocks:
|
|
83
73
|
worst = max(b.complexity for b in blocks)
|
|
84
74
|
rank = cc_rank(worst)
|
|
85
|
-
if rank in ("A", "B"):
|
|
75
|
+
if rank in ("A", "B", "C"):
|
|
86
76
|
return "clean", None
|
|
87
|
-
if rank == "
|
|
77
|
+
if rank == "D":
|
|
88
78
|
return (
|
|
89
79
|
"complex",
|
|
90
|
-
f"
|
|
80
|
+
f"High complexity (grade {rank},"
|
|
91
81
|
f" score {worst})",
|
|
92
82
|
)
|
|
93
83
|
return (
|
|
94
84
|
"warning",
|
|
95
|
-
f"
|
|
85
|
+
f"Severe complexity (grade {rank},"
|
|
96
86
|
f" score {worst})",
|
|
97
87
|
)
|
|
98
88
|
except (ImportError, AttributeError, SyntaxError): # nosec B110
|
|
@@ -101,44 +91,6 @@ def _score_radon(node: CodeNode) -> tuple[str, str | None]:
|
|
|
101
91
|
return "clean", None
|
|
102
92
|
|
|
103
93
|
|
|
104
|
-
def _is_exempt_file(node: CodeNode) -> bool:
|
|
105
|
-
"""Check if a file node should skip line-count scoring.
|
|
106
|
-
|
|
107
|
-
HTML templates and test files are naturally long, so line count
|
|
108
|
-
does not indicate poor quality for these file types.
|
|
109
|
-
"""
|
|
110
|
-
if node.node_type != "file":
|
|
111
|
-
return False
|
|
112
|
-
if node.language == "html":
|
|
113
|
-
return True
|
|
114
|
-
if node.language is None and node.name.endswith(".html"):
|
|
115
|
-
return True
|
|
116
|
-
if os.path.basename(node.name).startswith("test_"):
|
|
117
|
-
return True
|
|
118
|
-
return False
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def _score_line_count(node: CodeNode) -> tuple[str, str | None]:
|
|
122
|
-
"""Score based on line-count thresholds (two-tier: yellow/red)."""
|
|
123
|
-
if _is_exempt_file(node):
|
|
124
|
-
return "clean", None
|
|
125
|
-
thresholds = LINE_THRESHOLDS.get(node.node_type)
|
|
126
|
-
if thresholds and node.line_count:
|
|
127
|
-
yellow, red = thresholds
|
|
128
|
-
if node.line_count > red:
|
|
129
|
-
return (
|
|
130
|
-
"warning",
|
|
131
|
-
f"This {node.node_type} is"
|
|
132
|
-
f" {node.line_count} lines long",
|
|
133
|
-
)
|
|
134
|
-
if node.line_count > yellow:
|
|
135
|
-
return (
|
|
136
|
-
"complex",
|
|
137
|
-
f"Long {node.node_type}: {node.line_count} lines",
|
|
138
|
-
)
|
|
139
|
-
return "clean", None
|
|
140
|
-
|
|
141
|
-
|
|
142
94
|
def _score_param_count(node: CodeNode) -> tuple[str, str | None]:
|
|
143
95
|
"""Score based on parameter count."""
|
|
144
96
|
if node.node_type in ("function", "method"):
|
|
@@ -162,7 +114,7 @@ def _score_quality(
|
|
|
162
114
|
warnings: list[str] = []
|
|
163
115
|
quality = "clean"
|
|
164
116
|
|
|
165
|
-
for scorer in (_score_radon,
|
|
117
|
+
for scorer in (_score_radon, _score_param_count):
|
|
166
118
|
label, warning = scorer(node)
|
|
167
119
|
quality = _worst_quality(quality, label)
|
|
168
120
|
if warning:
|
|
@@ -21,6 +21,9 @@ from codedocent.renderer import LANGUAGE_COLORS, DEFAULT_COLOR, NODE_ICONS
|
|
|
21
21
|
IDLE_TIMEOUT = 300 # 5 minutes
|
|
22
22
|
IDLE_CHECK_INTERVAL = 30 # seconds
|
|
23
23
|
MAX_BODY_SIZE = 10 * 1024 * 1024 # 10 MB
|
|
24
|
+
_TEMPLATES_DIR = os.path.realpath(
|
|
25
|
+
os.path.join(os.path.dirname(__file__), "templates"),
|
|
26
|
+
)
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
def _node_to_dict(node: CodeNode, include_source: bool = False) -> dict:
|
|
@@ -179,18 +182,31 @@ def _execute_replace( # pylint: disable=too-many-return-statements
|
|
|
179
182
|
if node_id not in _Handler.node_lookup:
|
|
180
183
|
return (404, {"success": False, "error": "Unknown node ID"})
|
|
181
184
|
node = _Handler.node_lookup[node_id]
|
|
182
|
-
if node.node_type
|
|
185
|
+
if node.node_type == "directory":
|
|
183
186
|
return (
|
|
184
187
|
400,
|
|
185
188
|
{"success": False,
|
|
186
|
-
"error": "Cannot replace directory
|
|
189
|
+
"error": "Cannot replace directory blocks"},
|
|
187
190
|
)
|
|
188
191
|
new_source = body.get("source", "")
|
|
189
192
|
if not isinstance(new_source, str):
|
|
190
193
|
return (400, {"success": False, "error": "source must be a string"})
|
|
194
|
+
if len(new_source.encode("utf-8")) > 1_000_000:
|
|
195
|
+
return (
|
|
196
|
+
400,
|
|
197
|
+
{"success": False, "error": "Replacement too large (max 1MB)"},
|
|
198
|
+
)
|
|
191
199
|
abs_path = _resolve_filepath(node, _Handler.cache_dir)
|
|
192
200
|
real_path = os.path.realpath(abs_path)
|
|
193
201
|
real_root = os.path.realpath(_Handler.cache_dir)
|
|
202
|
+
if real_path == _TEMPLATES_DIR or real_path.startswith(
|
|
203
|
+
_TEMPLATES_DIR + os.sep,
|
|
204
|
+
):
|
|
205
|
+
return (
|
|
206
|
+
400,
|
|
207
|
+
{"success": False,
|
|
208
|
+
"error": "Cannot replace tool template files"},
|
|
209
|
+
)
|
|
194
210
|
inside = real_path == real_root or real_path.startswith(
|
|
195
211
|
real_root + os.sep,
|
|
196
212
|
)
|
|
@@ -671,18 +671,16 @@ function cdCreateCodeActions(bodyEl, nodeId, nodeType, nodeName, filepath, langu
|
|
|
671
671
|
});
|
|
672
672
|
actions.appendChild(btnAI);
|
|
673
673
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
cdShowReplacePanel(bodyEl, nodeId, source, pre, btnShow);
|
|
682
|
-
});
|
|
674
|
+
var btnReplace = document.createElement('button');
|
|
675
|
+
btnReplace.className = 'cd-code-btn';
|
|
676
|
+
btnReplace.textContent = 'Replace Code';
|
|
677
|
+
btnReplace.addEventListener('click', function(e) {
|
|
678
|
+
e.stopPropagation();
|
|
679
|
+
ensureSource(function(source) {
|
|
680
|
+
cdShowReplacePanel(bodyEl, nodeId, source, pre, btnShow);
|
|
683
681
|
});
|
|
684
|
-
|
|
685
|
-
|
|
682
|
+
});
|
|
683
|
+
actions.appendChild(btnReplace);
|
|
686
684
|
|
|
687
685
|
var childrenEl = bodyEl.querySelector('.cd-node__children');
|
|
688
686
|
if (childrenEl) {
|
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: codedocent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Code visualization for non-programmers
|
|
5
|
-
License
|
|
5
|
+
License: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Requires-Dist: tree-sitter>=0.23
|
|
10
|
-
Requires-Dist: tree-sitter-language-pack>=0.13
|
|
11
|
-
Requires-Dist: radon>=6.0
|
|
12
|
-
Requires-Dist: pathspec>=0.11
|
|
13
|
-
Requires-Dist: jinja2>=3.1
|
|
14
|
-
Requires-Dist: ollama>=0.4
|
|
15
8
|
Provides-Extra: dev
|
|
16
|
-
|
|
17
|
-
Dynamic: license-file
|
|
9
|
+
License-File: LICENSE
|
|
18
10
|
|
|
19
11
|
# codedocent
|
|
20
12
|
|
|
@@ -24,6 +16,21 @@ Dynamic: license-file
|
|
|
24
16
|
|
|
25
17
|
A docent is a guide who explains things to people who aren't experts. Codedocent does that for code.
|
|
26
18
|
|
|
19
|
+
## The problem
|
|
20
|
+
|
|
21
|
+
You're staring at a codebase you didn't write — maybe thousands of files across dozens of directories — and you need to understand what it does. Reading every file isn't realistic. You need a way to visualize the code structure, get a high-level map of what's where, and drill into the parts that matter without losing context.
|
|
22
|
+
|
|
23
|
+
Codedocent parses the codebase into a navigable, visual block structure and explains each piece in plain English. It's an AI code analysis tool that runs entirely on your machine — no API keys, no cloud, no data leaving your laptop. Point it at any codebase and get a structural overview you can explore interactively, understand quickly, and share as a static HTML file.
|
|
24
|
+
|
|
25
|
+
## Who this is for
|
|
26
|
+
|
|
27
|
+
- **Developers onboarding onto an unfamiliar codebase** — get oriented in minutes instead of days
|
|
28
|
+
- **Non-programmers** (managers, designers, PMs) who need to understand what code does without reading it
|
|
29
|
+
- **Solo developers inheriting legacy code** — map out the structure before making changes
|
|
30
|
+
- **Code reviewers** who want a high-level overview before diving into details
|
|
31
|
+
- **Security reviewers** who need a structural map of an application
|
|
32
|
+
- **Students** learning to read and navigate real-world codebases
|
|
33
|
+
|
|
27
34
|
## What you see
|
|
28
35
|
|
|
29
36
|
Nested, color-coded blocks representing directories, files, classes, and functions — the entire structure of a codebase laid out visually. Each block shows a plain English summary, a pseudocode translation, and quality warnings (green/yellow/red). Click any block to drill down; breadcrumbs navigate you back up. You can export code from any block or paste replacement code back into the source file. All AI runs locally through Ollama — nothing leaves your machine.
|
|
@@ -49,6 +56,10 @@ codedocent --gui # graphical launcher
|
|
|
49
56
|
|
|
50
57
|
Parses code structure with tree-sitter, scores quality with static analysis, and sends individual blocks to a local Ollama model for plain English summaries and pseudocode. Interactive mode analyzes on click — typically 1-2 seconds per block. Full mode analyzes everything upfront into a self-contained HTML file you can share.
|
|
51
58
|
|
|
59
|
+
## Why local
|
|
60
|
+
|
|
61
|
+
All AI processing runs through Ollama on your machine. Your code is never uploaded, transmitted, or stored anywhere external. No API keys, no accounts, no cloud services. This matters when you're working with proprietary code, client projects, or anything you can't share — codedocent works fully air-gapped. The `--no-ai` mode removes the AI dependency entirely while keeping the structural visualization and quality scoring.
|
|
62
|
+
|
|
52
63
|
## Supported languages
|
|
53
64
|
|
|
54
65
|
Full AST parsing for Python and JavaScript/TypeScript (functions, classes, methods, imports). File-level detection for 23 extensions including C, C++, Rust, Go, Java, Ruby, PHP, Swift, Kotlin, Scala, HTML, CSS, and config formats.
|
|
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codedocent"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Code visualization for non-programmers"
|
|
9
|
-
license = "MIT"
|
|
9
|
+
license = {text = "MIT"}
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
dependencies = [
|
|
@@ -157,8 +157,8 @@ def test_quality_complex_branchy():
|
|
|
157
157
|
|
|
158
158
|
node = _make_func_node(name="decide", source=source)
|
|
159
159
|
quality, warnings = _score_quality(node)
|
|
160
|
-
assert quality
|
|
161
|
-
assert warnings is
|
|
160
|
+
assert quality == "clean"
|
|
161
|
+
assert warnings is None
|
|
162
162
|
|
|
163
163
|
|
|
164
164
|
def test_directory_summary_no_ai():
|
|
@@ -243,22 +243,6 @@ def test_strip_think_tags():
|
|
|
243
243
|
assert "SUMMARY: hello" in result
|
|
244
244
|
|
|
245
245
|
|
|
246
|
-
def test_long_function_warning():
|
|
247
|
-
from codedocent.quality import _score_quality
|
|
248
|
-
|
|
249
|
-
# 56-line function
|
|
250
|
-
lines = ["def long_func():"]
|
|
251
|
-
for i in range(55):
|
|
252
|
-
lines.append(f" x = {i}")
|
|
253
|
-
source = "\n".join(lines) + "\n"
|
|
254
|
-
|
|
255
|
-
node = _make_func_node(name="long_func", source=source)
|
|
256
|
-
node.line_count = 56
|
|
257
|
-
quality, warnings = _score_quality(node)
|
|
258
|
-
assert warnings is not None
|
|
259
|
-
assert any("Long function" in w for w in warnings)
|
|
260
|
-
|
|
261
|
-
|
|
262
246
|
def test_many_params_warning():
|
|
263
247
|
from codedocent.quality import _score_quality
|
|
264
248
|
|
|
@@ -410,93 +394,6 @@ def test_garbage_response_fallback(mock_ollama, tmp_path):
|
|
|
410
394
|
# ---------------------------------------------------------------------------
|
|
411
395
|
|
|
412
396
|
|
|
413
|
-
def test_quality_function_yellow_tier():
|
|
414
|
-
from codedocent.quality import _score_quality
|
|
415
|
-
|
|
416
|
-
# 60-line function (above 50 yellow, below 100 red)
|
|
417
|
-
lines = ["def long_func():"]
|
|
418
|
-
for i in range(59):
|
|
419
|
-
lines.append(f" x = {i}")
|
|
420
|
-
source = "\n".join(lines) + "\n"
|
|
421
|
-
|
|
422
|
-
node = _make_func_node(name="long_func", source=source)
|
|
423
|
-
node.line_count = 60
|
|
424
|
-
quality, warnings = _score_quality(node)
|
|
425
|
-
assert quality == "complex"
|
|
426
|
-
assert warnings is not None
|
|
427
|
-
assert any("60 lines" in w for w in warnings)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
def test_quality_function_red_tier():
|
|
431
|
-
from codedocent.quality import _score_quality
|
|
432
|
-
|
|
433
|
-
# 110-line function (above 100 red)
|
|
434
|
-
lines = ["def very_long_func():"]
|
|
435
|
-
for i in range(109):
|
|
436
|
-
lines.append(f" x = {i}")
|
|
437
|
-
source = "\n".join(lines) + "\n"
|
|
438
|
-
|
|
439
|
-
node = _make_func_node(name="very_long_func", source=source)
|
|
440
|
-
node.line_count = 110
|
|
441
|
-
quality, warnings = _score_quality(node)
|
|
442
|
-
assert quality == "warning"
|
|
443
|
-
assert warnings is not None
|
|
444
|
-
assert any("110 lines" in w for w in warnings)
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
def test_quality_file_yellow_tier():
|
|
448
|
-
from codedocent.quality import _score_quality
|
|
449
|
-
|
|
450
|
-
# 550-line file (above 500 yellow, below 1000 red)
|
|
451
|
-
lines = [f"x_{i} = {i}" for i in range(550)]
|
|
452
|
-
source = "\n".join(lines) + "\n"
|
|
453
|
-
|
|
454
|
-
node = _make_file_node(name="big.py", source=source)
|
|
455
|
-
node.line_count = 550
|
|
456
|
-
quality, warnings = _score_quality(node)
|
|
457
|
-
assert quality == "complex"
|
|
458
|
-
assert warnings is not None
|
|
459
|
-
assert any("550 lines" in w for w in warnings)
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
def test_quality_file_red_tier():
|
|
463
|
-
from codedocent.quality import _score_quality
|
|
464
|
-
|
|
465
|
-
# 1100-line file (above 1000 red)
|
|
466
|
-
lines = [f"x_{i} = {i}" for i in range(1100)]
|
|
467
|
-
source = "\n".join(lines) + "\n"
|
|
468
|
-
|
|
469
|
-
node = _make_file_node(name="huge.py", source=source)
|
|
470
|
-
node.line_count = 1100
|
|
471
|
-
quality, warnings = _score_quality(node)
|
|
472
|
-
assert quality == "warning"
|
|
473
|
-
assert warnings is not None
|
|
474
|
-
assert any("1100 lines" in w for w in warnings)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
def test_quality_class_yellow_tier():
|
|
478
|
-
from codedocent.quality import _score_quality
|
|
479
|
-
|
|
480
|
-
# 311-line class (above 300 yellow, below 600 red)
|
|
481
|
-
lines = ["class BigClass:"]
|
|
482
|
-
for i in range(310):
|
|
483
|
-
lines.append(f" x_{i} = {i}")
|
|
484
|
-
source = "\n".join(lines) + "\n"
|
|
485
|
-
|
|
486
|
-
node = CodeNode(
|
|
487
|
-
name="BigClass",
|
|
488
|
-
node_type="class",
|
|
489
|
-
language="python",
|
|
490
|
-
filepath="test.py",
|
|
491
|
-
start_line=1,
|
|
492
|
-
end_line=311,
|
|
493
|
-
source=source,
|
|
494
|
-
line_count=311,
|
|
495
|
-
)
|
|
496
|
-
quality, warnings = _score_quality(node)
|
|
497
|
-
assert quality == "complex"
|
|
498
|
-
|
|
499
|
-
|
|
500
397
|
def test_quality_rollup_to_file():
|
|
501
398
|
from codedocent.quality import _rollup_quality
|
|
502
399
|
|
|
@@ -544,42 +441,6 @@ def test_quality_directory_returns_none():
|
|
|
544
441
|
assert warnings is None
|
|
545
442
|
|
|
546
443
|
|
|
547
|
-
# ---------------------------------------------------------------------------
|
|
548
|
-
# Phase 8: Exempt HTML templates and test files from line-count scoring
|
|
549
|
-
# ---------------------------------------------------------------------------
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
def test_quality_html_file_exempt_from_line_count():
|
|
553
|
-
from codedocent.quality import _score_quality
|
|
554
|
-
|
|
555
|
-
node = _make_file_node(name="base.html", lang="html", source="<div></div>\n")
|
|
556
|
-
node.line_count = 1500
|
|
557
|
-
quality, warnings = _score_quality(node)
|
|
558
|
-
assert quality == "clean"
|
|
559
|
-
assert warnings is None
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
def test_quality_test_file_exempt_from_line_count():
|
|
563
|
-
from codedocent.quality import _score_quality
|
|
564
|
-
|
|
565
|
-
node = _make_file_node(name="test_foo.py", lang="python", source="x = 1\n")
|
|
566
|
-
node.line_count = 600
|
|
567
|
-
quality, warnings = _score_quality(node)
|
|
568
|
-
assert quality == "clean"
|
|
569
|
-
assert warnings is None
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
def test_quality_regular_python_file_still_scored():
|
|
573
|
-
from codedocent.quality import _score_quality
|
|
574
|
-
|
|
575
|
-
node = _make_file_node(name="app.py", lang="python", source="x = 1\n")
|
|
576
|
-
node.line_count = 600
|
|
577
|
-
quality, warnings = _score_quality(node)
|
|
578
|
-
assert quality == "complex"
|
|
579
|
-
assert warnings is not None
|
|
580
|
-
assert any("600 lines" in w for w in warnings)
|
|
581
|
-
|
|
582
|
-
|
|
583
444
|
# ---------------------------------------------------------------------------
|
|
584
445
|
# Batch 2: Security audit fixes 8-16
|
|
585
446
|
# ---------------------------------------------------------------------------
|
|
@@ -655,3 +516,69 @@ def test_radon_syntax_error_returns_clean():
|
|
|
655
516
|
)
|
|
656
517
|
quality, warnings = _score_quality(node)
|
|
657
518
|
assert quality == "clean"
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# ---------------------------------------------------------------------------
|
|
522
|
+
# Security fixes: replace endpoint guards
|
|
523
|
+
# ---------------------------------------------------------------------------
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def test_replace_rejects_oversized_payload():
|
|
527
|
+
"""Fix 1: payloads over 1 MB are rejected with 400 (byte-size)."""
|
|
528
|
+
from codedocent.server import _execute_replace, _Handler
|
|
529
|
+
|
|
530
|
+
node = _make_func_node(name="target", source="def target():\n pass\n")
|
|
531
|
+
node.node_id = "test_node_001"
|
|
532
|
+
_Handler.node_lookup = {"test_node_001": node}
|
|
533
|
+
_Handler.cache_dir = "/tmp/test_proj"
|
|
534
|
+
|
|
535
|
+
giant = "x" * 1_000_001
|
|
536
|
+
status, result = _execute_replace("test_node_001", {"source": giant})
|
|
537
|
+
assert status == 400
|
|
538
|
+
assert "too large" in result["error"]
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def test_replace_rejects_template_filepath():
|
|
542
|
+
"""Fix 3: files inside codedocent's templates dir are rejected."""
|
|
543
|
+
from codedocent.server import _execute_replace, _Handler, _TEMPLATES_DIR
|
|
544
|
+
|
|
545
|
+
for name in ("interactive.html", "base.html"):
|
|
546
|
+
tmpl_path = os.path.join(_TEMPLATES_DIR, name)
|
|
547
|
+
node = _make_file_node(name=name, source="<html></html>\n")
|
|
548
|
+
node.filepath = tmpl_path
|
|
549
|
+
node.node_id = "tmpl_node_001"
|
|
550
|
+
_Handler.node_lookup = {"tmpl_node_001": node}
|
|
551
|
+
_Handler.cache_dir = _TEMPLATES_DIR
|
|
552
|
+
|
|
553
|
+
status, result = _execute_replace(
|
|
554
|
+
"tmpl_node_001", {"source": "<p>hacked</p>"},
|
|
555
|
+
)
|
|
556
|
+
assert status == 400
|
|
557
|
+
assert "Cannot replace tool template files" in result["error"]
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def test_replace_accepts_file_node(tmp_path):
|
|
561
|
+
"""Happy-path: file-level replace succeeds end-to-end."""
|
|
562
|
+
from codedocent.server import _execute_replace, _Handler
|
|
563
|
+
|
|
564
|
+
original = "x = 1\ny = 2\n"
|
|
565
|
+
replacement = "x = 10\ny = 20\nz = 30\n"
|
|
566
|
+
|
|
567
|
+
target = tmp_path / "test.py"
|
|
568
|
+
target.write_text(original, encoding="utf-8")
|
|
569
|
+
|
|
570
|
+
node = _make_file_node(name="test.py", source=original)
|
|
571
|
+
node.filepath = str(target)
|
|
572
|
+
node.node_id = "file_node_001"
|
|
573
|
+
_Handler.node_lookup = {"file_node_001": node}
|
|
574
|
+
_Handler.cache_dir = str(tmp_path)
|
|
575
|
+
_Handler.root = _make_dir_node(
|
|
576
|
+
name="proj", children=[node], filepath=str(tmp_path),
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
status, result = _execute_replace(
|
|
580
|
+
"file_node_001", {"source": replacement},
|
|
581
|
+
)
|
|
582
|
+
assert status == 200
|
|
583
|
+
assert result["success"] is True
|
|
584
|
+
assert target.read_text(encoding="utf-8") == replacement
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|