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.
Files changed (31) hide show
  1. {pystdoc-0.2.0/src/pystdoc.egg-info → pystdoc-0.3.0}/PKG-INFO +2 -2
  2. {pystdoc-0.2.0 → pystdoc-0.3.0}/README.md +1 -1
  3. {pystdoc-0.2.0 → pystdoc-0.3.0}/pyproject.toml +1 -1
  4. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/__init__.py +1 -1
  5. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/cli.py +2 -2
  6. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/design_engine.py +17 -0
  7. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/engine.py +152 -52
  8. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/llm_client.py +28 -18
  9. {pystdoc-0.2.0 → pystdoc-0.3.0/src/pystdoc.egg-info}/PKG-INFO +2 -2
  10. {pystdoc-0.2.0 → pystdoc-0.3.0}/LICENSE +0 -0
  11. {pystdoc-0.2.0 → pystdoc-0.3.0}/setup.cfg +0 -0
  12. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/cache.py +0 -0
  13. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/call_graph.py +0 -0
  14. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/compilation_db.py +0 -0
  15. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/db.py +0 -0
  16. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/doc_writer.py +0 -0
  17. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/hasher.py +0 -0
  18. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/parser_clang.py +0 -0
  19. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/parser_generic.py +0 -0
  20. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/parser_python.py +0 -0
  21. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/report_engine.py +0 -0
  22. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/scanner.py +0 -0
  23. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc/symbols.py +0 -0
  24. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc.egg-info/SOURCES.txt +0 -0
  25. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc.egg-info/dependency_links.txt +0 -0
  26. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc.egg-info/entry_points.txt +0 -0
  27. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc.egg-info/requires.txt +0 -0
  28. {pystdoc-0.2.0 → pystdoc-0.3.0}/src/pystdoc.egg-info/top_level.txt +0 -0
  29. {pystdoc-0.2.0 → pystdoc-0.3.0}/tests/test_designgen.py +0 -0
  30. {pystdoc-0.2.0 → pystdoc-0.3.0}/tests/test_docgen.py +0 -0
  31. {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.2.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` | `4` | Number of parallel LLM workers |
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` | `4` | Number of parallel LLM workers |
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.2.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 = [
@@ -1,3 +1,3 @@
1
1
  """pystdoc: Python Structural & Topological Documentation Engine."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
@@ -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=4, help="Number of parallel LLM workers (default: 4)")
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=4, help="Number of parallel LLM workers (default: 4)")
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 standard LLM options and language support."""
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 = 4,
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 (Parallel: {concurrency} workers, Lang: {norm_lang}, SQLite index): {target_dir} ===")
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
- is_changed = db.update_file_hash(rel_path.as_posix(), h)
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
- processed_counter = [0]
202
+ started_counter = [0]
203
+ completed_counter = [0]
152
204
 
153
- print(f"[3/5] Pass 1: Level-by-Level parallel analysis (Total {total_levels} levels, Workers: {concurrency})...")
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
- cached_data = db.load_symbol_cache(node.unique_id) if not force and not is_sym_changed else None
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
- processed_counter[0] += 1
176
- curr_count = processed_counter[0]
177
- percent = (curr_count / total_symbols_count * 100.0) if total_symbols_count > 0 else 100.0
178
- progress_str = f"[{curr_count}/{total_symbols_count} ({percent:5.1f}%)]"
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
- print(f" {progress_str} [Cached]: {node.unique_id} (Lvl {node.dag_level})")
188
- elif llm_client and (is_sym_changed or force or not cached_data):
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 Processing]: {node.unique_id} (Lvl {node.dag_level}){dep_info}")
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
- db.save_symbol_cache(
221
- node.unique_id,
222
- {
223
- "purpose": sym.purpose,
224
- "inputs_note": sym.inputs_note,
225
- "outputs_note": sym.outputs_note,
226
- "overview": sym.overview,
227
- "top_down_context": sym.top_down_context,
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
- print(f" {progress_str} [Static Info]: {node.unique_id}")
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 Update]: {v_node.unique_id} (Referenced by: {funcs_names})")
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
- db.save_symbol_cache(
317
- v_node.unique_id,
318
- {
319
- "purpose": v_sym.purpose,
320
- "inputs_note": v_sym.inputs_note,
321
- "outputs_note": v_sym.outputs_note,
322
- "overview": v_sym.overview,
323
- "top_down_context": v_sym.top_down_context,
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
- print(f" {v_progress} [Retained Variable Context]: {v_node.unique_id}")
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 = 180,
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.timeout = timeout
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(self, messages: List[Dict[str, str]], json_mode: bool = False, max_retries: int = 3) -> str:
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": 4096,
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=self.timeout) as resp:
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.5 * attempt)
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.5 * attempt)
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 its design intent and technical specifications in JSON format.
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 matching this schema (all string values in {language}):
216
+ Return ONLY a valid JSON object (all string values in {language}):
208
217
  {{
209
- "purpose": "Core design intent and fundamental purpose of this symbol (1-2 sentences)",
210
- "inputs": "Meaning, constraints, or pre-conditions of input arguments",
211
- "outputs": "Meaning of return value or side effects / state mutations",
212
- "overview": "Clear human-readable summary of the implementation and lifecycle (2-4 sentences)"
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 matching this schema (all string values in {language}):
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.2.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` | `4` | Number of parallel LLM workers |
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