pystdoc 0.2.0__tar.gz → 0.3.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.
- {pystdoc-0.2.0/src/pystdoc.egg-info → pystdoc-0.3.0}/PKG-INFO +2 -2
- {pystdoc-0.2.0 → pystdoc-0.3.0}/README.md +1 -1
- {pystdoc-0.2.0 → pystdoc-0.3.0}/pyproject.toml +1 -1
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/__init__.py +1 -1
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/cli.py +2 -2
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/design_engine.py +17 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/engine.py +152 -52
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/llm_client.py +28 -18
- {pystdoc-0.2.0 → pystdoc-0.3.0/src/pystdoc.egg-info}/PKG-INFO +2 -2
- {pystdoc-0.2.0 → pystdoc-0.3.0}/LICENSE +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/setup.cfg +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/cache.py +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/call_graph.py +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/compilation_db.py +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/db.py +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/doc_writer.py +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/hasher.py +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/parser_clang.py +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/parser_generic.py +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/parser_python.py +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/report_engine.py +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/scanner.py +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/symbols.py +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc.egg-info/SOURCES.txt +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc.egg-info/dependency_links.txt +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc.egg-info/entry_points.txt +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc.egg-info/requires.txt +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc.egg-info/top_level.txt +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/tests/test_designgen.py +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/tests/test_docgen.py +0 -0
- {pystdoc-0.2.0 → pystdoc-0.3.0}/tests/test_reportgen.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pystdoc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Python Structural & Topological Documentation Engine (pystdoc): High-precision, LLM-powered hierarchical codebase and architecture documentation generator.
|
|
5
5
|
Author: tab4moji
|
|
6
6
|
License: MIT
|
|
@@ -120,7 +120,7 @@ designgen --dir ./my_project/
|
|
|
120
120
|
| `--model` | `-m`, `LLM_MODEL` | `gemma4-26b-a4b` | LLM model identifier |
|
|
121
121
|
| `--token` | `--api-key`, `OPENAI_API_KEY` | `None` | API Bearer token |
|
|
122
122
|
| `--context-size` | `--ctx-size` | `16384` | Context window size |
|
|
123
|
-
| `--concurrency` | `-j` | `
|
|
123
|
+
| `--concurrency` | `-j` | `1` | Number of parallel LLM workers |
|
|
124
124
|
| `--force` | `-f` | `false` | Force regenerate all documents ignoring cache |
|
|
125
125
|
| `--compile-commands` | | `None` | Path to `compile_commands.json` |
|
|
126
126
|
|
|
@@ -97,7 +97,7 @@ designgen --dir ./my_project/
|
|
|
97
97
|
| `--model` | `-m`, `LLM_MODEL` | `gemma4-26b-a4b` | LLM model identifier |
|
|
98
98
|
| `--token` | `--api-key`, `OPENAI_API_KEY` | `None` | API Bearer token |
|
|
99
99
|
| `--context-size` | `--ctx-size` | `16384` | Context window size |
|
|
100
|
-
| `--concurrency` | `-j` | `
|
|
100
|
+
| `--concurrency` | `-j` | `1` | Number of parallel LLM workers |
|
|
101
101
|
| `--force` | `-f` | `false` | Force regenerate all documents ignoring cache |
|
|
102
102
|
| `--compile-commands` | | `None` | Path to `compile_commands.json` |
|
|
103
103
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pystdoc"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Python Structural & Topological Documentation Engine (pystdoc): High-precision, LLM-powered hierarchical codebase and architecture documentation generator."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -26,7 +26,7 @@ def docgen_main() -> None:
|
|
|
26
26
|
parser.add_argument("--force", "-f", action="store_true", help="Force regenerate all documents ignoring cache")
|
|
27
27
|
parser.add_argument("--allow-fallback", action="store_true", help="Allow fallback to static template on LLM failure")
|
|
28
28
|
parser.add_argument("--compile-commands", default=None, help="Path to compile_commands.json (auto-detected if omitted)")
|
|
29
|
-
parser.add_argument("--concurrency", "-j", type=int, default=
|
|
29
|
+
parser.add_argument("--concurrency", "-j", type=int, default=1, help="Number of parallel LLM workers (default: 1)")
|
|
30
30
|
|
|
31
31
|
args = parser.parse_args()
|
|
32
32
|
sys.exit(
|
|
@@ -98,7 +98,7 @@ def reportgen_main() -> None:
|
|
|
98
98
|
parser.add_argument("--force", "-f", action="store_true", help="Force regenerate all documents ignoring cache")
|
|
99
99
|
parser.add_argument("--allow-fallback", action="store_true", help="Allow fallback to static template on LLM failure")
|
|
100
100
|
parser.add_argument("--compile-commands", default=None, help="Path to compile_commands.json (auto-detected if omitted)")
|
|
101
|
-
parser.add_argument("--concurrency", "-j", type=int, default=
|
|
101
|
+
parser.add_argument("--concurrency", "-j", type=int, default=1, help="Number of parallel LLM workers (default: 1)")
|
|
102
102
|
parser.add_argument("--skip-docgen", action="store_true", help="Skip docgen step")
|
|
103
103
|
parser.add_argument("--skip-designgen", action="store_true", help="Skip designgen step")
|
|
104
104
|
|
|
@@ -4,6 +4,7 @@ import hashlib
|
|
|
4
4
|
import json
|
|
5
5
|
import re
|
|
6
6
|
import sys
|
|
7
|
+
import time
|
|
7
8
|
from collections import defaultdict
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Any, Callable, Dict, List, Optional, Set
|
|
@@ -197,6 +198,7 @@ def generate_data_models_doc(
|
|
|
197
198
|
write_flushed_text(out_file, cached_content.strip() + "\n")
|
|
198
199
|
return cached_content
|
|
199
200
|
|
|
201
|
+
start_t = time.time()
|
|
200
202
|
reduced_types_summary = hierarchical_reduce_summaries(
|
|
201
203
|
types_raw,
|
|
202
204
|
"Type Definitions",
|
|
@@ -249,6 +251,9 @@ Structure the Markdown as follows:
|
|
|
249
251
|
raise LLMError(f"Failed to generate data models document: {e}") from e
|
|
250
252
|
content = "# Data Structure Design & Data Models\n\n## 1. Design Intent & Core Philosophy\nProvides core data structure definitions.\n"
|
|
251
253
|
|
|
254
|
+
elapsed = time.time() - start_t
|
|
255
|
+
print(f" -> [Done in {elapsed:5.1f}s]: .docgen/design/data_models.md")
|
|
256
|
+
|
|
252
257
|
write_flushed_text(out_file, content.strip() + "\n")
|
|
253
258
|
if db:
|
|
254
259
|
db.save_design_cache(cache_key, input_hash, content)
|
|
@@ -283,6 +288,7 @@ def generate_execution_model_doc(
|
|
|
283
288
|
write_flushed_text(out_file, cached_content.strip() + "\n")
|
|
284
289
|
return cached_content
|
|
285
290
|
|
|
291
|
+
start_t = time.time()
|
|
286
292
|
reduced_funcs_summary = hierarchical_reduce_summaries(
|
|
287
293
|
funcs_raw,
|
|
288
294
|
"Function Call Structures",
|
|
@@ -325,6 +331,9 @@ Structure the Markdown as follows:
|
|
|
325
331
|
raise LLMError(f"Failed to generate execution model document: {e}") from e
|
|
326
332
|
content = "# System Execution Model & Runtime Architecture\n\n## 1. Design Intent & Execution Paradigm\nProvides system execution flow.\n"
|
|
327
333
|
|
|
334
|
+
elapsed = time.time() - start_t
|
|
335
|
+
print(f" -> [Done in {elapsed:5.1f}s]: .docgen/design/execution_model.md")
|
|
336
|
+
|
|
328
337
|
write_flushed_text(out_file, content.strip() + "\n")
|
|
329
338
|
if db:
|
|
330
339
|
db.save_design_cache(cache_key, input_hash, content)
|
|
@@ -359,6 +368,7 @@ def generate_module_docs(
|
|
|
359
368
|
module_summaries[mod_name] = cached_content
|
|
360
369
|
continue
|
|
361
370
|
|
|
371
|
+
start_t = time.time()
|
|
362
372
|
reduced_module_elements = hierarchical_reduce_summaries(
|
|
363
373
|
doc_lines,
|
|
364
374
|
f"Module `{mod_name}` Elements",
|
|
@@ -402,6 +412,9 @@ Structure the Markdown as follows:
|
|
|
402
412
|
raise LLMError(f"Failed to generate module `{mod_name}` document: {e}") from e
|
|
403
413
|
content = f"# Module Design: `{mod_name}`\n\n## 1. Design Intent & Responsibilities\nProvides capabilities for `{mod_name}`.\n"
|
|
404
414
|
|
|
415
|
+
elapsed = time.time() - start_t
|
|
416
|
+
print(f" -> [Done in {elapsed:5.1f}s]: .docgen/design/modules/{mod_name}.md")
|
|
417
|
+
|
|
405
418
|
write_flushed_text(out_file, content.strip() + "\n")
|
|
406
419
|
if db:
|
|
407
420
|
db.save_design_cache(cache_key, input_hash, content)
|
|
@@ -440,6 +453,7 @@ def generate_overview_doc(
|
|
|
440
453
|
write_flushed_text(out_file, cached_content.strip() + "\n")
|
|
441
454
|
return cached_content
|
|
442
455
|
|
|
456
|
+
start_t = time.time()
|
|
443
457
|
reduced_modules_overview = hierarchical_reduce_summaries(
|
|
444
458
|
mod_lines,
|
|
445
459
|
"All Modules Overview",
|
|
@@ -478,6 +492,9 @@ Requirements:
|
|
|
478
492
|
raise LLMError(f"Failed to generate overview document: {e}") from e
|
|
479
493
|
content = "# System Architecture Overview\n\n## 1. Architectural Overview & Design Philosophy\nProvides overall system architectural design.\n"
|
|
480
494
|
|
|
495
|
+
elapsed = time.time() - start_t
|
|
496
|
+
print(f" -> [Done in {elapsed:5.1f}s]: .docgen/design/overview.md")
|
|
497
|
+
|
|
481
498
|
write_flushed_text(out_file, content.strip() + "\n")
|
|
482
499
|
if db:
|
|
483
500
|
db.save_design_cache(cache_key, input_hash, content)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
"""Core engine for docgen: level-by-level parallel LLM analysis and SQLite storage with
|
|
1
|
+
"""Core engine for docgen: level-by-level parallel LLM analysis and SQLite storage with static bypass for enums/constants and real-time progress."""
|
|
2
2
|
|
|
3
3
|
import concurrent.futures
|
|
4
4
|
import fcntl
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
7
|
import threading
|
|
8
|
+
import time
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Dict, List, Optional
|
|
10
11
|
|
|
@@ -45,7 +46,7 @@ class FileLock:
|
|
|
45
46
|
try:
|
|
46
47
|
fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
47
48
|
except BlockingIOError:
|
|
48
|
-
print(f"Error: Another docgen process is currently processing this directory: {self.lock_file}", file=sys.stderr)
|
|
49
|
+
print(f"Error: Another docgen process is currently processing this directory: {self.lock_file}", file=sys.stderr, flush=True)
|
|
49
50
|
sys.exit(1)
|
|
50
51
|
return self
|
|
51
52
|
|
|
@@ -58,6 +59,58 @@ class FileLock:
|
|
|
58
59
|
pass
|
|
59
60
|
|
|
60
61
|
|
|
62
|
+
def generate_static_symbol_doc(sym, lang_norm: str) -> Dict[str, str]:
|
|
63
|
+
"""Generate high-precision documentation for enums, constants, and basic types from static AST metadata."""
|
|
64
|
+
is_ja = lang_norm in ("Japanese", "日本語")
|
|
65
|
+
kind = sym.kind.lower()
|
|
66
|
+
|
|
67
|
+
if "enum" in kind or kind == "enum_constant":
|
|
68
|
+
if is_ja:
|
|
69
|
+
return {
|
|
70
|
+
"purpose": f"状態コードまたは識別子定数 `{sym.name}` を定義する列挙型要素。",
|
|
71
|
+
"inputs": "なし(定数定義)。",
|
|
72
|
+
"outputs": f"`{sym.signature or sym.name}` の定数識別値。",
|
|
73
|
+
"overview": f"システム全体で一貫したステータス管理や条件判定に使用される `{sym.name}` の定義。",
|
|
74
|
+
}
|
|
75
|
+
else:
|
|
76
|
+
return {
|
|
77
|
+
"purpose": f"Defines status code or identifier constant `{sym.name}`.",
|
|
78
|
+
"inputs": "None (constant definition).",
|
|
79
|
+
"outputs": f"Constant identifier value `{sym.signature or sym.name}`.",
|
|
80
|
+
"overview": f"Enumeration definition used for status handling and conditional flow across the system.",
|
|
81
|
+
}
|
|
82
|
+
elif "typedef" in kind or "type" in kind:
|
|
83
|
+
if is_ja:
|
|
84
|
+
return {
|
|
85
|
+
"purpose": f"データ型またはエイリアス `{sym.name}` の定義。",
|
|
86
|
+
"inputs": "なし(型定義)。",
|
|
87
|
+
"outputs": f"`{sym.signature or sym.name}` 型。",
|
|
88
|
+
"overview": f"システム全体で共通利用されるデータ構造または型エイリアス `{sym.name}` の仕様。",
|
|
89
|
+
}
|
|
90
|
+
else:
|
|
91
|
+
return {
|
|
92
|
+
"purpose": f"Defines data type or alias `{sym.name}`.",
|
|
93
|
+
"inputs": "None (type definition).",
|
|
94
|
+
"outputs": f"Type `{sym.signature or sym.name}`.",
|
|
95
|
+
"overview": f"Specification of data structure or type alias `{sym.name}` used across modules.",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if is_ja:
|
|
99
|
+
return {
|
|
100
|
+
"purpose": f"`{sym.name}` ({sym.kind}) の定義。",
|
|
101
|
+
"inputs": "パラメータ定義に従う。",
|
|
102
|
+
"outputs": "結果値または状態遷移。",
|
|
103
|
+
"overview": f"`{sym.name}` の基本仕様。",
|
|
104
|
+
}
|
|
105
|
+
else:
|
|
106
|
+
return {
|
|
107
|
+
"purpose": f"Defines `{sym.name}` ({sym.kind}).",
|
|
108
|
+
"inputs": "According to parameter definitions.",
|
|
109
|
+
"outputs": "Result value or state mutation.",
|
|
110
|
+
"overview": f"Basic specification for `{sym.name}`.",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
61
114
|
def run_docgen(
|
|
62
115
|
target_dir: Path,
|
|
63
116
|
use_llm: bool = True,
|
|
@@ -71,17 +124,17 @@ def run_docgen(
|
|
|
71
124
|
force: bool = False,
|
|
72
125
|
allow_fallback: bool = False,
|
|
73
126
|
compile_commands_path: Optional[str] = None,
|
|
74
|
-
concurrency: int =
|
|
127
|
+
concurrency: int = 1,
|
|
75
128
|
) -> int:
|
|
76
129
|
target_dir = target_dir.resolve()
|
|
77
130
|
norm_lang = normalize_language(language)
|
|
78
131
|
if not target_dir.exists() or not target_dir.is_dir():
|
|
79
|
-
print(f"Error: Specified directory does not exist: {target_dir}", file=sys.stderr)
|
|
132
|
+
print(f"Error: Specified directory does not exist: {target_dir}", file=sys.stderr, flush=True)
|
|
80
133
|
return 1
|
|
81
134
|
|
|
82
135
|
lock_path = target_dir / ".docgen" / ".lock"
|
|
83
136
|
with FileLock(lock_path):
|
|
84
|
-
print(f"=== docgen Started (
|
|
137
|
+
print(f"=== docgen Started (Workers: {concurrency}, Lang: {norm_lang}, SQLite index): {target_dir} ===", flush=True)
|
|
85
138
|
|
|
86
139
|
db_path = target_dir / ".docgen" / "index.db"
|
|
87
140
|
db = DocgenDB(db_path)
|
|
@@ -91,7 +144,7 @@ def run_docgen(
|
|
|
91
144
|
target_dir=target_dir,
|
|
92
145
|
)
|
|
93
146
|
if comp_db.loaded_path:
|
|
94
|
-
print(f"[CompDB] Detected compilation database: {comp_db.loaded_path}")
|
|
147
|
+
print(f"[CompDB] Detected compilation database: {comp_db.loaded_path}", flush=True)
|
|
95
148
|
|
|
96
149
|
llm_client = None
|
|
97
150
|
if use_llm:
|
|
@@ -104,23 +157,22 @@ def run_docgen(
|
|
|
104
157
|
if client.check_availability():
|
|
105
158
|
llm_client = client
|
|
106
159
|
auth_info = " (authenticated)" if client.token else ""
|
|
107
|
-
print(f"[LLM] Connected to server: {client.base_url} (model: {client.model}, context: {client.context_size}{auth_info})")
|
|
160
|
+
print(f"[LLM] Connected to server: {client.base_url} (model: {client.model}, context: {client.context_size}{auth_info})", flush=True)
|
|
108
161
|
else:
|
|
109
162
|
if not allow_fallback:
|
|
110
|
-
print(f"Error: Failed to connect to LLM server ({client.base_url}). Aborting without --allow-fallback.", file=sys.stderr)
|
|
163
|
+
print(f"Error: Failed to connect to LLM server ({client.base_url}). Aborting without --allow-fallback.", file=sys.stderr, flush=True)
|
|
111
164
|
db.close()
|
|
112
165
|
return 1
|
|
113
166
|
else:
|
|
114
|
-
print(f"[LLM Warning] LLM server unreachable ({client.base_url}). Fallback to static templates.")
|
|
167
|
+
print(f"[LLM Warning] LLM server unreachable ({client.base_url}). Fallback to static templates.", flush=True)
|
|
115
168
|
|
|
116
169
|
# 1. Scan files
|
|
117
170
|
matched_files = scan_files(target_dir)
|
|
118
171
|
files_txt = write_files_list(target_dir, matched_files)
|
|
119
|
-
print(f"[1/5] Scanned files: {len(matched_files)} detected -> {files_txt}")
|
|
172
|
+
print(f"[1/5] Scanned files: {len(matched_files)} detected -> {files_txt}", flush=True)
|
|
120
173
|
|
|
121
174
|
# 2. Compute hashes and enumerate all symbols
|
|
122
175
|
file_hashes: Dict[Path, str] = {}
|
|
123
|
-
file_changed: Dict[Path, bool] = {}
|
|
124
176
|
file_symbols: Dict[Path, list] = {}
|
|
125
177
|
all_symbol_nodes: List[SymbolNode] = []
|
|
126
178
|
|
|
@@ -128,8 +180,7 @@ def run_docgen(
|
|
|
128
180
|
full_path = target_dir / rel_path
|
|
129
181
|
h = compute_file_hash(full_path)
|
|
130
182
|
file_hashes[rel_path] = h
|
|
131
|
-
|
|
132
|
-
file_changed[rel_path] = is_changed
|
|
183
|
+
db.update_file_hash(rel_path.as_posix(), h)
|
|
133
184
|
|
|
134
185
|
symbols = extract_symbols(full_path, comp_db=comp_db)
|
|
135
186
|
file_symbols[rel_path] = symbols
|
|
@@ -138,7 +189,7 @@ def run_docgen(
|
|
|
138
189
|
all_symbol_nodes.extend(nodes)
|
|
139
190
|
|
|
140
191
|
total_symbols_count = len(all_symbol_nodes)
|
|
141
|
-
print(f"[2/5] Enumerated symbols (with FQDN): {total_symbols_count} symbols total")
|
|
192
|
+
print(f"[2/5] Enumerated symbols (with FQDN): {total_symbols_count} symbols total", flush=True)
|
|
142
193
|
|
|
143
194
|
link_variables_to_functions(all_symbol_nodes)
|
|
144
195
|
|
|
@@ -148,9 +199,10 @@ def run_docgen(
|
|
|
148
199
|
resolved_symbols_map: Dict[str, SymbolNode] = {node.unique_id: node for node in all_symbol_nodes}
|
|
149
200
|
|
|
150
201
|
print_lock = threading.Lock()
|
|
151
|
-
|
|
202
|
+
started_counter = [0]
|
|
203
|
+
completed_counter = [0]
|
|
152
204
|
|
|
153
|
-
print(f"[3/5] Pass 1: Level-by-Level
|
|
205
|
+
print(f"[3/5] Pass 1: Level-by-Level analysis (Total {total_levels} levels, Workers: {concurrency})...", flush=True)
|
|
154
206
|
|
|
155
207
|
def process_single_node(node: SymbolNode) -> None:
|
|
156
208
|
sym = node.symbol
|
|
@@ -169,13 +221,19 @@ def run_docgen(
|
|
|
169
221
|
is_sym_changed = db.update_symbol_hash(node.unique_id, rel_path.as_posix(), sym_hash)
|
|
170
222
|
db.save_symbol_metadata(node.unique_id, sym, rel_path.as_posix())
|
|
171
223
|
|
|
172
|
-
|
|
224
|
+
# Cache check with language key
|
|
225
|
+
cache_key = f"{node.unique_id}::{norm_lang}"
|
|
226
|
+
cached_data = db.load_symbol_cache(cache_key) or db.load_symbol_cache(node.unique_id) if not force and not is_sym_changed else None
|
|
173
227
|
|
|
174
228
|
with print_lock:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
percent = (
|
|
178
|
-
progress_str = f"[{
|
|
229
|
+
started_counter[0] += 1
|
|
230
|
+
curr_idx = started_counter[0]
|
|
231
|
+
percent = (curr_idx / total_symbols_count * 100.0) if total_symbols_count > 0 else 100.0
|
|
232
|
+
progress_str = f"[{curr_idx}/{total_symbols_count} ({percent:5.1f}%)]"
|
|
233
|
+
|
|
234
|
+
# Check if this symbol should be statically resolved without LLM (enum, enum_constant, typedef)
|
|
235
|
+
sym_kind_lower = sym.kind.lower()
|
|
236
|
+
is_static_bypass = ("enum" in sym_kind_lower or sym_kind_lower in ("enum_constant", "typedef"))
|
|
179
237
|
|
|
180
238
|
if cached_data:
|
|
181
239
|
sym.purpose = cached_data.get("purpose", sym.purpose)
|
|
@@ -184,16 +242,41 @@ def run_docgen(
|
|
|
184
242
|
sym.overview = cached_data.get("overview", sym.overview)
|
|
185
243
|
sym.top_down_context = cached_data.get("top_down_context", sym.top_down_context)
|
|
186
244
|
with print_lock:
|
|
187
|
-
|
|
188
|
-
|
|
245
|
+
completed_counter[0] += 1
|
|
246
|
+
print(f" {progress_str} [Cached]: {node.unique_id} (Lvl {node.dag_level})", flush=True)
|
|
247
|
+
elif is_static_bypass:
|
|
248
|
+
# Fast AST-based static generation (0s, prevents LLM deadlock on enums)
|
|
249
|
+
static_doc = generate_static_symbol_doc(sym, norm_lang)
|
|
250
|
+
sym.purpose = static_doc["purpose"]
|
|
251
|
+
sym.inputs_note = static_doc["inputs"]
|
|
252
|
+
sym.outputs_note = static_doc["outputs"]
|
|
253
|
+
sym.overview = static_doc["overview"]
|
|
254
|
+
|
|
255
|
+
save_payload = {
|
|
256
|
+
"purpose": sym.purpose,
|
|
257
|
+
"inputs_note": sym.inputs_note,
|
|
258
|
+
"outputs_note": sym.outputs_note,
|
|
259
|
+
"overview": sym.overview,
|
|
260
|
+
"top_down_context": sym.top_down_context,
|
|
261
|
+
}
|
|
262
|
+
db.save_symbol_cache(cache_key, save_payload)
|
|
263
|
+
db.save_symbol_cache(node.unique_id, save_payload)
|
|
264
|
+
|
|
265
|
+
with print_lock:
|
|
266
|
+
completed_counter[0] += 1
|
|
267
|
+
done_count = completed_counter[0]
|
|
268
|
+
done_pct = (done_count / total_symbols_count * 100.0) if total_symbols_count > 0 else 100.0
|
|
269
|
+
print(f" {progress_str} [Static Spec]: {node.unique_id} (Lvl {node.dag_level})", flush=True)
|
|
270
|
+
elif llm_client:
|
|
189
271
|
callee_context = build_callee_context_summary(node, resolved_symbols_map)
|
|
190
272
|
dep_info = f" (Callees: {len(node.direct_callee_ids)})" if node.direct_callee_ids else ""
|
|
191
273
|
with print_lock:
|
|
192
|
-
print(f" {progress_str} [LLM
|
|
274
|
+
print(f" {progress_str} [LLM Requesting...]: {node.unique_id} (Lvl {node.dag_level}){dep_info}", flush=True)
|
|
193
275
|
|
|
194
276
|
param_strs = [f"{p.name} ({p.type_hint})" if p.type_hint else p.name for p in sym.parameters]
|
|
195
277
|
lang = get_code_language(full_path.suffix)
|
|
196
278
|
|
|
279
|
+
start_sym_time = time.time()
|
|
197
280
|
explanation = llm_client.explain_symbol(
|
|
198
281
|
name=sym.name,
|
|
199
282
|
kind=sym.kind,
|
|
@@ -207,6 +290,7 @@ def run_docgen(
|
|
|
207
290
|
language=norm_lang,
|
|
208
291
|
allow_fallback=allow_fallback,
|
|
209
292
|
)
|
|
293
|
+
sym_elapsed = time.time() - start_sym_time
|
|
210
294
|
|
|
211
295
|
if explanation.get("purpose"):
|
|
212
296
|
sym.purpose = explanation["purpose"]
|
|
@@ -217,19 +301,25 @@ def run_docgen(
|
|
|
217
301
|
if explanation.get("overview"):
|
|
218
302
|
sym.overview = explanation["overview"]
|
|
219
303
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
304
|
+
save_payload = {
|
|
305
|
+
"purpose": sym.purpose,
|
|
306
|
+
"inputs_note": sym.inputs_note,
|
|
307
|
+
"outputs_note": sym.outputs_note,
|
|
308
|
+
"overview": sym.overview,
|
|
309
|
+
"top_down_context": sym.top_down_context,
|
|
310
|
+
}
|
|
311
|
+
db.save_symbol_cache(cache_key, save_payload)
|
|
312
|
+
db.save_symbol_cache(node.unique_id, save_payload)
|
|
313
|
+
|
|
314
|
+
with print_lock:
|
|
315
|
+
completed_counter[0] += 1
|
|
316
|
+
done_count = completed_counter[0]
|
|
317
|
+
done_pct = (done_count / total_symbols_count * 100.0) if total_symbols_count > 0 else 100.0
|
|
318
|
+
print(f" -> [Done in {sym_elapsed:5.1f}s] ({done_count}/{total_symbols_count} - {done_pct:5.1f}%): {node.unique_id}", flush=True)
|
|
230
319
|
else:
|
|
231
320
|
with print_lock:
|
|
232
|
-
|
|
321
|
+
completed_counter[0] += 1
|
|
322
|
+
print(f" {progress_str} [Static Info]: {node.unique_id}", flush=True)
|
|
233
323
|
|
|
234
324
|
write_single_symbol_doc(target_dir, rel_path, sym, prefix_name=prefix_in_unique_id, language=norm_lang)
|
|
235
325
|
|
|
@@ -241,17 +331,18 @@ def run_docgen(
|
|
|
241
331
|
try:
|
|
242
332
|
f.result()
|
|
243
333
|
except Exception as e:
|
|
244
|
-
print(f"\nError: {e}", file=sys.stderr)
|
|
334
|
+
print(f"\nError: {e}", file=sys.stderr, flush=True)
|
|
245
335
|
db.close()
|
|
246
336
|
return 1
|
|
247
337
|
|
|
248
338
|
# 4. Pass 2: Top-down variable refinement
|
|
249
339
|
var_nodes = [n for n in all_symbol_nodes if get_kind_prefix(n.symbol.kind) == "var"]
|
|
250
340
|
total_var_count = len(var_nodes)
|
|
251
|
-
print(f"[4/5] Pass 2: Top-down variable contextual refinement ({total_var_count} variables)...")
|
|
341
|
+
print(f"[4/5] Pass 2: Top-down variable contextual refinement ({total_var_count} variables)...", flush=True)
|
|
252
342
|
|
|
253
343
|
fn_nodes = [n for n in all_symbol_nodes if get_kind_prefix(n.symbol.kind) == "fn"]
|
|
254
344
|
var_counter = [0]
|
|
345
|
+
var_done_counter = [0]
|
|
255
346
|
|
|
256
347
|
def process_var_node(v_node: SymbolNode) -> None:
|
|
257
348
|
v_sym = v_node.symbol
|
|
@@ -287,11 +378,12 @@ def run_docgen(
|
|
|
287
378
|
if llm_client and parent_funcs and (force or not v_sym.top_down_context):
|
|
288
379
|
funcs_names = ", ".join(f["name"] for f in parent_funcs[:2])
|
|
289
380
|
with print_lock:
|
|
290
|
-
print(f" {v_progress} [Top-down Variable
|
|
381
|
+
print(f" {v_progress} [Top-down Variable Updating...]: {v_node.unique_id} (Referenced by: {funcs_names})", flush=True)
|
|
291
382
|
|
|
292
383
|
v_snippet = get_code_snippet(v_full, v_sym.line_start, v_sym.line_end)
|
|
293
384
|
lang = get_code_language(v_full.suffix)
|
|
294
385
|
|
|
386
|
+
start_var_time = time.time()
|
|
295
387
|
top_down_res = llm_client.refine_variable_top_down(
|
|
296
388
|
var_name=v_sym.name,
|
|
297
389
|
var_kind=v_sym.kind,
|
|
@@ -302,6 +394,7 @@ def run_docgen(
|
|
|
302
394
|
language=norm_lang,
|
|
303
395
|
allow_fallback=allow_fallback,
|
|
304
396
|
)
|
|
397
|
+
var_elapsed = time.time() - start_var_time
|
|
305
398
|
|
|
306
399
|
lines = []
|
|
307
400
|
if top_down_res.get("significance"):
|
|
@@ -313,16 +406,22 @@ def run_docgen(
|
|
|
313
406
|
|
|
314
407
|
v_sym.top_down_context = "\n".join(lines)
|
|
315
408
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
)
|
|
409
|
+
save_payload = {
|
|
410
|
+
"purpose": v_sym.purpose,
|
|
411
|
+
"inputs_note": v_sym.inputs_note,
|
|
412
|
+
"outputs_note": v_sym.outputs_note,
|
|
413
|
+
"overview": v_sym.overview,
|
|
414
|
+
"top_down_context": v_sym.top_down_context,
|
|
415
|
+
}
|
|
416
|
+
cache_key = f"{v_node.unique_id}::{norm_lang}"
|
|
417
|
+
db.save_symbol_cache(cache_key, save_payload)
|
|
418
|
+
db.save_symbol_cache(v_node.unique_id, save_payload)
|
|
419
|
+
|
|
420
|
+
with print_lock:
|
|
421
|
+
var_done_counter[0] += 1
|
|
422
|
+
vd = var_done_counter[0]
|
|
423
|
+
v_pct = (vd / total_var_count * 100.0) if total_var_count > 0 else 100.0
|
|
424
|
+
print(f" -> [Done in {var_elapsed:5.1f}s] ({vd}/{total_var_count} - {v_pct:5.1f}%): {v_node.unique_id}", flush=True)
|
|
326
425
|
|
|
327
426
|
prefix_in_unique_id = ""
|
|
328
427
|
if "::" in v_node.unique_id:
|
|
@@ -333,7 +432,8 @@ def run_docgen(
|
|
|
333
432
|
write_single_symbol_doc(target_dir, v_rel, v_sym, prefix_name=prefix_in_unique_id, language=norm_lang)
|
|
334
433
|
else:
|
|
335
434
|
with print_lock:
|
|
336
|
-
|
|
435
|
+
var_done_counter[0] += 1
|
|
436
|
+
print(f" {v_progress} [Retained Variable Context]: {v_node.unique_id}", flush=True)
|
|
337
437
|
|
|
338
438
|
if var_nodes:
|
|
339
439
|
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
|
|
@@ -342,7 +442,7 @@ def run_docgen(
|
|
|
342
442
|
try:
|
|
343
443
|
f.result()
|
|
344
444
|
except Exception as e:
|
|
345
|
-
print(f"\nError: {e}", file=sys.stderr)
|
|
445
|
+
print(f"\nError: {e}", file=sys.stderr, flush=True)
|
|
346
446
|
db.close()
|
|
347
447
|
return 1
|
|
348
448
|
|
|
@@ -357,6 +457,6 @@ def run_docgen(
|
|
|
357
457
|
total_individual_docs += len(ind_docs)
|
|
358
458
|
|
|
359
459
|
db.close()
|
|
360
|
-
print(f"[5/5] Recorded documents: {len(matched_files)} files, {total_individual_docs} individual symbol docs")
|
|
361
|
-
print("=== docgen Finished ===")
|
|
460
|
+
print(f"[5/5] Recorded documents: {len(matched_files)} files, {total_individual_docs} individual symbol docs", flush=True)
|
|
461
|
+
print("=== docgen Finished ===", flush=True)
|
|
362
462
|
return 0
|
|
@@ -42,7 +42,7 @@ class LLMClient:
|
|
|
42
42
|
token: Optional[str] = None,
|
|
43
43
|
api_key: Optional[str] = None,
|
|
44
44
|
context_size: int = 16384,
|
|
45
|
-
timeout: int =
|
|
45
|
+
timeout: int = 60,
|
|
46
46
|
):
|
|
47
47
|
raw_host = host or base_url or os.environ.get("LLM_HOST") or os.environ.get("OPENAI_BASE_URL")
|
|
48
48
|
self.user_specified_host = raw_host is not None
|
|
@@ -50,7 +50,7 @@ class LLMClient:
|
|
|
50
50
|
self.model = model or os.environ.get("LLM_MODEL", "gemma4-26b-a4b")
|
|
51
51
|
self.token = token or api_key or os.environ.get("LLM_TOKEN") or os.environ.get("OPENAI_API_KEY")
|
|
52
52
|
self.context_size = context_size or int(os.environ.get("LLM_CONTEXT_SIZE", "16384"))
|
|
53
|
-
self.
|
|
53
|
+
self.default_timeout = timeout
|
|
54
54
|
|
|
55
55
|
self.ensure_reachable()
|
|
56
56
|
|
|
@@ -118,14 +118,23 @@ class LLMClient:
|
|
|
118
118
|
"""Verify server connectivity."""
|
|
119
119
|
return self.ensure_reachable()
|
|
120
120
|
|
|
121
|
-
def chat_completion(
|
|
121
|
+
def chat_completion(
|
|
122
|
+
self,
|
|
123
|
+
messages: List[Dict[str, str]],
|
|
124
|
+
json_mode: bool = False,
|
|
125
|
+
max_tokens: int = 1024,
|
|
126
|
+
timeout: Optional[int] = None,
|
|
127
|
+
max_retries: int = 2,
|
|
128
|
+
) -> str:
|
|
122
129
|
"""Execute chat completion request with retry loop, token authentication, and context size limits."""
|
|
123
130
|
url = f"{self.base_url}/chat/completions"
|
|
131
|
+
req_timeout = timeout or self.default_timeout
|
|
132
|
+
|
|
124
133
|
payload: Dict[str, Any] = {
|
|
125
134
|
"model": self.model,
|
|
126
135
|
"messages": messages,
|
|
127
136
|
"temperature": 0.2,
|
|
128
|
-
"max_tokens":
|
|
137
|
+
"max_tokens": max_tokens,
|
|
129
138
|
"options": {
|
|
130
139
|
"num_ctx": self.context_size,
|
|
131
140
|
},
|
|
@@ -142,7 +151,7 @@ class LLMClient:
|
|
|
142
151
|
req.add_header("Authorization", f"Bearer {self.token}")
|
|
143
152
|
|
|
144
153
|
try:
|
|
145
|
-
with urllib.request.urlopen(req, timeout=
|
|
154
|
+
with urllib.request.urlopen(req, timeout=req_timeout) as resp:
|
|
146
155
|
res_data = json.loads(resp.read().decode("utf-8"))
|
|
147
156
|
return res_data["choices"][0]["message"]["content"]
|
|
148
157
|
except urllib.error.HTTPError as he:
|
|
@@ -153,16 +162,15 @@ class LLMClient:
|
|
|
153
162
|
pass
|
|
154
163
|
detail = f": {err_body}" if err_body else ""
|
|
155
164
|
last_error = f"HTTP Error {he.code}: {he.reason}{detail}"
|
|
156
|
-
# If 404 (e.g. model not found), don't retry in vain
|
|
157
165
|
if he.code == 404:
|
|
158
166
|
break
|
|
159
167
|
if attempt < max_retries:
|
|
160
|
-
time.sleep(1.
|
|
168
|
+
time.sleep(1.0 * attempt)
|
|
161
169
|
continue
|
|
162
170
|
except Exception as e:
|
|
163
171
|
last_error = str(e)
|
|
164
172
|
if attempt < max_retries:
|
|
165
|
-
time.sleep(1.
|
|
173
|
+
time.sleep(1.0 * attempt)
|
|
166
174
|
continue
|
|
167
175
|
|
|
168
176
|
raise LLMError(f"LLM API call failed ({url}, model={self.model}): {last_error}")
|
|
@@ -181,9 +189,10 @@ class LLMClient:
|
|
|
181
189
|
language: str = "English",
|
|
182
190
|
allow_fallback: bool = False,
|
|
183
191
|
) -> Dict[str, str]:
|
|
184
|
-
"""Generate explanations for a symbol in JSON format in the requested natural language."""
|
|
185
|
-
prompt = f"""Please analyze the following {lang} symbol `{name}` ({kind}) and output
|
|
192
|
+
"""Generate concise explanations for a symbol in JSON format in the requested natural language."""
|
|
193
|
+
prompt = f"""Please analyze the following {lang} symbol `{name}` ({kind}) and output concise design intent and specifications in JSON format.
|
|
186
194
|
Output Language: {language} (Write all text values in {language}).
|
|
195
|
+
Be concise and avoid repetition.
|
|
187
196
|
|
|
188
197
|
### Signature / Type:
|
|
189
198
|
`{signature}`
|
|
@@ -204,12 +213,12 @@ Output Language: {language} (Write all text values in {language}).
|
|
|
204
213
|
{code}
|
|
205
214
|
```
|
|
206
215
|
|
|
207
|
-
Return ONLY a valid JSON object
|
|
216
|
+
Return ONLY a valid JSON object (all string values in {language}):
|
|
208
217
|
{{
|
|
209
|
-
"purpose": "Core design intent and
|
|
210
|
-
"inputs": "
|
|
211
|
-
"outputs": "
|
|
212
|
-
"overview": "
|
|
218
|
+
"purpose": "Core design intent and purpose (1-2 concise sentences)",
|
|
219
|
+
"inputs": "Input parameters description (or None)",
|
|
220
|
+
"outputs": "Return value or side effects description",
|
|
221
|
+
"overview": "Concise summary of implementation and role (1-3 sentences)"
|
|
213
222
|
}}
|
|
214
223
|
"""
|
|
215
224
|
messages = [
|
|
@@ -218,7 +227,7 @@ Return ONLY a valid JSON object matching this schema (all string values in {lang
|
|
|
218
227
|
]
|
|
219
228
|
|
|
220
229
|
try:
|
|
221
|
-
raw_res = self.chat_completion(messages, json_mode=True)
|
|
230
|
+
raw_res = self.chat_completion(messages, json_mode=True, max_tokens=768, timeout=60)
|
|
222
231
|
m = re.search(r"\{[\s\S]*\}", raw_res)
|
|
223
232
|
if m:
|
|
224
233
|
return json.loads(m.group(0))
|
|
@@ -253,6 +262,7 @@ Return ONLY a valid JSON object matching this schema (all string values in {lang
|
|
|
253
262
|
|
|
254
263
|
prompt = f"""Please analyze the following {lang} variable/field `{var_name}` in the context of the caller/parent functions that reference it.
|
|
255
264
|
Output Language: {language} (Write all text in {language}).
|
|
265
|
+
Be concise and avoid repetition.
|
|
256
266
|
|
|
257
267
|
### Variable: `{var_name}` ({var_kind})
|
|
258
268
|
- Signature/Type: `{var_signature}`
|
|
@@ -260,7 +270,7 @@ Output Language: {language} (Write all text in {language}).
|
|
|
260
270
|
### Referencing Caller Functions:
|
|
261
271
|
{chr(10).join(context_lines)}
|
|
262
272
|
|
|
263
|
-
Return ONLY a valid JSON object
|
|
273
|
+
Return ONLY a valid JSON object (all string values in {language}):
|
|
264
274
|
{{
|
|
265
275
|
"significance": "Essential significance and design rationale in context of callers (1-2 sentences)",
|
|
266
276
|
"usage_scenario": "Data passing lifecycle and state management role across parent functions (1-2 sentences)",
|
|
@@ -273,7 +283,7 @@ Return ONLY a valid JSON object matching this schema (all string values in {lang
|
|
|
273
283
|
]
|
|
274
284
|
|
|
275
285
|
try:
|
|
276
|
-
raw_res = self.chat_completion(messages, json_mode=True)
|
|
286
|
+
raw_res = self.chat_completion(messages, json_mode=True, max_tokens=768, timeout=60)
|
|
277
287
|
m = re.search(r"\{[\s\S]*\}", raw_res)
|
|
278
288
|
if m:
|
|
279
289
|
return json.loads(m.group(0))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pystdoc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Python Structural & Topological Documentation Engine (pystdoc): High-precision, LLM-powered hierarchical codebase and architecture documentation generator.
|
|
5
5
|
Author: tab4moji
|
|
6
6
|
License: MIT
|
|
@@ -120,7 +120,7 @@ designgen --dir ./my_project/
|
|
|
120
120
|
| `--model` | `-m`, `LLM_MODEL` | `gemma4-26b-a4b` | LLM model identifier |
|
|
121
121
|
| `--token` | `--api-key`, `OPENAI_API_KEY` | `None` | API Bearer token |
|
|
122
122
|
| `--context-size` | `--ctx-size` | `16384` | Context window size |
|
|
123
|
-
| `--concurrency` | `-j` | `
|
|
123
|
+
| `--concurrency` | `-j` | `1` | Number of parallel LLM workers |
|
|
124
124
|
| `--force` | `-f` | `false` | Force regenerate all documents ignoring cache |
|
|
125
125
|
| `--compile-commands` | | `None` | Path to `compile_commands.json` |
|
|
126
126
|
|
|
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
|