pystdoc 0.8.2__tar.gz → 0.8.4__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 (81) hide show
  1. {pystdoc-0.8.2/src/pystdoc.egg-info → pystdoc-0.8.4}/PKG-INFO +2 -2
  2. {pystdoc-0.8.2 → pystdoc-0.8.4}/README.md +1 -1
  3. {pystdoc-0.8.2 → pystdoc-0.8.4}/pyproject.toml +1 -1
  4. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/__init__.py +1 -1
  5. pystdoc-0.8.4/src/pystdoc/call_graph.py +77 -0
  6. pystdoc-0.8.4/src/pystdoc/call_graph_nodes.py +77 -0
  7. pystdoc-0.8.4/src/pystdoc/callee_summary.py +66 -0
  8. pystdoc-0.8.4/src/pystdoc/doc_labels.py +171 -0
  9. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/doc_writer.py +144 -47
  10. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/engine.py +14 -1
  11. pystdoc-0.8.4/src/pystdoc/level_graph.py +152 -0
  12. pystdoc-0.8.4/src/pystdoc/scc.py +54 -0
  13. pystdoc-0.8.4/src/pystdoc/var_linker.py +118 -0
  14. {pystdoc-0.8.2 → pystdoc-0.8.4/src/pystdoc.egg-info}/PKG-INFO +2 -2
  15. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc.egg-info/SOURCES.txt +8 -0
  16. pystdoc-0.8.4/tests/test_call_graph_split.py +137 -0
  17. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_config.py +91 -0
  18. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_docgen.py +15 -2
  19. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_live_server_100.py +2 -2
  20. pystdoc-0.8.4/tests/test_real_litert_server.py +240 -0
  21. pystdoc-0.8.2/src/pystdoc/call_graph.py +0 -344
  22. {pystdoc-0.8.2 → pystdoc-0.8.4}/LICENSE +0 -0
  23. {pystdoc-0.8.2 → pystdoc-0.8.4}/setup.cfg +0 -0
  24. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/__main__.py +0 -0
  25. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/adapters/__init__.py +0 -0
  26. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/adapters/base.py +0 -0
  27. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/adapters/clang_adapter.py +0 -0
  28. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/adapters/generic_adapter.py +0 -0
  29. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/adapters/java_adapter.py +0 -0
  30. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/adapters/kotlin_adapter.py +0 -0
  31. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/adapters/python_adapter.py +0 -0
  32. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/cache.py +0 -0
  33. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/cli.py +0 -0
  34. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/compilation_db.py +0 -0
  35. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/config.py +0 -0
  36. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/db.py +0 -0
  37. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/design_engine.py +0 -0
  38. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/hasher.py +0 -0
  39. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/interrupt.py +0 -0
  40. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/llm_client.py +0 -0
  41. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/mcp_server.py +0 -0
  42. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/parser_clang.py +0 -0
  43. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/parser_generic.py +0 -0
  44. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/parser_java.py +0 -0
  45. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/parser_kotlin.py +0 -0
  46. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/parser_python.py +0 -0
  47. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/perf.py +0 -0
  48. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/progress.py +0 -0
  49. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/query.py +0 -0
  50. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/report_engine.py +0 -0
  51. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/scanner.py +0 -0
  52. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/similarity.py +0 -0
  53. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/symbols.py +0 -0
  54. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/ui_detector.py +0 -0
  55. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc/watcher.py +0 -0
  56. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc.egg-info/dependency_links.txt +0 -0
  57. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc.egg-info/entry_points.txt +0 -0
  58. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc.egg-info/requires.txt +0 -0
  59. {pystdoc-0.8.2 → pystdoc-0.8.4}/src/pystdoc.egg-info/top_level.txt +0 -0
  60. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_adapters.py +0 -0
  61. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_cli.py +0 -0
  62. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_coverage_boost.py +0 -0
  63. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_design_engine_full.py +0 -0
  64. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_designgen.py +0 -0
  65. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_engine.py +0 -0
  66. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_engine_full.py +0 -0
  67. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_final_100.py +0 -0
  68. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_interrupt.py +0 -0
  69. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_llm_client.py +0 -0
  70. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_mcp_server.py +0 -0
  71. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_parser_generic.py +0 -0
  72. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_parser_java.py +0 -0
  73. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_parser_kotlin.py +0 -0
  74. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_pep8.py +0 -0
  75. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_perf.py +0 -0
  76. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_progress.py +0 -0
  77. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_query.py +0 -0
  78. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_reduce_cache.py +0 -0
  79. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_reportgen.py +0 -0
  80. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_ui_detector.py +0 -0
  81. {pystdoc-0.8.2 → pystdoc-0.8.4}/tests/test_watcher.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pystdoc
