pystdoc 0.2.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {pystdoc-0.2.0/src/pystdoc.egg-info → pystdoc-0.4.0}/PKG-INFO +2 -2
  2. {pystdoc-0.2.0 → pystdoc-0.4.0}/README.md +1 -1
  3. {pystdoc-0.2.0 → pystdoc-0.4.0}/pyproject.toml +1 -1
  4. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/__init__.py +1 -1
  5. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/cli.py +2 -2
  6. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/design_engine.py +17 -0
  7. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/doc_writer.py +16 -8
  8. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/engine.py +152 -52
  9. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/llm_client.py +108 -36
  10. {pystdoc-0.2.0 → pystdoc-0.4.0/src/pystdoc.egg-info}/PKG-INFO +2 -2
  11. {pystdoc-0.2.0 → pystdoc-0.4.0}/LICENSE +0 -0
  12. {pystdoc-0.2.0 → pystdoc-0.4.0}/setup.cfg +0 -0
  13. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/cache.py +0 -0
  14. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/call_graph.py +0 -0
  15. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/compilation_db.py +0 -0
  16. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/db.py +0 -0
  17. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/hasher.py +0 -0
  18. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/parser_clang.py +0 -0
  19. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/parser_generic.py +0 -0
  20. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/parser_python.py +0 -0
  21. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/report_engine.py +0 -0
  22. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/scanner.py +0 -0
  23. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc/symbols.py +0 -0
  24. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc.egg-info/SOURCES.txt +0 -0
  25. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc.egg-info/dependency_links.txt +0 -0
  26. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc.egg-info/entry_points.txt +0 -0
  27. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc.egg-info/requires.txt +0 -0
  28. {pystdoc-0.2.0 → pystdoc-0.4.0}/src/pystdoc.egg-info/top_level.txt +0 -0
  29. {pystdoc-0.2.0 → pystdoc-0.4.0}/tests/test_designgen.py +0 -0
  30. {pystdoc-0.2.0 → pystdoc-0.4.0}/tests/test_docgen.py +0 -0
  31. {pystdoc-0.2.0 → pystdoc-0.4.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.4.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.4.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.4.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)
@@ -14,11 +14,11 @@ from pystdoc.symbols import Symbol, get_kind_prefix
14
14
 
15
15
 
16
16
  def normalize_language(lang: Optional[str]) -> str:
17
- """Normalize language string into English representation (e.g. 'Japanese', 'English')."""
17
+ """Normalize language string into standard representation (e.g. 'Japanese', 'English')."""
18
18
  if not lang:
19
19
  return "English"
20
20
  l_lower = lang.strip().lower()
21
- if l_lower in ("japanese", "ja", "jp"):
21
+ if l_lower in ("japanese", "ja", "jp", "日本語"):
22
22
  return "Japanese"
23
23
  elif l_lower in ("english", "en"):
24
24
  return "English"
@@ -78,6 +78,9 @@ def format_symbol_section(
78
78
  language: str = "English",
79
79
  ) -> str:
80
80
  """Format a symbol's documentation into standardized Markdown string."""
81
+ norm_lang = normalize_language(language)
82
+ is_ja = norm_lang == "Japanese"
83
+
81
84
  h_prefix = "#" * level
82
85
  kind_display = sym.kind
83
86
  if sym.fqdn:
