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.
Files changed (33) hide show
  1. {codedocent-0.3.0 → codedocent-0.4.0}/PKG-INFO +23 -12
  2. {codedocent-0.3.0 → codedocent-0.4.0}/README.md +19 -0
  3. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/quality.py +6 -54
  4. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/server.py +18 -2
  5. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/templates/interactive.html +9 -11
  6. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent.egg-info/PKG-INFO +23 -12
  7. {codedocent-0.3.0 → codedocent-0.4.0}/pyproject.toml +2 -2
  8. {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_analyzer.py +68 -141
  9. {codedocent-0.3.0 → codedocent-0.4.0}/LICENSE +0 -0
  10. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/__init__.py +0 -0
  11. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/__main__.py +0 -0
  12. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/analyzer.py +0 -0
  13. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/cli.py +0 -0
  14. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/editor.py +0 -0
  15. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/gui.py +0 -0
  16. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/ollama_utils.py +0 -0
  17. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/parser.py +0 -0
  18. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/renderer.py +0 -0
  19. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/scanner.py +0 -0
  20. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent/templates/base.html +0 -0
  21. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent.egg-info/SOURCES.txt +0 -0
  22. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent.egg-info/dependency_links.txt +0 -0
  23. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent.egg-info/entry_points.txt +0 -0
  24. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent.egg-info/requires.txt +4 -4
  25. {codedocent-0.3.0 → codedocent-0.4.0}/codedocent.egg-info/top_level.txt +0 -0
  26. {codedocent-0.3.0 → codedocent-0.4.0}/setup.cfg +0 -0
  27. {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_cli.py +0 -0
  28. {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_editor.py +0 -0
  29. {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_gui.py +0 -0
  30. {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_parser.py +0 -0
  31. {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_renderer.py +0 -0
  32. {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_scanner.py +0 -0
  33. {codedocent-0.3.0 → codedocent-0.4.0}/tests/test_server.py +0 -0
@@ -1,20 +1,12 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: codedocent
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Code visualization for non-programmers
5
- License-Expression: MIT
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
- Requires-Dist: pytest>=7.0; extra == "dev"
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, line counts, and parameter checks."""
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 == "C":
77
+ if rank == "D":
88
78
  return (
89
79
  "complex",
90
- f"Moderate complexity (grade {rank},"
80
+ f"High complexity (grade {rank},"
91
81
  f" score {worst})",
92
82
  )
93
83
  return (
94
84
  "warning",
95
- f"High complexity (grade {rank},"
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, _score_line_count, _score_param_count):
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 in ("directory", "file"):
185
+ if node.node_type == "directory":
183
186
  return (
184
187
  400,
185
188
  {"success": False,
186
- "error": "Cannot replace directory/file blocks"},
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
- if (nodeType === 'function' || nodeType === 'method' || nodeType === 'class') {
675
- var btnReplace = document.createElement('button');
676
- btnReplace.className = 'cd-code-btn';
677
- btnReplace.textContent = 'Replace Code';
678
- btnReplace.addEventListener('click', function(e) {
679
- e.stopPropagation();
680
- ensureSource(function(source) {
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
- actions.appendChild(btnReplace);
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.4
1
+ Metadata-Version: 2.1
2
2
  Name: codedocent
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Code visualization for non-programmers
5
- License-Expression: MIT
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
- Requires-Dist: pytest>=7.0; extra == "dev"
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.3.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 in ("complex", "warning")
161
- assert warnings is not None
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
@@ -1,9 +1,9 @@
1
- tree-sitter>=0.23
2
- tree-sitter-language-pack>=0.13
3
- radon>=6.0
4
- pathspec>=0.11
5
1
  jinja2>=3.1
6
2
  ollama>=0.4
3
+ pathspec>=0.11
4
+ radon>=6.0
5
+ tree-sitter-language-pack>=0.13
6
+ tree-sitter>=0.23
7
7
 
8
8
  [dev]
9
9
  pytest>=7.0
File without changes
File without changes
File without changes