3
- Version: 0.8.2
3
+ Version: 0.8.4
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
@@ -78,7 +78,7 @@ your_project/
78
78
  - Evaluates call graphs in $O(V+E)$ linear time.
79
79
  - Automatically breaks cyclic mutual recursions and organizes code symbols into dependency-safe execution levels.
80
80
  - Level-by-level parallel LLM execution guarantees context-rich bottom-up summaries without race conditions.
81
- 2. **High-Speed Incremental Sync & Semantic Change Propagation (v0.8.0 - v0.8.2)**:
81
+ 2. **High-Speed Incremental Sync & Semantic Change Propagation (v0.8.0 - v0.8.4)**:
82
82
  - **Selective Bottom-Up Propagation & Immediate Snippet Flush**: Detects AST and logic-level code differences. If regenerated specifications remain semantically unchanged, cascading LLM re-analysis terminates early while Markdown documents (source code snippets, line ranges, caller/callee references) are immediately updated with latest code. Signature, kind, and callee alterations are guaranteed to propagate upstream.
83
83
  - **Content-Defined Map-Reduce Caching**: Intermediate summaries in architecture synthesis (`designgen`) are chunked using stable hash boundaries and cached per group in SQLite, ensuring that only groups containing edited functions are re-summarized.
84
84
  - **Architectural Entry-Point Filtering**: For system execution modeling (`execution_model.md`), key architectural functions (entry points, CLI routers, and core dispatchers) are automatically filtered and passed directly into the LLM context, bypassing heavy multi-step Map-Reduce loops and speeding up generation by up to 90%+.
@@ -49,7 +49,7 @@ your_project/
49
49
  - Evaluates call graphs in $O(V+E)$ linear time.
50
50
  - Automatically breaks cyclic mutual recursions and organizes code symbols into dependency-safe execution levels.
51
51
  - Level-by-level parallel LLM execution guarantees context-rich bottom-up summaries without race conditions.
52
- 2. **High-Speed Incremental Sync & Semantic Change Propagation (v0.8.0 - v0.8.2)**:
52
+ 2. **High-Speed Incremental Sync & Semantic Change Propagation (v0.8.0 - v0.8.4)**:
53
53
  - **Selective Bottom-Up Propagation & Immediate Snippet Flush**: Detects AST and logic-level code differences. If regenerated specifications remain semantically unchanged, cascading LLM re-analysis terminates early while Markdown documents (source code snippets, line ranges, caller/callee references) are immediately updated with latest code. Signature, kind, and callee alterations are guaranteed to propagate upstream.
54
54
  - **Content-Defined Map-Reduce Caching**: Intermediate summaries in architecture synthesis (`designgen`) are chunked using stable hash boundaries and cached per group in SQLite, ensuring that only groups containing edited functions are re-summarized.
55
55
  - **Architectural Entry-Point Filtering**: For system execution modeling (`execution_model.md`), key architectural functions (entry points, CLI routers, and core dispatchers) are automatically filtered and passed directly into the LLM context, bypassing heavy multi-step Map-Reduce loops and speeding up generation by up to 90%+.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pystdoc"
