code2docs 3.0.30__tar.gz → 3.0.32__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 (68) hide show
  1. {code2docs-3.0.30 → code2docs-3.0.32}/PKG-INFO +7 -7
  2. {code2docs-3.0.30 → code2docs-3.0.32}/README.md +6 -6
  3. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/__init__.py +1 -1
  4. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/cli.py +30 -0
  5. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/examples_gen.py +6 -34
  6. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/readme_gen.py +42 -8
  7. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/templates/readme.md.j2 +10 -25
  8. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs.egg-info/PKG-INFO +7 -7
  9. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs.egg-info/SOURCES.txt +1 -0
  10. {code2docs-3.0.30 → code2docs-3.0.32}/pyproject.toml +1 -1
  11. code2docs-3.0.32/tests/test_markdown_validator.py +75 -0
  12. {code2docs-3.0.30 → code2docs-3.0.32}/LICENSE +0 -0
  13. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/__main__.py +0 -0
  14. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/analyzers/__init__.py +0 -0
  15. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/analyzers/dependency_scanner.py +0 -0
  16. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/analyzers/docstring_extractor.py +0 -0
  17. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/analyzers/endpoint_detector.py +0 -0
  18. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/analyzers/markdown_validator.py +0 -0
  19. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/analyzers/project_scanner.py +0 -0
  20. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/base.py +0 -0
  21. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/config.py +0 -0
  22. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/examples/advanced_usage.py +0 -0
  23. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/examples/quickstart.py +0 -0
  24. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/formatters/__init__.py +0 -0
  25. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/formatters/badges.py +0 -0
  26. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/formatters/markdown.py +0 -0
  27. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/formatters/toc.py +0 -0
  28. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/__init__.py +0 -0
  29. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/_registry_adapters.py +0 -0
  30. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/_source_links.py +0 -0
  31. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/api_changelog_gen.py +0 -0
  32. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/api_reference_gen.py +0 -0
  33. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/architecture_gen.py +0 -0
  34. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/changelog_gen.py +0 -0
  35. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/code2llm_gen.py +0 -0
  36. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/config_docs_gen.py +0 -0
  37. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/contributing_gen.py +0 -0
  38. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/coverage_gen.py +0 -0
  39. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/depgraph_gen.py +0 -0
  40. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/getting_started_gen.py +0 -0
  41. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/mkdocs_gen.py +0 -0
  42. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/module_docs_gen.py +0 -0
  43. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/generators/org_readme_gen.py +0 -0
  44. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/llm_helper.py +0 -0
  45. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/registry.py +0 -0
  46. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/sync/__init__.py +0 -0
  47. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/sync/differ.py +0 -0
  48. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/sync/updater.py +0 -0
  49. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/sync/watcher.py +0 -0
  50. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/templates/api_module.md.j2 +0 -0
  51. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/templates/architecture.md.j2 +0 -0
  52. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/templates/example_usage.py.j2 +0 -0
  53. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/templates/index.md.j2 +0 -0
  54. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs/templates/module_doc.md.j2 +0 -0
  55. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs.egg-info/dependency_links.txt +0 -0
  56. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs.egg-info/entry_points.txt +0 -0
  57. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs.egg-info/requires.txt +0 -0
  58. {code2docs-3.0.30 → code2docs-3.0.32}/code2docs.egg-info/top_level.txt +0 -0
  59. {code2docs-3.0.30 → code2docs-3.0.32}/setup.cfg +0 -0
  60. {code2docs-3.0.30 → code2docs-3.0.32}/tests/test_analyzers.py +0 -0
  61. {code2docs-3.0.30 → code2docs-3.0.32}/tests/test_cli.py +0 -0
  62. {code2docs-3.0.30 → code2docs-3.0.32}/tests/test_code2docs.py +0 -0
  63. {code2docs-3.0.30 → code2docs-3.0.32}/tests/test_config.py +0 -0
  64. {code2docs-3.0.30 → code2docs-3.0.32}/tests/test_formatters.py +0 -0
  65. {code2docs-3.0.30 → code2docs-3.0.32}/tests/test_generators.py +0 -0
  66. {code2docs-3.0.30 → code2docs-3.0.32}/tests/test_llm_helper.py +0 -0
  67. {code2docs-3.0.30 → code2docs-3.0.32}/tests/test_registry.py +0 -0
  68. {code2docs-3.0.30 → code2docs-3.0.32}/tests/test_sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code2docs