@@ -86,11 +89,13 @@ def format_symbol_section(
86
89
  snippet = get_code_snippet(file_path, sym.line_start, sym.line_end)
87
90
  lang_id = get_code_language(file_path.suffix)
88
91
 
92
+ default_purpose = f"`{sym.name}` の処理を実行する。" if is_ja else f"Executes `{sym.name}` operations."
93
+
89
94
  lines = [
90
95
  f"{h_prefix} {sym.kind.capitalize()} Documentation: `{sym.name}`",
91
96
  "",
92
97
  "## 1. Design Intent & Purpose",
93
- sym.purpose or f"Executes `{sym.name}` operations.",
98
+ sym.purpose or default_purpose,
94
99
  "",
95
100
  "## 2. Basic Information",
96
101
  f"- **Name**: `{sym.name}`",
@@ -132,7 +137,7 @@ def format_symbol_section(
132
137
  if sym.children:
133
138
  lines.append("")
134
139
  for child in sym.children:
135
- lines.append(format_symbol_section(child, file_path, level=level + 1, language=language))
140
+ lines.append(format_symbol_section(child, file_path, level=level + 1, language=norm_lang))
136
141
 
137
142
  return "\n".join(lines)
138
143
 
@@ -145,6 +150,7 @@ def write_single_symbol_doc(
145
150
  language: str = "English",
146
151
  ) -> Path:
147
152
  """Write an individual symbol document with immediate flush."""
153
+ norm_lang = normalize_language(language)
148
154
  docgen_dir = target_dir / ".docgen" / "documents"
149
155
  docgen_dir.mkdir(parents=True, exist_ok=True)
150
156
 
@@ -153,7 +159,7 @@ def write_single_symbol_doc(
153
159
  out_file = docgen_dir / f"{rel_path.as_posix()}.{k_prefix}.{sym_id}.md"
154
160
 
155
161
  full_path = target_dir / rel_path
156
- content = format_symbol_section(symbol, full_path, level=1, language=language)
162
+ content = format_symbol_section(symbol, full_path, level=1, language=norm_lang)
157
163
 
158
164
  write_flushed_text(out_file, content + "\n")
159
165
  return out_file
@@ -167,10 +173,11 @@ def write_individual_symbol_docs(
167
173
  language: str = "English",
168
174
  ) -> List[Path]:
169
175
  """Recursively write documentation files for each individual symbol with immediate flush."""
176
+ norm_lang = normalize_language(language)
170
177
  created_files: List[Path] = []
171
178
 
172
179
  for sym in symbols:
173
- out_file = write_single_symbol_doc(target_dir, rel_path, sym, prefix_name=prefix_name, language=language)
180
+ out_file = write_single_symbol_doc(target_dir, rel_path, sym, prefix_name=prefix_name, language=norm_lang)
174
181
  created_files.append(out_file)
175
182
 
176
183
  if sym.children:
@@ -180,7 +187,7 @@ def write_individual_symbol_docs(
180
187
  rel_path,
181
188
  sym.children,
182
189
  prefix_name=child_prefix,
183
- language=language,
190
+ language=norm_lang,
184
191
  )
185
192
  created_files.extend(child_files)
186
193
 
@@ -195,6 +202,7 @@ def write_symbol_doc(
195
202
  language: str = "English",
196
203
  ) -> Path:
197
204
  """Write comprehensive file documentation with immediate flush."""
205
+ norm_lang = normalize_language(language)
198
206
  docgen_dir = target_dir / ".docgen" / "documents"
199
207
  docgen_dir.mkdir(parents=True, exist_ok=True)
200
208
 
@@ -215,7 +223,7 @@ def write_symbol_doc(
215
223
 
216
224
  lines.append("")
217
225
  for sym in symbols:
218
- lines.append(format_symbol_section(sym, full_path, level=2, language=language))
226
+ lines.append(format_symbol_section(sym, full_path, level=2, language=norm_lang))
219
227
  lines.append("")
220
228
 
221
229
  write_flushed_text(out_file, "\n".join(lines).strip() + "\n")
@@ -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
@@ -1,4 +1,4 @@
1
- """LLM Client for LiteRT-LM, Ollama, and OpenAI-compatible endpoints with standard host/token/context/language options."""
1
+ """LLM Client for LiteRT-LM, Ollama, and OpenAI-compatible endpoints with multi-language JSON normalization and retry resilience."""
2
2
 
3
3
  import json
4
4
  import os
@@ -31,6 +31,71 @@ def normalize_host_url(host: Optional[str]) -> str:
31
31
  return h
32
32
 
33
33
 
34
+ def normalize_llm_json_dict(data: Dict[str, Any]) -> Dict[str, str]:
35
+ """Normalize JSON dictionary keys from any language (English, Japanese, etc.) into standardized keys."""
36
+ normalized: Dict[str, str] = {}
37
+
38
+ for k in ("purpose", "目的", "設計意図", "意図", "description", "target", "role", "機能"):
39
+ if k in data and data[k]:
40
+ normalized["purpose"] = str(data[k]).strip()
41
+ break
42
+
43
+ for k in ("inputs", "入力", "引数", "パラメータ", "input", "arguments", "parameters"):
44
+ if k in data and data[k]:
45
+ normalized["inputs"] = str(data[k]).strip()
46
+ break
47
+
48
+ for k in ("outputs", "出力", "戻り値", "返り値", "output", "returns", "result"):
49
+ if k in data and data[k]:
50
+ normalized["outputs"] = str(data[k]).strip()
51
+ break
52
+
53
+ for k in ("overview", "概要", "要約", "説明", "summary", "details"):
54
+ if k in data and data[k]:
55
+ normalized["overview"] = str(data[k]).strip()
56
+ break
57
+
58
+ for k in ("significance", "重要性", "役割", "意義", "role"):
59
+ if k in data and data[k]:
60
+ normalized["significance"] = str(data[k]).strip()
61
+ break
62
+
63
+ for k in ("usage_scenario", "利用シナリオ", "使用シナリオ", "データフロー", "データ受け渡し", "scenario"):
64
+ if k in data and data[k]:
65
+ normalized["usage_scenario"] = str(data[k]).strip()
66
+ break
67
+
68
+ for k in ("top_down_summary", "要約", "概要", "全体設計要約", "summary"):
69
+ if k in data and data[k]:
70
+ normalized["top_down_summary"] = str(data[k]).strip()
71
+ break
72
+
73
+ return normalized
74
+
75
+
76
+ def extract_json_from_text(raw_text: str) -> Dict[str, Any]:
77
+ """Robustly extract and parse JSON object from LLM response text."""
78
+ clean = raw_text.strip()
79
+ # 1. Try markdown code block extraction
80
+ m_code = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", clean, re.IGNORECASE)
81
+ if m_code:
82
+ try:
83
+ return json.loads(m_code.group(1).strip())
84
+ except Exception:
85
+ pass
86
+
87
+ # 2. Try outermost curly braces
88
+ m_brace = re.search(r"\{[\s\S]*\}", clean)
89
+ if m_brace:
90
+ try:
91
+ return json.loads(m_brace.group(0).strip())
92
+ except Exception:
93
+ pass
94
+
95
+ # 3. Direct parse
96
+ return json.loads(clean)
97
+
98
+
34
99
  class LLMClient:
35
100
  """OpenAI-compatible client with standard host, token, context size, and model configuration."""
36
101
 
@@ -42,7 +107,7 @@ class LLMClient:
42
107
  token: Optional[str] = None,
43
108
  api_key: Optional[str] = None,
44
109
  context_size: int = 16384,
45
- timeout: int = 180,
110
+ timeout: int = 60,
46
111
  ):
47
112
  raw_host = host or base_url or os.environ.get("LLM_HOST") or os.environ.get("OPENAI_BASE_URL")
48
113
  self.user_specified_host = raw_host is not None
@@ -50,7 +115,7 @@ class LLMClient:
50
115
  self.model = model or os.environ.get("LLM_MODEL", "gemma4-26b-a4b")
51
116
  self.token = token or api_key or os.environ.get("LLM_TOKEN") or os.environ.get("OPENAI_API_KEY")
52
117
  self.context_size = context_size or int(os.environ.get("LLM_CONTEXT_SIZE", "16384"))
53
- self.timeout = timeout
118
+ self.default_timeout = timeout
54
119
 
55
120
  self.ensure_reachable()
56
121
 
@@ -59,7 +124,6 @@ class LLMClient:
59
124
  if self._ping_url(self.base_url):
60
125
  return True
61
126
 
62
- # If the user explicitly passed a host/base_url, never overwrite it with default WSL fallback
63
127
  if self.user_specified_host:
64
128
  return False
65
129
 
@@ -118,14 +182,23 @@ class LLMClient:
118
182
  """Verify server connectivity."""
119
183
  return self.ensure_reachable()
120
184
 
121
- def chat_completion(self, messages: List[Dict[str, str]], json_mode: bool = False, max_retries: int = 3) -> str:
185
+ def chat_completion(
186
+ self,
187
+ messages: List[Dict[str, str]],
188
+ json_mode: bool = False,
189
+ max_tokens: int = 1024,
190
+ timeout: Optional[int] = None,
191
+ max_retries: int = 2,
192
+ ) -> str:
122
193
  """Execute chat completion request with retry loop, token authentication, and context size limits."""
123
194
  url = f"{self.base_url}/chat/completions"
195
+ req_timeout = timeout or self.default_timeout
196
+
124
197
  payload: Dict[str, Any] = {
125
198
  "model": self.model,
126
199
  "messages": messages,
127
200
  "temperature": 0.2,
128
- "max_tokens": 4096,
201
+ "max_tokens": max_tokens,
129
202
  "options": {
130
203
  "num_ctx": self.context_size,
131
204
  },
@@ -142,7 +215,7 @@ class LLMClient:
142
215
  req.add_header("Authorization", f"Bearer {self.token}")
143
216
 
144
217
  try:
145
- with urllib.request.urlopen(req, timeout=self.timeout) as resp:
218
+ with urllib.request.urlopen(req, timeout=req_timeout) as resp:
146
219
  res_data = json.loads(resp.read().decode("utf-8"))
147
220
  return res_data["choices"][0]["message"]["content"]
148
221
  except urllib.error.HTTPError as he:
@@ -153,16 +226,15 @@ class LLMClient:
153
226
  pass
154
227
  detail = f": {err_body}" if err_body else ""
155
228
  last_error = f"HTTP Error {he.code}: {he.reason}{detail}"
156
- # If 404 (e.g. model not found), don't retry in vain
157
229
  if he.code == 404:
158
230
  break
159
231
  if attempt < max_retries:
160
- time.sleep(1.5 * attempt)
232
+ time.sleep(1.0 * attempt)
161
233
  continue
162
234
  except Exception as e:
163
235
  last_error = str(e)
164
236
  if attempt < max_retries:
165
- time.sleep(1.5 * attempt)
237
+ time.sleep(1.0 * attempt)
166
238
  continue
167
239
 
168
240
  raise LLMError(f"LLM API call failed ({url}, model={self.model}): {last_error}")
@@ -181,9 +253,10 @@ class LLMClient:
181
253
  language: str = "English",
182
254
  allow_fallback: bool = False,
183
255
  ) -> 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.
186
- Output Language: {language} (Write all text values in {language}).
256
+ """Generate concise explanations for a symbol in JSON format in the requested natural language."""
257
+ prompt = f"""Please analyze the following {lang} symbol `{name}` ({kind}) and output concise design intent and specifications in JSON format.
258
+ Output Language: {language} (Write all explanation text in {language}).
259
+ Be concise and avoid repetition.
187
260
 
188
261
  ### Signature / Type:
189
262
  `{signature}`
@@ -204,12 +277,12 @@ Output Language: {language} (Write all text values in {language}).
204
277
  {code}
205
278
  ```
206
279
 
207
- Return ONLY a valid JSON object matching this schema (all string values in {language}):
280
+ Return ONLY a valid JSON object matching these keys (values written in {language}):
208
281
  {{
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)"
282
+ "purpose": "Core design intent and purpose (1-2 concise sentences in {language})",
283
+ "inputs": "Input parameters description (or None)",
284
+ "outputs": "Return value or side effects description",
285
+ "overview": "Concise summary of implementation and role (1-3 sentences in {language})"
213
286
  }}
214
287
  """
215
288
  messages = [
@@ -218,19 +291,18 @@ Return ONLY a valid JSON object matching this schema (all string values in {lang
218
291
  ]
219
292
 
220
293
  try:
221
- raw_res = self.chat_completion(messages, json_mode=True)
222
- m = re.search(r"\{[\s\S]*\}", raw_res)
223
- if m:
224
- return json.loads(m.group(0))
225
- return json.loads(raw_res)
294
+ raw_res = self.chat_completion(messages, json_mode=False, max_tokens=768, timeout=60)
295
+ parsed = extract_json_from_text(raw_res)
296
+ return normalize_llm_json_dict(parsed)
226
297
  except Exception as e:
227
298
  if not allow_fallback:
228
299
  raise LLMError(f"Failed to analyze symbol `{name}`: {e}") from e
300
+ is_ja = language in ("Japanese", "日本語")
229
301
  return {
230
- "purpose": f"Executes `{name}` operations.",
231
- "inputs": "Accepts input parameters.",
232
- "outputs": "Returns result value.",
233
- "overview": f"Basic operation for `{name}`.",
302
+ "purpose": f"`{name}` の処理を実行する。" if is_ja else f"Executes `{name}` operations.",
303
+ "inputs": "パラメータを受け取る。" if is_ja else "Accepts input parameters.",
304
+ "outputs": "結果値を返す。" if is_ja else "Returns result value.",
305
+ "overview": f"`{name}` の基本処理。" if is_ja else f"Basic operation for `{name}`.",
234
306
  }
235
307
 
236
308
  def refine_variable_top_down(
@@ -253,6 +325,7 @@ Return ONLY a valid JSON object matching this schema (all string values in {lang
253
325
 
254
326
  prompt = f"""Please analyze the following {lang} variable/field `{var_name}` in the context of the caller/parent functions that reference it.
255
327
  Output Language: {language} (Write all text in {language}).
328
+ Be concise and avoid repetition.
256
329
 
257
330
  ### Variable: `{var_name}` ({var_kind})
258
331
  - Signature/Type: `{var_signature}`
@@ -260,7 +333,7 @@ Output Language: {language} (Write all text in {language}).
260
333
  ### Referencing Caller Functions:
261
334
  {chr(10).join(context_lines)}
262
335
 
263
- Return ONLY a valid JSON object matching this schema (all string values in {language}):
336
+ Return ONLY a valid JSON object (all string values in {language}):
264
337
  {{
265
338
  "significance": "Essential significance and design rationale in context of callers (1-2 sentences)",
266
339
  "usage_scenario": "Data passing lifecycle and state management role across parent functions (1-2 sentences)",
@@ -273,16 +346,15 @@ Return ONLY a valid JSON object matching this schema (all string values in {lang
273
346
  ]
274
347
 
275
348
  try:
276
- raw_res = self.chat_completion(messages, json_mode=True)
277
- m = re.search(r"\{[\s\S]*\}", raw_res)
278
- if m:
279
- return json.loads(m.group(0))
280
- return json.loads(raw_res)
349
+ raw_res = self.chat_completion(messages, json_mode=False, max_tokens=768, timeout=60)
350
+ parsed = extract_json_from_text(raw_res)
351
+ return normalize_llm_json_dict(parsed)
281
352
  except Exception as e:
282
353
  if not allow_fallback:
283
354
  raise LLMError(f"Failed top-down variable refinement for `{var_name}`: {e}") from e
355
+ is_ja = language in ("Japanese", "日本語")
284
356
  return {
285
- "significance": f"`{var_name}` is a state/data utilized by callers.",
286
- "usage_scenario": "Passed and transformed across calling functions.",
287
- "top_down_summary": f"Design summary for `{var_name}`.",
357
+ "significance": f"`{var_name}` は呼び出し元で使用される状態/データ。" if is_ja else f"`{var_name}` is a state/data utilized by callers.",
358
+ "usage_scenario": "呼び出し元関数間で受け渡される。" if is_ja else "Passed and transformed across calling functions.",
359
+ "top_down_summary": f"`{var_name}` の設計概要。" if is_ja else f"Design summary for `{var_name}`.",
288
360
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pystdoc
3
- Version: 0.2.0
3
+ Version: 0.4.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