7
- version = "0.8.2"
7
+ version = "0.8.4"
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 (pystdoc)."""
2
2
 
3
- __version__ = "0.8.2"
3
+ __version__ = "0.8.4"
@@ -0,0 +1,77 @@
1
+ """Call graph facade: Level DAG ordering and backward-compatible exports.
2
+
3
+ Purpose:
4
+ Partition symbol nodes into dependency-safe levels (callees before
5
+ callers, SCC groups together) for level-by-level parallel LLM runs.
6
+ The implementation is split across small modules; the public names
7
+ below are re-exported so existing imports keep working:
8
+
9
+ - call_graph_nodes : SymbolNode, flatten_symbols
10
+ - var_linker : link_variables_to_functions
11
+ - scc : tarjan_scc
12
+ - level_graph : ordering helpers
13
+ - callee_summary : build_callee_context_summary
14
+
15
+ Change history:
16
+ 1. 2026-11-02 (v0.8.3): Split into the modules above. Fixed the
17
+ redundant SCC re-queueing in ranking (see level_graph.py).
18
+ """
19
+
20
+ from typing import List
21
+
22
+ from pystdoc.callee_summary import build_callee_context_summary
23
+ from pystdoc.call_graph_nodes import SymbolNode, flatten_symbols
24
+ from pystdoc.level_graph import (
25
+ assemble_levels,
26
+ assign_scc_groups,
27
+ build_callee_edges,
28
+ build_condensation,
29
+ index_nodes,
30
+ rank_sccs,
31
+ )
32
+ from pystdoc.scc import tarjan_scc
33
+ from pystdoc.symbols import get_kind_prefix
34
+ from pystdoc.var_linker import link_variables_to_functions
35
+
36
+ __all__ = [
37
+ "SymbolNode",
38
+ "flatten_symbols",
39
+ "link_variables_to_functions",
40
+ "tarjan_scc",
41
+ "order_symbols_by_levels",
42
+ "order_symbols_bottom_up",
43
+ "build_callee_context_summary",
44
+ ]
45
+
46
+
47
+ def order_symbols_by_levels(
48
+ nodes: List[SymbolNode],
49
+ ) -> List[List[SymbolNode]]:
50
+ """Partition symbol nodes into strictly dependency-safe levels."""
51
+ id_to_node, fqdn_to_ids, short_to_ids = index_nodes(nodes)
52
+ caller_to_callees = build_callee_edges(
53
+ nodes, id_to_node, fqdn_to_ids, short_to_ids
54
+ )
55
+
56
+ base_nodes = [n for n in nodes if get_kind_prefix(n.symbol.kind) != "fn"]
57
+ for node in base_nodes:
58
+ node.dag_level = 0
59
+ fn_ids = [
60
+ n.unique_id for n in nodes if get_kind_prefix(n.symbol.kind) == "fn"
61
+ ]
62
+
63
+ sccs = tarjan_scc(fn_ids, caller_to_callees)
64
+ node_to_scc_idx = assign_scc_groups(sccs, id_to_node)
65
+ scc_callees, scc_callers = build_condensation(
66
+ node_to_scc_idx, caller_to_callees
67
+ )
68
+ scc_rank = rank_sccs(len(sccs), scc_callees, scc_callers)
69
+ return assemble_levels(base_nodes, sccs, scc_rank, id_to_node)
70
+
71
+
72
+ def order_symbols_bottom_up(nodes: List[SymbolNode]) -> List[SymbolNode]:
73
+ """Flattened bottom-up order."""
74
+ flat: List[SymbolNode] = []
75
+ for level in order_symbols_by_levels(nodes):
76
+ flat.extend(level)
77
+ return flat
@@ -0,0 +1,77 @@
1
+ """SymbolNode data structure and symbol flattening.
2
+
3
+ Purpose:
4
+ Define the flat graph node (SymbolNode) and convert a hierarchical
5
+ Symbol tree into a flat list of nodes with unique IDs and FQDNs.
6
+
7
+ Change history:
8
+ 1. 2026-11-02 (v0.8.3): Split out of call_graph.py (no logic change).
9
+ """
10
+
11
+ from collections import defaultdict
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional, Set
15
+
16
+ from pystdoc.symbols import Symbol, get_kind_prefix
17
+
18
+
19
+ @dataclass
20
+ class SymbolNode:
21
+ symbol: Symbol
22
+ rel_path: Path
23
+ full_path: Path
24
+ unique_id: str
25
+ fqdn: str = ""
26
+ direct_callee_ids: Set[str] = field(default_factory=set)
27
+ direct_caller_ids: Set[str] = field(default_factory=set)
28
+ scc_group_ids: List[str] = field(default_factory=list)
29
+ dag_level: int = 0
30
+
31
+
32
+ def flatten_symbols(
33
+ symbols: List[Symbol],
34
+ rel_path: Path,
35
+ full_path: Path,
36
+ prefix: str = "",
37
+ seen_ids: Optional[Dict[str, int]] = None,
38
+ ) -> List[SymbolNode]:
39
+ """Recursively collect symbols into flat list with FQDN and prefixes."""
40
+ if seen_ids is None:
41
+ seen_ids = defaultdict(int)
42
+ nodes: List[SymbolNode] = []
43
+ for sym in symbols:
44
+ k_prefix = get_kind_prefix(sym.kind)
45
+ raw_name = f"{prefix}{sym.name}" if prefix else sym.name
46
+ identifier = f"{k_prefix}.{raw_name}"
47
+ base_uid = f"{rel_path.as_posix()}::{identifier}"
48
+ seen_ids[base_uid] += 1
49
+ if seen_ids[base_uid] == 1:
50
+ uid = base_uid
51
+ else:
52
+ uid = f"{base_uid}#{seen_ids[base_uid]}"
53
+
54
+ base_mod = rel_path.with_suffix("").as_posix().replace("/", ".")
55
+ fqdn = sym.fqdn or f"{base_mod}.{raw_name}"
56
+
57
+ node = SymbolNode(
58
+ symbol=sym,
59
+ rel_path=rel_path,
60
+ full_path=full_path,
61
+ unique_id=uid,
62
+ fqdn=fqdn,
63
+ direct_callee_ids=set(),
64
+ scc_group_ids=[],
65
+ dag_level=0,
66
+ )
67
+ nodes.append(node)
68
+ if sym.children:
69
+ child_nodes = flatten_symbols(
70
+ sym.children,
71
+ rel_path,
72
+ full_path,
73
+ prefix=f"{raw_name}.",
74
+ seen_ids=seen_ids,
75
+ )
76
+ nodes.extend(child_nodes)
77
+ return nodes
@@ -0,0 +1,66 @@
1
+ """Callee context summary builder for LLM prompts.
2
+
3
+ Purpose:
4
+ Build a text summary of directly called functions and mutual
5
+ recursion peers of a symbol node, used as bottom-up LLM context.
6
+
7
+ Change history:
8
+ 1. 2026-11-02 (v0.8.3): Split out of call_graph.py (no logic change).
9
+ """
10
+
11
+ from typing import Dict, List
12
+
13
+ from pystdoc.call_graph_nodes import SymbolNode
14
+
15
+
16
+ def build_callee_context_summary(
17
+ node: SymbolNode,
18
+ resolved_symbols: Dict[str, SymbolNode],
19
+ ) -> str:
20
+ """Build summary text of called functions and recursion peers for LLM."""
21
+ lines: List[str] = []
22
+
23
+ if node.scc_group_ids:
24
+ peer_names = []
25
+ for peer_id in node.scc_group_ids:
26
+ peer_node = resolved_symbols.get(peer_id)
27
+ if peer_node:
28
+ peer_names.append(
29
+ f"`{peer_node.symbol.name}` ({peer_node.rel_path.name})"
30
+ )
31
+ if peer_names:
32
+ lines.append("[Mutual Recursion Group]:")
33
+ lines.append(
34
+ f"- Note: This function operates in mutual recursion "
35
+ f"with {', '.join(peer_names)}."
36
+ )
37
+ lines.append("")
38
+
39
+ if node.direct_callee_ids:
40
+ lines.append("[Direct Callees Summary]:")
41
+ for callee_id in sorted(node.direct_callee_ids):
42
+ callee_node = resolved_symbols.get(callee_id)
43
+ if callee_node:
44
+ sym = callee_node.symbol
45
+ purpose = sym.purpose or "Executes operation"
46
+ params_str = ", ".join(
47
+ f"{p.name}: {p.type_hint}" for p in sym.parameters
48
+ )
49
+ inputs = sym.inputs_note or params_str or "None"
50
+ outputs = sym.outputs_note or sym.return_type or "None"
51
+ fqdn_str = sym.fqdn or sym.name
52
+ lines.append(
53
+ f"- Function `{sym.name}` (FQDN: `{fqdn_str}`, "
54
+ f"File: `{callee_node.rel_path.name}`):"
55
+ )
56
+ lines.append(f" - Purpose: {purpose}")
57
+ lines.append(f" - Inputs: {inputs}")
58
+ lines.append(f" - Outputs: {outputs}")
59
+ if sym.overview:
60
+ first_lines = sym.overview.strip().splitlines()[:2]
61
+ summary_str = " / ".join(
62
+ line_item.strip() for line_item in first_lines
63
+ )
64
+ lines.append(f" - Summary: {summary_str}")
65
+
66
+ return "\n".join(lines)
@@ -0,0 +1,171 @@
1
+ """Documentation label localization and JSON template management."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Optional
6
+
7
+ DEFAULT_ENGLISH_LABELS: Dict[str, str] = {
8
+ "language": "English",
9
+ "doc_title_template": "{kind} Documentation: `{name}`",
10
+ "basic_info_title": "## 1. Basic Information",
11
+ "label_name": "Name",
12
+ "label_fqdn": "FQDN",
13
+ "label_symbol_kind": "Symbol Kind",
14
+ "label_location": "Location",
15
+ "location_template": "Line {start} to Line {end}",
16
+ "label_signature": "Signature / Type",
17
+ "parameters_title": "## 2. Parameters",
18
+ "label_none": "None",
19
+ "input_return_title": "## 3. Input / Return Specifications",
20
+ "label_input_constraints": "Input Constraints",
21
+ "label_return_type": "Return Type",
22
+ "label_return_value_side_effects": "Return Value & Side Effects",
23
+ "label_void_none": "void / None",
24
+ "called_functions_title": "## 4. Called Functions",
25
+ "referencing_functions_title": "## 4. Referencing Functions",
26
+ "source_code_snippet_title": "## Source Code Snippet",
27
+ "file_doc_title_template": "File Documentation: `{path}`",
28
+ "label_file_hash": "File SHA-256",
29
+ "label_detected_symbols": "Detected Symbols",
30
+ "symbol_list_title": "## Symbol List",
31
+ "symbol_list_item_template": "- `{name}` ({kind}) [Lines: {line_range}]",
32
+ "kind_function": "Function",
33
+ "kind_class": "Class",
34
+ "kind_method": "Method",
35
+ "kind_variable": "Variable",
36
+ "kind_struct": "Struct",
37
+ "kind_enum": "Enum",
38
+ "kind_interface": "Interface",
39
+ "kind_constant": "Constant",
40
+ "kind_type": "Type",
41
+ "kind_module": "Module",
42
+ "prose_referenced_by_template": (
43
+ "In the system architecture, it is referenced by {callers}."
44
+ ),
45
+ "prose_root_entry_point": (
46
+ "It serves as the root entry point (system bootstrap and dispatcher)."
47
+ ),
48
+ "prose_implementation_overview_template": (
49
+ "Implementation overview: {overview}"
50
+ ),
51
+ }
52
+
53
+
54
+ def get_default_labels(language: str = "English") -> Dict[str, str]:
55
+ """Return default dictionary of documentation labels (English base)."""
56
+ return dict(DEFAULT_ENGLISH_LABELS)
57
+
58
+
59
+ def translate_labels_with_llm(
60
+ llm_client: Any,
61
+ target_language: str,
62
+ ) -> Dict[str, str]:
63
+ """Translate documentation labels to target_language using LLM."""
64
+ from pystdoc.llm_client import extract_json_from_text
65
+
66
+ base_labels = dict(DEFAULT_ENGLISH_LABELS)
67
+ base_labels["language"] = target_language
68
+
69
+ system_prompt = (
70
+ "You are an expert technical documentation localizer. "
71
+ f"Translate software documentation labels and headings into natural, "
72
+ f"standard software engineering terms in {target_language}.\n"
73
+ "Guidelines:\n"
74
+ "- In Japanese software engineering, documentation is conventionally "
75
+ "called '仕様書' (e.g. '関数仕様書' for Function Documentation, "
76
+ "'クラス仕様書' for Class Documentation, "
77
+ "'ファイル仕様書' for File Documentation).\n"
78
+ "- When translating into Japanese, translate `doc_title_template` "
79
+ "as '{kind}仕様書: `{name}`', `file_doc_title_template` as "
80
+ "'ファイル仕様書: `{path}`', `kind_function` as '関数', "
81
+ "`kind_class` as 'クラス', `kind_method` as 'メソッド', "
82
+ "`kind_variable` as '変数', `kind_struct` as '構造体', "
83
+ "`kind_enum` as '列挙型', `kind_constant` as '定数'.\n"
84
+ "- For other languages, translate software terms idiomatic to that "
85
+ "language (e.g. Funktion in German, 函数 in Chinese).\n"
86
+ "- Keep placeholders {kind}, {name}, {path}, {start}, {end}, "
87
+ "{line_range}, {callers}, {overview} exactly as is.\n"
88
+ "- Respond strictly with a valid JSON object matching the input keys."
89
+ )
90
+ user_prompt = (
91
+ f"Translate the following JSON documentation labels into "
92
+ f"{target_language}:\n\n"
93
+ f"```json\n"
94
+ f"{json.dumps(base_labels, indent=2, ensure_ascii=False)}\n"
95
+ f"```\n\n"
96
+ f"Output ONLY valid JSON."
97
+ )
98
+
99
+ try:
100
+ messages = [
101
+ {"role": "system", "content": system_prompt},
102
+ {"role": "user", "content": user_prompt},
103
+ ]
104
+ response_text = llm_client.chat_completion(messages)
105
+ parsed = extract_json_from_text(response_text)
106
+ if isinstance(parsed, dict) and len(parsed) >= 5:
107
+ merged = dict(base_labels)
108
+ for k, v in parsed.items():
109
+ if k in merged and isinstance(v, str) and v.strip():
110
+ merged[k] = v.strip()
111
+ merged["language"] = target_language
112
+ return merged
113
+ except Exception:
114
+ pass
115
+
116
+ return base_labels
117
+
118
+
119
+ def get_or_init_document_labels(
120
+ target_dir: Optional[Path],
121
+ language: str = "English",
122
+ llm_client: Optional[Any] = None,
123
+ ) -> Dict[str, str]:
124
+ """Load or initialize document_labels.json under target_dir/.pystdoc/."""
125
+ from pystdoc.doc_writer import normalize_language
126
+
127
+ norm_lang = normalize_language(language)
128
+ defaults = get_default_labels(norm_lang)
129
+
130
+ if target_dir is None:
131
+ return defaults
132
+
133
+ pystdoc_dir = target_dir / ".pystdoc"
134
+ labels_file = pystdoc_dir / "document_labels.json"
135
+
136
+ if labels_file.exists():
137
+ try:
138
+ content = labels_file.read_text(encoding="utf-8")
139
+ loaded = json.loads(content)
140
+ if (
141
+ isinstance(loaded, dict)
142
+ and loaded.get("language") == norm_lang
143
+ ):
144
+ merged = dict(defaults)
145
+ merged.update(loaded)
146
+ return merged
147
+ except Exception:
148
+ pass
149
+
150
+ # If language is not English, generate localized labels via LLM
151
+ final_labels = defaults
152
+ if norm_lang != "English" and llm_client is not None:
153
+ try:
154
+ print(
155
+ f"[Labels] Generating localized document labels for "
156
+ f"{norm_lang} via LLM...",
157
+ flush=True,
158
+ )
159
+ final_labels = translate_labels_with_llm(llm_client, norm_lang)
160
+ except Exception:
161
+ final_labels = defaults
162
+
163
+ # Save initial labels JSON for the target language
164
+ try:
165
+ pystdoc_dir.mkdir(parents=True, exist_ok=True)
166
+ content = json.dumps(final_labels, indent=2, ensure_ascii=False) + "\n"
167
+ labels_file.write_text(content, encoding="utf-8")
168
+ except Exception:
169
+ pass
170
+
171
+ return final_labels