3
- Version: 3.0.30
3
+ Version: 3.0.32
4
4
  Summary: Auto-generate and sync project documentation from source code analysis
5
5
  Author-email: Tom Sapletta <tom@sapletta.com>
6
6
  License-Expression: Apache-2.0
@@ -53,17 +53,17 @@ Dynamic: license-file
53
53
 
54
54
  ## AI Cost Tracking
55
55
 
56
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-3.0.30-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
57
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-24.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
56
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-3.0.32-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
57
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-25.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
58
58
 
59
- - 🤖 **LLM usage:** $7.5000 (70 commits)
60
- - 👤 **Human dev:** ~$2434 (24.3h @ $100/h, 30min dedup)
59
+ - 🤖 **LLM usage:** $7.5000 (72 commits)
60
+ - 👤 **Human dev:** ~$2534 (25.3h @ $100/h, 30min dedup)
61
61
 
62
62
  Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
63
63
 
64
64
  ---
65
65
 
66
- ![version](https://img.shields.io/badge/version-3.0.30-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)
66
+ ![version](https://img.shields.io/badge/version-3.0.32-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)
67
67
 
68
68
  > Auto-generate and sync project documentation from source code analysis.
69
69
 
@@ -200,7 +200,7 @@ code2docs can update only specific sections of an existing README using markers:
200
200
  ```markdown
201
201
  <!-- code2docs:start --># code2docs
202
202
 
203
- ![version](https://img.shields.io/badge/version-3.0.30-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey) ![functions](https://img.shields.io/badge/functions-276-green)
203
+ ![version](https://img.shields.io/badge/version-3.0.32-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey) ![functions](https://img.shields.io/badge/functions-276-green)
204
204
  > **276** functions | **57** classes | **51** files | CC̄ = 3.8
205
205
 
206
206
  > Auto-generated project documentation from source code analysis.
@@ -1,16 +1,16 @@
1
1
  ## AI Cost Tracking
2
2
 
3
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-3.0.30-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
4
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-24.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
3
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-3.0.32-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
4
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-25.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
5
5
 
6
- - 🤖 **LLM usage:** $7.5000 (70 commits)
7
- - 👤 **Human dev:** ~$2434 (24.3h @ $100/h, 30min dedup)
6
+ - 🤖 **LLM usage:** $7.5000 (72 commits)
7
+ - 👤 **Human dev:** ~$2534 (25.3h @ $100/h, 30min dedup)
8
8
 
9
9
  Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
10
10
 
11
11
  ---
12
12
 
13
- ![version](https://img.shields.io/badge/version-3.0.30-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)
13
+ ![version](https://img.shields.io/badge/version-3.0.32-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)
14
14
 
15
15
  > Auto-generate and sync project documentation from source code analysis.
16
16
 
@@ -147,7 +147,7 @@ code2docs can update only specific sections of an existing README using markers:
147
147
  ```markdown
148
148
  <!-- code2docs:start --># code2docs
149
149
 
150
- ![version](https://img.shields.io/badge/version-3.0.30-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey) ![functions](https://img.shields.io/badge/functions-276-green)
150
+ ![version](https://img.shields.io/badge/version-3.0.32-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey) ![functions](https://img.shields.io/badge/functions-276-green)
151
151
  > **276** functions | **57** classes | **51** files | CC̄ = 3.8
152
152
 
153
153
  > Auto-generated project documentation from source code analysis.
@@ -5,7 +5,7 @@ Uses code2llm's AnalysisResult to produce human-readable documentation:
5
5
  README.md, API references, module docs, examples, and architecture diagrams.
6
6
  """
7
7
 
8
- __version__ = '3.0.30'
8
+ __version__ = '3.0.32'
9
9
  __author__ = 'Tom Sapletta'
10
10
  __all__ = ['Code2DocsConfig', 'generate_readme', 'generate_docs', 'analyze_and_document']
11
11
 
@@ -93,6 +93,36 @@ def check(project_path, config_path, target) -> None:
93
93
  config = _load_config(project_path, config_path)
94
94
  _run_check(project_path, config, target)
95
95
 
96
+ @main.command()
97
+ @click.argument('project_path', default='.', type=click.Path(exists=True))
98
+ @click.option('--pattern', multiple=True, default=('*.md',), show_default=True,
99
+ help='Glob pattern(s) of markdown files to validate.')
100
+ @click.option('--strict', is_flag=True, help='Exit with non-zero status if any issues are found.')
101
+ def validate(project_path, pattern, strict) -> None:
102
+ """Validate generated markdown: broken links, table shape, duplicate headings."""
103
+ from .analyzers.markdown_validator import validate_markdown_tree
104
+ project = Path(project_path).resolve()
105
+ console.print(f'[bold blue]🔎 code2docs validate:[/] {project.name}')
106
+ report = validate_markdown_tree(project, patterns=pattern)
107
+ if report.ok:
108
+ console.print(f'[green]{report.summary()}[/]')
109
+ return
110
+ table = Table(title='Markdown Issues', show_lines=False)
111
+ table.add_column('File', style='cyan', overflow='fold')
112
+ table.add_column('Line', justify='right')
113
+ table.add_column('Kind', style='magenta')
114
+ table.add_column('Message', style='dim', overflow='fold')
115
+ for issue in report.issues:
116
+ try:
117
+ rel = Path(issue.file).relative_to(project)
118
+ except ValueError:
119
+ rel = Path(issue.file)
120
+ table.add_row(str(rel), str(issue.line), issue.kind, issue.message)
121
+ console.print(table)
122
+ console.print(f'[yellow]{report.summary()}[/]')
123
+ if strict:
124
+ sys.exit(1)
125
+
96
126
  @main.command()
97
127
  @click.argument('project_path', default='.', type=click.Path(exists=True))
98
128
  @click.option('--config', '-c', 'config_path', default=None, help='Path to code2docs.yaml')
@@ -1,33 +1,3 @@
1
-
2
- PORT_4 = 4
3
- CONSTANT_5 = 5
4
- CONSTANT_50 = 50
5
-
6
-
7
- PORT_4 = PORT_4
8
- CONSTANT_5 = CONSTANT_5
9
- CONSTANT_50 = CONSTANT_50
10
-
11
-
12
- PORT_4 = PORT_4
13
- CONSTANT_5 = CONSTANT_5
14
- CONSTANT_50 = CONSTANT_50
15
-
16
-
17
- PORT_4 = PORT_4
18
- CONSTANT_5 = CONSTANT_5
19
- CONSTANT_50 = CONSTANT_50
20
-
21
-
22
-
23
- PORT_4 = PORT_4
24
- CONSTANT_5 = CONSTANT_5
25
- CONSTANT_50 = CONSTANT_50
26
-
27
- PORT_4 = PORT_4
28
- CONSTANT_5 = CONSTANT_5
29
- CONSTANT_50 = CONSTANT_50
30
-
31
1
  """Auto-generate usage examples from public signatures and entry points."""
32
2
 
33
3
  from pathlib import Path
@@ -37,10 +7,12 @@ from code2llm.api import AnalysisResult, FunctionInfo, ClassInfo
37
7
 
38
8
  from ..config import Code2DocsConfig
39
9
 
40
- # Default type hints → example values
10
+ PORT_4 = 4
11
+ CONSTANT_5 = 5
12
+ CONSTANT_50 = 50
41
13
 
42
- if __name__ == "__main__":
43
- _TYPE_EXAMPLES = {
14
+ # Default type hints → example values
15
+ _TYPE_EXAMPLES = {
44
16
  "str": '"./my-project"',
45
17
  "Path": 'Path("./my-project")',
46
18
  "int": "10",
@@ -55,7 +27,7 @@ if __name__ == "__main__":
55
27
  }
56
28
 
57
29
  # Arg name → realistic example value
58
- _ARG_EXAMPLES = {
30
+ _ARG_EXAMPLES = {
59
31
  "project_path": '"./my-project"',
60
32
  "path": '"./my-project"',
61
33
  "source": '"./src"',
@@ -59,7 +59,36 @@ class ReadmeGenerator:
59
59
  extras = self._extract_extras()
60
60
  lang = getattr(deps, 'language', 'python') or 'python'
61
61
  docs_nav_items, generated_files = self._collect_existing_docs()
62
- return {'docs_nav_items': docs_nav_items, 'generated_files': generated_files, 'project_name': project_name, 'project_path': self.result.project_path, 'project_description': project_description, 'badges': generate_badges(project_name, self.config.readme.badges, stats, deps), 'stats': stats, 'avg_complexity': avg_complexity, 'dependencies': deps, 'language': lang, 'endpoints': endpoints, 'public_functions': public_functions, 'public_classes': public_classes, 'entry_points': entry_points, 'module_tree': module_tree, 'modules': self.result.modules, 'sync_markers': self.config.readme.sync_markers, 'author': metadata.get('author', ''), 'license': metadata.get('license', ''), 'license_file': metadata.get('license_file', ''), 'contributors': metadata.get('contributors', []), 'repo_url': self.config.repo_url, 'version': metadata.get('version', '0.1.0'), 'extras': extras}
62
+ has_contributing = (Path(self.result.project_path) / 'CONTRIBUTING.md').exists()
63
+ return {'docs_nav_items': docs_nav_items, 'generated_files': generated_files, 'has_contributing': has_contributing, 'project_name': project_name, 'project_path': self.result.project_path, 'project_description': project_description, 'badges': generate_badges(project_name, self.config.readme.badges, stats, deps), 'stats': stats, 'avg_complexity': avg_complexity, 'dependencies': deps, 'language': lang, 'endpoints': endpoints, 'public_functions': public_functions, 'public_classes': public_classes, 'entry_points': entry_points, 'module_tree': module_tree, 'modules': self.result.modules, 'sync_markers': self.config.readme.sync_markers, 'author': metadata.get('author', ''), 'license': metadata.get('license', ''), 'license_file': metadata.get('license_file', ''), 'contributors': metadata.get('contributors', []), 'repo_url': self.config.repo_url, 'version': metadata.get('version', '0.1.0'), 'extras': extras}
64
+
65
+ _DOC_FILE_SPECS = [
66
+ ('docs/getting-started.md', 'Getting Started', '🚀', 'Quick start guide'),
67
+ ('docs/api.md', 'API Reference', '📚', 'Complete API documentation'),
68
+ ('docs/modules.md', 'Module Reference', '📦', 'Module reference with metrics'),
69
+ ('docs/architecture.md', 'Architecture', '🏛️', 'Architecture with diagrams'),
70
+ ('docs/dependency-graph.md', 'Dependency Graph', '🔗', 'Module dependency graphs'),
71
+ ('docs/coverage.md', 'Coverage', '📊', 'Docstring coverage report'),
72
+ ('docs/configuration.md', 'Configuration', '🔧', 'Configuration reference'),
73
+ ('docs/api-changelog.md', 'API Changelog', '📝', 'API change tracking'),
74
+ ('CONTRIBUTING.md', 'Contributing', '🤝', 'Contribution guidelines'),
75
+ ('examples', 'Examples', '💡', 'Usage examples and code samples'),
76
+ ('mkdocs.yml', 'MkDocs Config', '⚙️', 'MkDocs site configuration'),
77
+ ]
78
+
79
+ def _collect_existing_docs(self) -> tuple:
80
+ """Return (docs_nav_items, generated_files) only for files/dirs that exist."""
81
+ project = Path(self.result.project_path)
82
+ nav_items = []
83
+ generated = []
84
+ for rel_path, title, icon, description in self._DOC_FILE_SPECS:
85
+ target = project / rel_path
86
+ if not target.exists():
87
+ continue
88
+ link = f'./{rel_path}'
89
+ nav_items.append({'title': title, 'icon': icon, 'path': link, 'description': description})
90
+ generated.append({'output': rel_path, 'description': description, 'link': link})
91
+ return nav_items, generated
63
92
 
64
93
  def _calc_avg_complexity(self) -> float:
65
94
  """Calculate average cyclomatic complexity."""
@@ -181,14 +210,19 @@ class ReadmeGenerator:
181
210
  pass
182
211
 
183
212
  def _detect_license(self, metadata: Dict) -> None:
184
- """Detect license type from LICENSE files."""
185
- license_paths = [Path(self.result.project_path) / lf for lf in ['LICENSE', 'LICENSE.txt', 'LICENSE.md', 'COPYING']]
186
- parent_path = Path(self.result.project_path).parent
187
- if parent_path != Path(self.result.project_path):
188
- license_paths += [parent_path / lf for lf in ['LICENSE', 'LICENSE.txt', 'LICENSE.md', 'COPYING']]
189
- for license_path in license_paths:
213
+ """Detect license type from LICENSE files.
214
+
215
+ Only sets ``license_file`` (used as a link target) when the file lives
216
+ in the project root, so README links resolve correctly.
217
+ """
218
+ project_root = Path(self.result.project_path)
219
+ in_project = [project_root / lf for lf in ['LICENSE', 'LICENSE.txt', 'LICENSE.md', 'COPYING']]
220
+ parent = project_root.parent
221
+ in_parent = [parent / lf for lf in ['LICENSE', 'LICENSE.txt', 'LICENSE.md', 'COPYING']] if parent != project_root else []
222
+ for license_path in in_project + in_parent:
190
223
  if license_path.exists():
191
- metadata['license_file'] = license_path.name
224
+ if license_path.parent == project_root:
225
+ metadata['license_file'] = license_path.name
192
226
  if not metadata['license']:
193
227
  try:
194
228
  content = license_path.read_text(encoding='utf-8').lower()
@@ -316,7 +316,7 @@ Content outside the markers is preserved when regenerating. Enable this with `sy
316
316
  {% endfor %}
317
317
  {% endif %}
318
318
 
319
- We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
319
+ {% if has_contributing %}We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.{% else %}We welcome contributions! Open an issue or pull request to get started.{% endif %}
320
320
 
321
321
  ### Development Setup
322
322
 
@@ -353,38 +353,23 @@ pytest
353
353
  ```
354
354
  {% endif %}
355
355
 
356
- {% if "docs_nav" in sections %}
356
+ {% if "docs_nav" in sections and docs_nav_items %}
357
357
  ## Documentation
358
358
 
359
- {% if repo_url %}
360
- - 📖 [Full Documentation]({{ repo_url }}/tree/main/docs) — API reference, module docs, architecture
361
- - 🚀 [Getting Started]({{ repo_url }}/blob/main/docs/getting-started.md) — Quick start guide
362
- - 📚 [API Reference]({{ repo_url }}/blob/main/docs/api.md) — Complete API documentation
363
- - 🔧 [Configuration]({{ repo_url }}/blob/main/docs/configuration.md) — Configuration options
364
- {% else %}
365
- - 📖 [Full Documentation](./docs) — API reference, module docs, architecture
366
- - 🚀 [Getting Started](./docs/getting-started.md) — Quick start guide
367
- - 📚 [API Reference](./docs/api.md) — Complete API documentation
368
- - 🔧 [Configuration](./docs/configuration.md) — Configuration options
369
- {% endif %}
370
- - 💡 [Examples](./examples) — Usage examples and code samples
359
+ {% for item in docs_nav_items %}
360
+ - {{ item.icon }} [{{ item.title }}]({{ item.path }}) — {{ item.description }}
361
+ {% endfor %}
371
362
 
363
+ {% if generated_files %}
372
364
  ### Generated Files
373
365
 
374
366
  | Output | Description | Link |
375
367
  |--------|-------------|------|
376
368
  | `README.md` | Project overview (this file) | — |
377
- | `docs/api.md` | Consolidated API reference | [View](./docs/api.md) |
378
- | `docs/modules.md` | Module reference with metrics | [View](./docs/modules.md) |
379
- | `docs/architecture.md` | Architecture with diagrams | [View](./docs/architecture.md) |
380
- | `docs/dependency-graph.md` | Dependency graphs | [View](./docs/dependency-graph.md) |
381
- | `docs/coverage.md` | Docstring coverage report | [View](./docs/coverage.md) |
382
- | `docs/getting-started.md` | Getting started guide | [View](./docs/getting-started.md) |
383
- | `docs/configuration.md` | Configuration reference | [View](./docs/configuration.md) |
384
- | `docs/api-changelog.md` | API change tracking | [View](./docs/api-changelog.md) |
385
- | `CONTRIBUTING.md` | Contribution guidelines | [View](./CONTRIBUTING.md) |
386
- | `examples/` | Usage examples | [Browse](./examples) |
387
- | `mkdocs.yml` | MkDocs configuration | — |
369
+ {% for file in generated_files %}
370
+ | `{{ file.output }}` | {{ file.description }} | [View]({{ file.link }}) |
371
+ {% endfor %}
372
+ {% endif %}
388
373
  {% endif %}
389
374
 
390
375
  {% if sync_markers %}<!-- code2docs:end -->{% endif %}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code2docs
3
- Version: 3.0.30
3
+ Version: 3.0.32
4
4
  Summary: Auto-generate and sync project documentation from source code analysis
5
5
  Author-email: Tom Sapletta <tom@sapletta.com>
6
6
  License-Expression: Apache-2.0
@@ -53,17 +53,17 @@ Dynamic: license-file
53
53
 
54
54
  ## AI Cost Tracking
55
55
 
56
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-3.0.30-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
57
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-24.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
56
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-3.0.32-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
57
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-25.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
58
58
 
59
- - 🤖 **LLM usage:** $7.5000 (70 commits)
60
- - 👤 **Human dev:** ~$2434 (24.3h @ $100/h, 30min dedup)
59
+ - 🤖 **LLM usage:** $7.5000 (72 commits)
60
+ - 👤 **Human dev:** ~$2534 (25.3h @ $100/h, 30min dedup)
61
61
 
62
62
  Generated on 2026-04-20 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
63
63
 
64
64
  ---
65
65
 
66
- ![version](https://img.shields.io/badge/version-3.0.30-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)
66
+ ![version](https://img.shields.io/badge/version-3.0.32-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)
67
67
 
68
68
  > Auto-generate and sync project documentation from source code analysis.
69
69
 
@@ -200,7 +200,7 @@ code2docs can update only specific sections of an existing README using markers:
200
200
  ```markdown
201
201
  <!-- code2docs:start --># code2docs
202
202
 
203
- ![version](https://img.shields.io/badge/version-3.0.30-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey) ![functions](https://img.shields.io/badge/functions-276-green)
203
+ ![version](https://img.shields.io/badge/version-3.0.32-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey) ![functions](https://img.shields.io/badge/functions-276-green)
204
204
  > **276** functions | **57** classes | **51** files | CC̄ = 3.8
205
205
 
206
206
  > Auto-generated project documentation from source code analysis.
@@ -61,5 +61,6 @@ tests/test_config.py
61
61
  tests/test_formatters.py
62
62
  tests/test_generators.py
63
63
  tests/test_llm_helper.py
64
+ tests/test_markdown_validator.py
64
65
  tests/test_registry.py
65
66
  tests/test_sync.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code2docs"
7
- version = "3.0.30"
7
+ version = "3.0.32"
8
8
  description = "Auto-generate and sync project documentation from source code analysis"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,75 @@
1
+ """Tests for markdown validator."""
2
+ from pathlib import Path
3
+
4
+ from code2docs.analyzers.markdown_validator import (
5
+ validate_markdown_file,
6
+ validate_markdown_tree,
7
+ )
8
+
9
+
10
+ def test_validate_broken_link(tmp_path: Path) -> None:
11
+ md = tmp_path / "README.md"
12
+ md.write_text(
13
+ "# Title\n\n"
14
+ "[missing](./does-not-exist.md)\n"
15
+ "[existing](./present.md)\n",
16
+ encoding="utf-8",
17
+ )
18
+ (tmp_path / "present.md").write_text("# Present", encoding="utf-8")
19
+ issues = validate_markdown_file(md, project_root=tmp_path)
20
+ kinds = {i.kind for i in issues}
21
+ assert "broken_link" in kinds
22
+ assert any("does-not-exist" in i.message for i in issues)
23
+ assert not any("present.md" in i.message for i in issues)
24
+
25
+
26
+ def test_validate_ignores_http_and_anchors(tmp_path: Path) -> None:
27
+ md = tmp_path / "a.md"
28
+ md.write_text(
29
+ "[web](https://example.com)\n"
30
+ "[anchor](#section)\n"
31
+ "[mail](mailto:x@example.com)\n",
32
+ encoding="utf-8",
33
+ )
34
+ assert validate_markdown_file(md, project_root=tmp_path) == []
35
+
36
+
37
+ def test_validate_ignores_links_in_code_fences(tmp_path: Path) -> None:
38
+ md = tmp_path / "a.md"
39
+ md.write_text(
40
+ "```\n[missing](./nope.md)\n```\n",
41
+ encoding="utf-8",
42
+ )
43
+ assert validate_markdown_file(md, project_root=tmp_path) == []
44
+
45
+
46
+ def test_validate_detects_table_shape(tmp_path: Path) -> None:
47
+ md = tmp_path / "a.md"
48
+ md.write_text(
49
+ "| a | b | c |\n"
50
+ "|---|---|---|\n"
51
+ "| 1 | 2 |\n",
52
+ encoding="utf-8",
53
+ )
54
+ issues = validate_markdown_file(md, project_root=tmp_path)
55
+ assert any(i.kind == "table_shape" for i in issues)
56
+
57
+
58
+ def test_validate_detects_duplicate_headings(tmp_path: Path) -> None:
59
+ md = tmp_path / "a.md"
60
+ md.write_text(
61
+ "## Intro\n\n## Intro\n",
62
+ encoding="utf-8",
63
+ )
64
+ issues = validate_markdown_file(md, project_root=tmp_path)
65
+ assert any(i.kind == "duplicate_heading" for i in issues)
66
+
67
+
68
+ def test_validate_tree_skips_ignored_dirs(tmp_path: Path) -> None:
69
+ (tmp_path / "good.md").write_text("# ok\n", encoding="utf-8")
70
+ vendor = tmp_path / "vendor"
71
+ vendor.mkdir()
72
+ (vendor / "bad.md").write_text("[x](./missing.md)\n", encoding="utf-8")
73
+ report = validate_markdown_tree(tmp_path)
74
+ assert report.files_checked == 1
75
+ assert report.ok
File without changes
File without changes
File without changes
File without changes