pystdoc 0.7.2__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pystdoc-0.7.2/src/pystdoc.egg-info → pystdoc-0.8.0}/PKG-INFO +10 -6
- {pystdoc-0.7.2 → pystdoc-0.8.0}/README.md +9 -5
- {pystdoc-0.7.2 → pystdoc-0.8.0}/pyproject.toml +43 -1
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/__init__.py +1 -1
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/db.py +42 -12
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/design_engine.py +209 -39
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/engine.py +26 -39
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/llm_client.py +3 -4
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/mcp_server.py +0 -3
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/perf.py +2 -2
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/query.py +1 -1
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/report_engine.py +6 -2
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/scanner.py +1 -1
- pystdoc-0.8.0/src/pystdoc/similarity.py +45 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0/src/pystdoc.egg-info}/PKG-INFO +10 -6
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc.egg-info/SOURCES.txt +2 -0
- pystdoc-0.8.0/tests/test_reduce_cache.py +78 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/LICENSE +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/setup.cfg +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/__main__.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/adapters/__init__.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/adapters/base.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/adapters/clang_adapter.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/adapters/generic_adapter.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/adapters/java_adapter.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/adapters/kotlin_adapter.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/adapters/python_adapter.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/cache.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/call_graph.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/cli.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/compilation_db.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/config.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/doc_writer.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/hasher.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/interrupt.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/parser_clang.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/parser_generic.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/parser_java.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/parser_kotlin.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/parser_python.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/progress.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/symbols.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/ui_detector.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc/watcher.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc.egg-info/dependency_links.txt +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc.egg-info/entry_points.txt +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc.egg-info/requires.txt +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/src/pystdoc.egg-info/top_level.txt +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_adapters.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_cli.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_config.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_coverage_boost.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_design_engine_full.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_designgen.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_docgen.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_engine.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_engine_full.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_final_100.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_interrupt.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_live_server_100.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_llm_client.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_mcp_server.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_parser_generic.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_parser_java.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_parser_kotlin.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_pep8.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_perf.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_progress.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_query.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_reportgen.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_ui_detector.py +0 -0
- {pystdoc-0.7.2 → pystdoc-0.8.0}/tests/test_watcher.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pystdoc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -78,21 +78,25 @@ 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. **
|
|
81
|
+
2. **High-Speed Incremental Sync & Semantic Change Propagation (v0.8.0)**:
|
|
82
|
+
- **Selective Bottom-Up Propagation**: Detects AST and logic-level code differences. If regenerated specifications remain semantically unchanged (evaluated by deterministic Jaccard / token-overlap metrics), change propagation terminates immediately without triggering cascading LLM re-analysis across upper modules.
|
|
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
|
+
- **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%+.
|
|
85
|
+
3. **3-in-1 Unified Documentation Pipeline**:
|
|
82
86
|
- `docgen`: Bottom-up & top-down symbol-level documentation with SHA-256 and SQLite caching (`.pystdoc/documents/`).
|
|
83
87
|
- `designgen`: Map-Reduce architectural synthesis (`.pystdoc/design/`).
|
|
84
88
|
- `reportgen` / `pystdoc`: Executive summary README (`.pystdoc/README.md`) answering *"What does this project actually do?"*
|
|
85
|
-
|
|
89
|
+
4. **Multi-Language Codebase Deep Understanding (C/C++, Python, Java, Kotlin, Shell)**:
|
|
86
90
|
- **C/C++**: `compile_commands.json` integration, include path resolution, and macro expansion via `libclang`.
|
|
87
91
|
- **Java / Kotlin**: AST parsing via `javalang` & `kopyt` with class hierarchy, companion objects, and data classes.
|
|
88
92
|
- **Python & Shell**: AST visitor and generic regex parser with import/call extraction.
|
|
89
93
|
- **Fully Qualified Domain Names (FQDN)**: Disambiguates identical symbol names across large monorepos.
|
|
90
|
-
|
|
94
|
+
5. **Feature Location & UI Component Pinpointing (`locate` / `pystdoc_locate_feature`)**:
|
|
91
95
|
- Pinpoint exact files, functions, UI elements, and line ranges (`Lines X-Y`) from high-level natural language queries (e.g., *"where is the debug notification button"*, *"add auth header"*) without running brute-force `Glob`/`Grep` across the entire project.
|
|
92
96
|
- Built-in multi-lingual token scoring with automatic boosting for UI elements (`@Composable`, `Button`, `Dialog`, `Activity`, `Modifier`, etc.).
|
|
93
|
-
|
|
97
|
+
6. **Impact & Inbound/Outbound Dependency Analysis (`impact` / `pystdoc_trace_impact`)**:
|
|
94
98
|
- Instant 360-degree dependency tracing: inspect where a symbol is called from (*Inbound Callers / References*) and what it uses (*Outbound Dependencies*) before making breaking code changes or deletions.
|
|
95
|
-
|
|
99
|
+
7. **Standard LLM Options & Multi-Language Support**:
|
|
96
100
|
- Works with **Ollama, LiteRT-LM, vLLM, and OpenAI API**.
|
|
97
101
|
- Supports `--host`, `--model`, `--token` / `--api-key`, `--context-size`, and `--language` (defaults to terminal language, e.g. `Japanese`, `English`, `日本語`).
|
|
98
102
|
|
|
@@ -49,21 +49,25 @@ 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. **
|
|
52
|
+
2. **High-Speed Incremental Sync & Semantic Change Propagation (v0.8.0)**:
|
|
53
|
+
- **Selective Bottom-Up Propagation**: Detects AST and logic-level code differences. If regenerated specifications remain semantically unchanged (evaluated by deterministic Jaccard / token-overlap metrics), change propagation terminates immediately without triggering cascading LLM re-analysis across upper modules.
|
|
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
|
+
- **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%+.
|
|
56
|
+
3. **3-in-1 Unified Documentation Pipeline**:
|
|
53
57
|
- `docgen`: Bottom-up & top-down symbol-level documentation with SHA-256 and SQLite caching (`.pystdoc/documents/`).
|
|
54
58
|
- `designgen`: Map-Reduce architectural synthesis (`.pystdoc/design/`).
|
|
55
59
|
- `reportgen` / `pystdoc`: Executive summary README (`.pystdoc/README.md`) answering *"What does this project actually do?"*
|
|
56
|
-
|
|
60
|
+
4. **Multi-Language Codebase Deep Understanding (C/C++, Python, Java, Kotlin, Shell)**:
|
|
57
61
|
- **C/C++**: `compile_commands.json` integration, include path resolution, and macro expansion via `libclang`.
|
|
58
62
|
- **Java / Kotlin**: AST parsing via `javalang` & `kopyt` with class hierarchy, companion objects, and data classes.
|
|
59
63
|
- **Python & Shell**: AST visitor and generic regex parser with import/call extraction.
|
|
60
64
|
- **Fully Qualified Domain Names (FQDN)**: Disambiguates identical symbol names across large monorepos.
|
|
61
|
-
|
|
65
|
+
5. **Feature Location & UI Component Pinpointing (`locate` / `pystdoc_locate_feature`)**:
|
|
62
66
|
- Pinpoint exact files, functions, UI elements, and line ranges (`Lines X-Y`) from high-level natural language queries (e.g., *"where is the debug notification button"*, *"add auth header"*) without running brute-force `Glob`/`Grep` across the entire project.
|
|
63
67
|
- Built-in multi-lingual token scoring with automatic boosting for UI elements (`@Composable`, `Button`, `Dialog`, `Activity`, `Modifier`, etc.).
|
|
64
|
-
|
|
68
|
+
6. **Impact & Inbound/Outbound Dependency Analysis (`impact` / `pystdoc_trace_impact`)**:
|
|
65
69
|
- Instant 360-degree dependency tracing: inspect where a symbol is called from (*Inbound Callers / References*) and what it uses (*Outbound Dependencies*) before making breaking code changes or deletions.
|
|
66
|
-
|
|
70
|
+
7. **Standard LLM Options & Multi-Language Support**:
|
|
67
71
|
- Works with **Ollama, LiteRT-LM, vLLM, and OpenAI API**.
|
|
68
72
|
- Supports `--host`, `--model`, `--token` / `--api-key`, `--context-size`, and `--language` (defaults to terminal language, e.g. `Japanese`, `English`, `日本語`).
|
|
69
73
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pystdoc"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.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 = [
|
|
@@ -51,3 +51,45 @@ filterwarnings = [
|
|
|
51
51
|
"ignore::ResourceWarning",
|
|
52
52
|
"ignore::pytest.PytestUnraisableExceptionWarning",
|
|
53
53
|
]
|
|
54
|
+
|
|
55
|
+
[tool.pylint.main]
|
|
56
|
+
# Third-party libs without static member info (javalang) / libclang.
|
|
57
|
+
extension-pkg-allow-list = []
|
|
58
|
+
|
|
59
|
+
[tool.pylint.messages_control]
|
|
60
|
+
# Intentional design choices of this code base:
|
|
61
|
+
# - broad-exception-caught: parsers must never crash a whole sync run
|
|
62
|
+
# - import-outside-toplevel: optional/heavy deps are imported lazily
|
|
63
|
+
# - complexity/size metrics, duplicate-code, cyclic-import (adapter registry)
|
|
64
|
+
# - invalid-name: ast.NodeVisitor visit_* methods and module singletons
|
|
65
|
+
disable = [
|
|
66
|
+
"broad-exception-caught",
|
|
67
|
+
"import-outside-toplevel",
|
|
68
|
+
"too-many-locals",
|
|
69
|
+
"too-many-branches",
|
|
70
|
+
"too-many-statements",
|
|
71
|
+
"too-many-arguments",
|
|
72
|
+
"too-many-positional-arguments",
|
|
73
|
+
"too-many-return-statements",
|
|
74
|
+
"too-many-instance-attributes",
|
|
75
|
+
"too-many-nested-blocks",
|
|
76
|
+
"too-many-lines",
|
|
77
|
+
"too-few-public-methods",
|
|
78
|
+
"duplicate-code",
|
|
79
|
+
"cyclic-import",
|
|
80
|
+
"no-else-return",
|
|
81
|
+
"no-else-break",
|
|
82
|
+
"invalid-name",
|
|
83
|
+
"disallowed-name",
|
|
84
|
+
"global-statement",
|
|
85
|
+
"protected-access",
|
|
86
|
+
"unnecessary-ellipsis",
|
|
87
|
+
"missing-function-docstring",
|
|
88
|
+
"missing-class-docstring",
|
|
89
|
+
"missing-module-docstring",
|
|
90
|
+
"consider-using-with",
|
|
91
|
+
"attribute-defined-outside-init",
|
|
92
|
+
"cell-var-from-loop",
|
|
93
|
+
"unused-argument",
|
|
94
|
+
"no-member",
|
|
95
|
+
]
|
|
@@ -6,6 +6,7 @@ import threading
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, Dict, List, Optional, Tuple
|
|
8
8
|
|
|
9
|
+
from pystdoc.similarity import is_semantically_same
|
|
9
10
|
from pystdoc.symbols import Symbol
|
|
10
11
|
|
|
11
12
|
|
|
@@ -71,9 +72,16 @@ class DocgenDB:
|
|
|
71
72
|
target_key TEXT PRIMARY KEY,
|
|
72
73
|
input_hash TEXT NOT NULL,
|
|
73
74
|
content TEXT NOT NULL,
|
|
75
|
+
input_text TEXT,
|
|
74
76
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
75
77
|
);
|
|
76
78
|
""")
|
|
79
|
+
try:
|
|
80
|
+
self.conn.execute(
|
|
81
|
+
"ALTER TABLE design_cache ADD COLUMN input_text TEXT;"
|
|
82
|
+
)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
77
85
|
|
|
78
86
|
self.conn.execute("""
|
|
79
87
|
CREATE TABLE IF NOT EXISTS symbols_metadata (
|
|
@@ -231,41 +239,63 @@ class DocgenDB:
|
|
|
231
239
|
return None
|
|
232
240
|
|
|
233
241
|
def load_design_cache(
|
|
234
|
-
self,
|
|
242
|
+
self,
|
|
243
|
+
target_key: str,
|
|
244
|
+
current_input_hash: str,
|
|
245
|
+
input_text: Optional[str] = None,
|
|
246
|
+
similarity_threshold: float = 0.85,
|
|
235
247
|
) -> Optional[str]:
|
|
236
|
-
"""Load cached design document if input
|
|
248
|
+
"""Load cached design document if the input is unchanged.
|
|
249
|
+
|
|
250
|
+
Exact hash match is a hit. If ``input_text`` is given, a stored
|
|
251
|
+
input that is semantically similar (>= threshold) is also a hit,
|
|
252
|
+
so that wording-only changes do not cascade upward.
|
|
253
|
+
"""
|
|
237
254
|
with self.lock:
|
|
238
255
|
cur = self.conn.cursor()
|
|
239
256
|
cur.execute(
|
|
240
257
|
"""
|
|
241
|
-
SELECT content FROM design_cache
|
|
242
|
-
WHERE target_key = ?
|
|
258
|
+
SELECT content, input_hash, input_text FROM design_cache
|
|
259
|
+
WHERE target_key = ?
|
|
243
260
|
""",
|
|
244
|
-
(target_key,
|
|
261
|
+
(target_key,),
|
|
245
262
|
)
|
|
246
263
|
row = cur.fetchone()
|
|
247
|
-
|
|
248
|
-
return row["content"]
|
|
264
|
+
if not row:
|
|
249
265
|
return None
|
|
266
|
+
if row["input_hash"] == current_input_hash:
|
|
267
|
+
return row["content"]
|
|
268
|
+
if input_text is not None and row["input_text"]:
|
|
269
|
+
if is_semantically_same(
|
|
270
|
+
row["input_text"], input_text, similarity_threshold
|
|
271
|
+
):
|
|
272
|
+
return row["content"]
|
|
273
|
+
return None
|
|
250
274
|
|
|
251
275
|
def save_design_cache(
|
|
252
|
-
self,
|
|
276
|
+
self,
|
|
277
|
+
target_key: str,
|
|
278
|
+
input_hash: str,
|
|
279
|
+
content: str,
|
|
280
|
+
input_text: Optional[str] = None,
|
|
253
281
|
) -> None:
|
|
254
|
-
"""Save design doc content
|
|
282
|
+
"""Save design doc content, input hash and text (immediate commit)."""
|
|
255
283
|
with self.lock:
|
|
256
284
|
with self.conn:
|
|
257
285
|
self.conn.execute(
|
|
258
286
|
"""
|
|
259
287
|
INSERT INTO design_cache (
|
|
260
|
-
target_key, input_hash, content,
|
|
288
|
+
target_key, input_hash, content, input_text,
|
|
289
|
+
updated_at
|
|
261
290
|
)
|
|
262
|
-
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
|
291
|
+
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
263
292
|
ON CONFLICT(target_key) DO UPDATE SET
|
|
264
293
|
input_hash=excluded.input_hash,
|
|
265
294
|
content=excluded.content,
|
|
295
|
+
input_text=excluded.input_text,
|
|
266
296
|
updated_at=CURRENT_TIMESTAMP;
|
|
267
297
|
""",
|
|
268
|
-
(target_key, input_hash, content),
|
|
298
|
+
(target_key, input_hash, content, input_text),
|
|
269
299
|
)
|
|
270
300
|
|
|
271
301
|
def save_symbol_metadata(
|
|
@@ -169,6 +169,43 @@ def chunk_list_by_size(
|
|
|
169
169
|
return chunks
|
|
170
170
|
|
|
171
171
|
|
|
172
|
+
def chunk_list_stable(
|
|
173
|
+
items: List[str], max_items: int = 15, max_chars: int = 3500
|
|
174
|
+
) -> List[List[str]]:
|
|
175
|
+
"""Split items into chunks whose boundaries are content-defined.
|
|
176
|
+
|
|
177
|
+
A boundary is placed after an item whose hash hits a target value, so
|
|
178
|
+
inserting/removing/editing one item only changes the chunk(s) around
|
|
179
|
+
it; the rest of the chunks stay byte-identical (cache friendly).
|
|
180
|
+
Size limits are still enforced as hard boundaries.
|
|
181
|
+
"""
|
|
182
|
+
divisor = max(2, max_items // 2)
|
|
183
|
+
chunks: List[List[str]] = []
|
|
184
|
+
current_chunk: List[str] = []
|
|
185
|
+
current_length = 0
|
|
186
|
+
|
|
187
|
+
for item in items:
|
|
188
|
+
item_len = len(item)
|
|
189
|
+
if current_chunk and (
|
|
190
|
+
len(current_chunk) >= max_items
|
|
191
|
+
or current_length + item_len > max_chars
|
|
192
|
+
):
|
|
193
|
+
chunks.append(current_chunk)
|
|
194
|
+
current_chunk = []
|
|
195
|
+
current_length = 0
|
|
196
|
+
current_chunk.append(item)
|
|
197
|
+
current_length += item_len
|
|
198
|
+
digest = hashlib.sha1(item.encode("utf-8")).hexdigest()
|
|
199
|
+
if int(digest[:8], 16) % divisor == 0:
|
|
200
|
+
chunks.append(current_chunk)
|
|
201
|
+
current_chunk = []
|
|
202
|
+
current_length = 0
|
|
203
|
+
|
|
204
|
+
if current_chunk:
|
|
205
|
+
chunks.append(current_chunk)
|
|
206
|
+
return chunks
|
|
207
|
+
|
|
208
|
+
|
|
172
209
|
def hierarchical_reduce_summaries(
|
|
173
210
|
items: List[str],
|
|
174
211
|
category_title: str,
|
|
@@ -178,8 +215,15 @@ def hierarchical_reduce_summaries(
|
|
|
178
215
|
max_items_per_chunk: int = 15,
|
|
179
216
|
max_chars_per_chunk: int = 3500,
|
|
180
217
|
depth: int = 0,
|
|
218
|
+
db: Optional[DocgenDB] = None,
|
|
219
|
+
force: bool = False,
|
|
181
220
|
) -> str:
|
|
182
|
-
"""Recursively reduce descriptions into a concise summary.
|
|
221
|
+
"""Recursively reduce descriptions into a concise summary.
|
|
222
|
+
|
|
223
|
+
When ``db`` is given, each group summary is cached by the hash of its
|
|
224
|
+
input text, and groups are formed by content-defined chunking. Thus
|
|
225
|
+
only the groups that contain changed items are sent to the LLM again.
|
|
226
|
+
"""
|
|
183
227
|
if not items:
|
|
184
228
|
return "(None)"
|
|
185
229
|
|
|
@@ -189,15 +233,25 @@ def hierarchical_reduce_summaries(
|
|
|
189
233
|
) or depth > 5:
|
|
190
234
|
return "\n".join(items)
|
|
191
235
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
236
|
+
if db is not None:
|
|
237
|
+
chunks = chunk_list_stable(
|
|
238
|
+
sorted(items),
|
|
239
|
+
max_items=max_items_per_chunk,
|
|
240
|
+
max_chars=max_chars_per_chunk,
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
chunks = chunk_list_by_size(
|
|
244
|
+
items,
|
|
245
|
+
max_items=max_items_per_chunk,
|
|
246
|
+
max_chars=max_chars_per_chunk,
|
|
247
|
+
)
|
|
195
248
|
if len(chunks) <= 1 and depth > 0:
|
|
196
249
|
return "\n".join(items)
|
|
197
250
|
|
|
198
251
|
intermediate_summaries: List[str] = []
|
|
199
252
|
|
|
200
253
|
for idx, chunk in enumerate(chunks, 1):
|
|
254
|
+
InterruptionState.check_interrupted()
|
|
201
255
|
chunk_text = "\n".join(chunk)
|
|
202
256
|
if not llm_client:
|
|
203
257
|
summary_item = (
|
|
@@ -207,6 +261,26 @@ def hierarchical_reduce_summaries(
|
|
|
207
261
|
intermediate_summaries.append(summary_item)
|
|
208
262
|
continue
|
|
209
263
|
|
|
264
|
+
group_cache_key = ""
|
|
265
|
+
group_hash = ""
|
|
266
|
+
if db is not None:
|
|
267
|
+
group_hash = hashlib.sha256(
|
|
268
|
+
f"{language}\n{category_title}\n{chunk_text}".encode("utf-8")
|
|
269
|
+
).hexdigest()
|
|
270
|
+
group_cache_key = (
|
|
271
|
+
f"reduce::{language}::{category_title}::{group_hash}"
|
|
272
|
+
)
|
|
273
|
+
if not force:
|
|
274
|
+
cached_summary = db.load_design_cache(
|
|
275
|
+
group_cache_key, group_hash
|
|
276
|
+
)
|
|
277
|
+
if cached_summary:
|
|
278
|
+
# Header without index keeps upper levels stable.
|
|
279
|
+
intermediate_summaries.append(
|
|
280
|
+
f"#### Group Summary\n{cached_summary.strip()}"
|
|
281
|
+
)
|
|
282
|
+
continue
|
|
283
|
+
|
|
210
284
|
grp_info = f"(Group {idx}/{len(chunks)})"
|
|
211
285
|
prompt = f"""Please analyze {category_title} {grp_info}
|
|
212
286
|
and create a concise, factual summary (200-400 words) in {language}.
|
|
@@ -231,9 +305,17 @@ Output as Markdown bullet points in {language}:"""
|
|
|
231
305
|
|
|
232
306
|
try:
|
|
233
307
|
summary = llm_client.chat_completion(messages)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
308
|
+
if db is not None:
|
|
309
|
+
db.save_design_cache(
|
|
310
|
+
group_cache_key, group_hash, summary.strip()
|
|
311
|
+
)
|
|
312
|
+
intermediate_summaries.append(
|
|
313
|
+
f"#### Group Summary\n{summary.strip()}"
|
|
314
|
+
)
|
|
315
|
+
else:
|
|
316
|
+
intermediate_summaries.append(
|
|
317
|
+
f"#### Group {idx} Summary\n{summary.strip()}"
|
|
318
|
+
)
|
|
237
319
|
except Exception as e:
|
|
238
320
|
if not allow_fallback:
|
|
239
321
|
raise LLMError(
|
|
@@ -257,6 +339,8 @@ Output as Markdown bullet points in {language}:"""
|
|
|
257
339
|
max_items_per_chunk=max_items_per_chunk,
|
|
258
340
|
max_chars_per_chunk=max_chars_per_chunk,
|
|
259
341
|
depth=depth + 1,
|
|
342
|
+
db=db,
|
|
343
|
+
force=force,
|
|
260
344
|
)
|
|
261
345
|
|
|
262
346
|
return "\n\n".join(intermediate_summaries)
|
|
@@ -292,7 +376,9 @@ def generate_data_models_doc(
|
|
|
292
376
|
out_file = target_dir / ".pystdoc" / "design" / "data_models.md"
|
|
293
377
|
|
|
294
378
|
if db and not force and out_file.exists():
|
|
295
|
-
cached_content = db.load_design_cache(
|
|
379
|
+
cached_content = db.load_design_cache(
|
|
380
|
+
cache_key, input_hash, combined_input
|
|
381
|
+
)
|
|
296
382
|
if cached_content:
|
|
297
383
|
if tracker:
|
|
298
384
|
tracker.advance(1, extra="[Cached]: data_models.md")
|
|
@@ -305,20 +391,37 @@ def generate_data_models_doc(
|
|
|
305
391
|
if tracker:
|
|
306
392
|
tracker.render_current(extra="data_models.md")
|
|
307
393
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
"
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
394
|
+
total_types_len = sum(len(it) for it in types_raw)
|
|
395
|
+
if total_types_len <= 15000 or len(types_raw) <= 50:
|
|
396
|
+
reduced_types_summary = "\n".join(types_raw) if types_raw else "(None)"
|
|
397
|
+
else:
|
|
398
|
+
reduced_types_summary = hierarchical_reduce_summaries(
|
|
399
|
+
types_raw,
|
|
400
|
+
"Type Definitions",
|
|
401
|
+
llm_client,
|
|
402
|
+
language=norm_lang,
|
|
403
|
+
allow_fallback=allow_fallback,
|
|
404
|
+
max_items_per_chunk=30,
|
|
405
|
+
max_chars_per_chunk=8000,
|
|
406
|
+
db=db,
|
|
407
|
+
force=force,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
total_vars_len = sum(len(it) for it in vars_raw)
|
|
411
|
+
if total_vars_len <= 15000 or len(vars_raw) <= 50:
|
|
412
|
+
reduced_vars_summary = "\n".join(vars_raw) if vars_raw else "(None)"
|
|
413
|
+
else:
|
|
414
|
+
reduced_vars_summary = hierarchical_reduce_summaries(
|
|
415
|
+
vars_raw,
|
|
416
|
+
"Variables and Fields",
|
|
417
|
+
llm_client,
|
|
418
|
+
language=norm_lang,
|
|
419
|
+
allow_fallback=allow_fallback,
|
|
420
|
+
max_items_per_chunk=30,
|
|
421
|
+
max_chars_per_chunk=8000,
|
|
422
|
+
db=db,
|
|
423
|
+
force=force,
|
|
424
|
+
)
|
|
322
425
|
|
|
323
426
|
prompt = f"""Please analyze aggregated types and variables/fields,
|
|
324
427
|
and create a factual "Data Models & Data Structure Design" doc in {norm_lang}.
|
|
@@ -405,10 +508,48 @@ Structure the Markdown as follows:
|
|
|
405
508
|
|
|
406
509
|
write_flushed_text(out_file, content.strip() + "\n")
|
|
407
510
|
if db:
|
|
408
|
-
db.save_design_cache(
|
|
511
|
+
db.save_design_cache(
|
|
512
|
+
cache_key, input_hash, content, combined_input
|
|
513
|
+
)
|
|
409
514
|
return content
|
|
410
515
|
|
|
411
516
|
|
|
517
|
+
def select_architectural_functions(
|
|
518
|
+
fn_docs: List[Dict[str, Any]], max_count: int = 60
|
|
519
|
+
) -> List[Dict[str, Any]]:
|
|
520
|
+
"""Select key architectural functions (entry points, pipelines).
|
|
521
|
+
|
|
522
|
+
Instead of passing hundreds of utility/test/leaf functions, we prioritize:
|
|
523
|
+
1. Functions with 'main', 'cli', 'run', 'start', 'handler', 'app', 'serve'.
|
|
524
|
+
2. Functions with UI interaction roles.
|
|
525
|
+
3. Functions that call many others (pipeline orchestrators).
|
|
526
|
+
4. Functions with no callers inside the project (root entry points).
|
|
527
|
+
"""
|
|
528
|
+
if len(fn_docs) <= max_count:
|
|
529
|
+
return fn_docs
|
|
530
|
+
|
|
531
|
+
def score(d: Dict[str, Any]) -> int:
|
|
532
|
+
fname = d["file_name"].lower()
|
|
533
|
+
s = 0
|
|
534
|
+
if any(
|
|
535
|
+
k in fname
|
|
536
|
+
for k in (
|
|
537
|
+
"main", "cli", "run", "start", "handler", "app", "serve",
|
|
538
|
+
"pipeline", "engine", "orchestrat"
|
|
539
|
+
)
|
|
540
|
+
):
|
|
541
|
+
s += 20
|
|
542
|
+
if d.get("ui_role"):
|
|
543
|
+
s += 15
|
|
544
|
+
s += len(d.get("callees") or []) * 2
|
|
545
|
+
if not d.get("referencing_funcs"):
|
|
546
|
+
s += 5
|
|
547
|
+
return s
|
|
548
|
+
|
|
549
|
+
scored = sorted(fn_docs, key=score, reverse=True)
|
|
550
|
+
return scored[:max_count]
|
|
551
|
+
|
|
552
|
+
|
|
412
553
|
def generate_execution_model_doc(
|
|
413
554
|
fn_docs: List[Dict[str, Any]],
|
|
414
555
|
llm_client: Optional[LLMClient],
|
|
@@ -420,11 +561,15 @@ def generate_execution_model_doc(
|
|
|
420
561
|
ui_info: Optional[Dict[str, Any]] = None,
|
|
421
562
|
tracker: Optional[PhaseProgressTracker] = None,
|
|
422
563
|
) -> str:
|
|
423
|
-
"""Step 2: Generate execution_model.md with
|
|
564
|
+
"""Step 2: Generate execution_model.md with filtered functions."""
|
|
424
565
|
norm_lang = normalize_language(language)
|
|
566
|
+
|
|
567
|
+
# 1. Filter to core entry points and high-level callers
|
|
568
|
+
arch_funcs = select_architectural_functions(fn_docs, max_count=60)
|
|
569
|
+
|
|
425
570
|
funcs_raw = []
|
|
426
|
-
for d in
|
|
427
|
-
callees_str = ", ".join(d["callees"][:
|
|
571
|
+
for d in arch_funcs:
|
|
572
|
+
callees_str = ", ".join(d["callees"][:4]) if d["callees"] else "None"
|
|
428
573
|
ui_extra = f" / UI Role: {d['ui_role']}" if d.get("ui_role") else ""
|
|
429
574
|
line_desc = (
|
|
430
575
|
f"- Function `{d['file_name']}` (Signature: `{d['signature']}`): "
|
|
@@ -438,7 +583,9 @@ def generate_execution_model_doc(
|
|
|
438
583
|
out_file = target_dir / ".pystdoc" / "design" / "execution_model.md"
|
|
439
584
|
|
|
440
585
|
if db and not force and out_file.exists():
|
|
441
|
-
cached_content = db.load_design_cache(
|
|
586
|
+
cached_content = db.load_design_cache(
|
|
587
|
+
cache_key, input_hash, combined_input
|
|
588
|
+
)
|
|
442
589
|
if cached_content:
|
|
443
590
|
if tracker:
|
|
444
591
|
tracker.advance(
|
|
@@ -453,13 +600,22 @@ def generate_execution_model_doc(
|
|
|
453
600
|
if tracker:
|
|
454
601
|
tracker.render_current(extra="execution_model.md")
|
|
455
602
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
603
|
+
# If items fit comfortably in a single LLM context, directly use them
|
|
604
|
+
total_raw_len = sum(len(it) for it in funcs_raw)
|
|
605
|
+
if total_raw_len <= 18000 or len(funcs_raw) <= 60:
|
|
606
|
+
reduced_funcs_summary = "\n".join(funcs_raw)
|
|
607
|
+
else:
|
|
608
|
+
reduced_funcs_summary = hierarchical_reduce_summaries(
|
|
609
|
+
funcs_raw,
|
|
610
|
+
"Function Call Structures",
|
|
611
|
+
llm_client,
|
|
612
|
+
language=norm_lang,
|
|
613
|
+
allow_fallback=allow_fallback,
|
|
614
|
+
max_items_per_chunk=30,
|
|
615
|
+
max_chars_per_chunk=8000,
|
|
616
|
+
db=db,
|
|
617
|
+
force=force,
|
|
618
|
+
)
|
|
463
619
|
|
|
464
620
|
ui_context_text = ""
|
|
465
621
|
if ui_info and ui_info.get("has_ui"):
|
|
@@ -554,7 +710,9 @@ Structure the Markdown as follows:
|
|
|
554
710
|
|
|
555
711
|
write_flushed_text(out_file, content.strip() + "\n")
|
|
556
712
|
if db:
|
|
557
|
-
db.save_design_cache(
|
|
713
|
+
db.save_design_cache(
|
|
714
|
+
cache_key, input_hash, content, combined_input
|
|
715
|
+
)
|
|
558
716
|
return content
|
|
559
717
|
|
|
560
718
|
|
|
@@ -592,7 +750,9 @@ def generate_module_docs(
|
|
|
592
750
|
)
|
|
593
751
|
|
|
594
752
|
if db and not force and out_file.exists():
|
|
595
|
-
cached_content = db.load_design_cache(
|
|
753
|
+
cached_content = db.load_design_cache(
|
|
754
|
+
cache_key, input_hash, combined_input
|
|
755
|
+
)
|
|
596
756
|
if cached_content:
|
|
597
757
|
if tracker:
|
|
598
758
|
tracker.advance(
|
|
@@ -615,6 +775,8 @@ def generate_module_docs(
|
|
|
615
775
|
llm_client,
|
|
616
776
|
language=norm_lang,
|
|
617
777
|
allow_fallback=allow_fallback,
|
|
778
|
+
db=db,
|
|
779
|
+
force=force,
|
|
618
780
|
)
|
|
619
781
|
|
|
620
782
|
prompt = f"""Please analyze elements in module `{mod_name}`
|
|
@@ -700,7 +862,9 @@ Structure the Markdown as follows:
|
|
|
700
862
|
|
|
701
863
|
write_flushed_text(out_file, content.strip() + "\n")
|
|
702
864
|
if db:
|
|
703
|
-
db.save_design_cache(
|
|
865
|
+
db.save_design_cache(
|
|
866
|
+
cache_key, input_hash, content, combined_input
|
|
867
|
+
)
|
|
704
868
|
module_summaries[mod_name] = content
|
|
705
869
|
|
|
706
870
|
return module_summaries
|
|
@@ -740,7 +904,9 @@ def generate_overview_doc(
|
|
|
740
904
|
out_file = target_dir / ".pystdoc" / "design" / "overview.md"
|
|
741
905
|
|
|
742
906
|
if db and not force and out_file.exists():
|
|
743
|
-
cached_content = db.load_design_cache(
|
|
907
|
+
cached_content = db.load_design_cache(
|
|
908
|
+
cache_key, input_hash, combined_input
|
|
909
|
+
)
|
|
744
910
|
if cached_content:
|
|
745
911
|
if tracker:
|
|
746
912
|
tracker.advance(1, extra="[Cached]: overview.md")
|
|
@@ -758,6 +924,8 @@ def generate_overview_doc(
|
|
|
758
924
|
llm_client,
|
|
759
925
|
language=norm_lang,
|
|
760
926
|
allow_fallback=allow_fallback,
|
|
927
|
+
db=db,
|
|
928
|
+
force=force,
|
|
761
929
|
)
|
|
762
930
|
|
|
763
931
|
ui_overview_text = ""
|
|
@@ -852,7 +1020,9 @@ Requirements:
|
|
|
852
1020
|
|
|
853
1021
|
write_flushed_text(out_file, content.strip() + "\n")
|
|
854
1022
|
if db:
|
|
855
|
-
db.save_design_cache(
|
|
1023
|
+
db.save_design_cache(
|
|
1024
|
+
cache_key, input_hash, content, combined_input
|
|
1025
|
+
)
|
|
856
1026
|
return content
|
|
857
1027
|
|
|
858
1028
|
|
|
@@ -46,6 +46,7 @@ from pystdoc.progress import (
|
|
|
46
46
|
)
|
|
47
47
|
from pystdoc.interrupt import InterruptionState
|
|
48
48
|
from pystdoc.perf import PerfProfileManager
|
|
49
|
+
from pystdoc.similarity import similarity
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
class FileLock:
|
|
@@ -58,7 +59,7 @@ class FileLock:
|
|
|
58
59
|
|
|
59
60
|
def __enter__(self):
|
|
60
61
|
self.lock_file.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
-
self.fd = open(self.lock_file, "w")
|
|
62
|
+
self.fd = open(self.lock_file, "w", encoding="utf-8")
|
|
62
63
|
try:
|
|
63
64
|
fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
64
65
|
except BlockingIOError:
|
|
@@ -130,18 +131,6 @@ def check_external_interface_changed(
|
|
|
130
131
|
if old_cache is None:
|
|
131
132
|
return True
|
|
132
133
|
|
|
133
|
-
def get_tokens(text: Optional[str]) -> Set[str]:
|
|
134
|
-
clean = re.sub(
|
|
135
|
-
r"[.,。、::\-_\(\)()「」『』`\"'\s\r\n\t]+", " ", text or ""
|
|
136
|
-
)
|
|
137
|
-
words = set(clean.lower().split())
|
|
138
|
-
# Add 2-grams for CJK strings
|
|
139
|
-
compact = "".join(clean.lower().split())
|
|
140
|
-
if len(compact) >= 2:
|
|
141
|
-
for i in range(len(compact) - 1):
|
|
142
|
-
words.add(compact[i:i+2])
|
|
143
|
-
return words
|
|
144
|
-
|
|
145
134
|
def normalize_spec_text(text: Optional[str]) -> str:
|
|
146
135
|
if not text:
|
|
147
136
|
return ""
|
|
@@ -160,28 +149,22 @@ def check_external_interface_changed(
|
|
|
160
149
|
if old_in != new_in or old_out != new_out:
|
|
161
150
|
if bool(old_in) != bool(new_in) or bool(old_out) != bool(new_out):
|
|
162
151
|
return True
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if t_out1 and t_out2:
|
|
172
|
-
jaccard_out = len(t_out1 & t_out2) / max(len(t_out1 | t_out2), 1)
|
|
173
|
-
if jaccard_out < 0.5:
|
|
174
|
-
return True
|
|
152
|
+
if old_in and new_in and similarity(
|
|
153
|
+
old_cache.get("inputs_note"), new_sym.inputs_note
|
|
154
|
+
) < 0.5:
|
|
155
|
+
return True
|
|
156
|
+
if old_out and new_out and similarity(
|
|
157
|
+
old_cache.get("outputs_note"), new_sym.outputs_note
|
|
158
|
+
) < 0.5:
|
|
159
|
+
return True
|
|
175
160
|
|
|
176
161
|
# Check purpose
|
|
177
162
|
if old_p == new_p:
|
|
178
163
|
return False
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if jaccard >= 0.5:
|
|
184
|
-
return False
|
|
164
|
+
if old_p and new_p and similarity(
|
|
165
|
+
old_cache.get("purpose"), new_sym.purpose
|
|
166
|
+
) >= 0.5:
|
|
167
|
+
return False
|
|
185
168
|
|
|
186
169
|
return True
|
|
187
170
|
|
|
@@ -924,7 +907,7 @@ def run_docgen(
|
|
|
924
907
|
)
|
|
925
908
|
|
|
926
909
|
# Process Level by Level
|
|
927
|
-
for
|
|
910
|
+
for lvl_nodes in level_groups:
|
|
928
911
|
InterruptionState.check_interrupted()
|
|
929
912
|
executor = concurrent.futures.ThreadPoolExecutor(
|
|
930
913
|
max_workers=concurrency,
|
|
@@ -1093,18 +1076,18 @@ def run_docgen(
|
|
|
1093
1076
|
parent_container_info is not None
|
|
1094
1077
|
and any(
|
|
1095
1078
|
t.symbol.name == parent_container_info["name"]
|
|
1096
|
-
and t.unique_id in
|
|
1079
|
+
and t.unique_id in interface_changed_symbol_ids
|
|
1097
1080
|
for t in type_nodes
|
|
1098
1081
|
if t.rel_path == v_rel
|
|
1099
1082
|
)
|
|
1100
1083
|
)
|
|
1101
1084
|
is_parent_func_updated = (
|
|
1102
1085
|
any(
|
|
1103
|
-
caller_id in
|
|
1086
|
+
caller_id in interface_changed_symbol_ids
|
|
1104
1087
|
for caller_id in v_node.direct_caller_ids
|
|
1105
1088
|
)
|
|
1106
1089
|
or any(
|
|
1107
|
-
f_node.unique_id in
|
|
1090
|
+
f_node.unique_id in interface_changed_symbol_ids
|
|
1108
1091
|
for f_node in fn_nodes
|
|
1109
1092
|
if f_node.rel_path == v_rel
|
|
1110
1093
|
and (
|
|
@@ -1182,10 +1165,14 @@ def run_docgen(
|
|
|
1182
1165
|
or top_down_res.get("significance")
|
|
1183
1166
|
or top_down_res.get("top_down_summary")
|
|
1184
1167
|
)
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1168
|
+
v_sym.top_down_context = sanitize_architectural_context(
|
|
1169
|
+
raw_v_role,
|
|
1170
|
+
fallback_text=(
|
|
1171
|
+
v_sym.top_down_context
|
|
1172
|
+
or v_sym.purpose
|
|
1173
|
+
or v_sym.name
|
|
1174
|
+
),
|
|
1175
|
+
)
|
|
1189
1176
|
if top_down_res.get("purpose"):
|
|
1190
1177
|
v_sym.purpose = top_down_res["purpose"]
|
|
1191
1178
|
if top_down_res.get("overview"):
|
|
@@ -15,8 +15,6 @@ from pystdoc.interrupt import InterruptionState, interruptible_sleep
|
|
|
15
15
|
class LLMError(RuntimeError):
|
|
16
16
|
"""Exception raised when LLM generation fails in strict mode."""
|
|
17
17
|
|
|
18
|
-
pass
|
|
19
|
-
|
|
20
18
|
|
|
21
19
|
def normalize_host_url(host: Optional[str]) -> str:
|
|
22
20
|
"""Normalize user-provided host string into a standard base URL."""
|
|
@@ -244,7 +242,8 @@ class LLMClient:
|
|
|
244
242
|
"""Detect WSL default gateway host IP via ip route or resolv.conf."""
|
|
245
243
|
try:
|
|
246
244
|
res = subprocess.run(
|
|
247
|
-
["ip", "route"], capture_output=True, text=True, timeout=1
|
|
245
|
+
["ip", "route"], capture_output=True, text=True, timeout=1,
|
|
246
|
+
check=False
|
|
248
247
|
)
|
|
249
248
|
for line in res.stdout.splitlines():
|
|
250
249
|
if line.startswith("default via"):
|
|
@@ -253,7 +252,7 @@ class LLMClient:
|
|
|
253
252
|
pass
|
|
254
253
|
|
|
255
254
|
try:
|
|
256
|
-
resolv = Path("/etc/resolv.conf").read_text()
|
|
255
|
+
resolv = Path("/etc/resolv.conf").read_text(encoding="utf-8")
|
|
257
256
|
for line in resolv.splitlines():
|
|
258
257
|
if line.strip().startswith("nameserver"):
|
|
259
258
|
return line.split()[1].strip()
|
|
@@ -401,7 +401,6 @@ def create_mcp_server(
|
|
|
401
401
|
|
|
402
402
|
# Capture output or format response
|
|
403
403
|
from io import StringIO
|
|
404
|
-
import sys
|
|
405
404
|
old_stdout = sys.stdout
|
|
406
405
|
old_stderr = sys.stderr
|
|
407
406
|
capture_out = StringIO()
|
|
@@ -439,7 +438,6 @@ def create_mcp_server(
|
|
|
439
438
|
return _missing_docgen_msg(target_dir)
|
|
440
439
|
|
|
441
440
|
from io import StringIO
|
|
442
|
-
import sys
|
|
443
441
|
old_stdout = sys.stdout
|
|
444
442
|
old_stderr = sys.stderr
|
|
445
443
|
capture = StringIO()
|
|
@@ -547,7 +545,6 @@ def create_mcp_server(
|
|
|
547
545
|
if not docgen_dir.exists():
|
|
548
546
|
return _missing_docgen_msg(target_dir)
|
|
549
547
|
from io import StringIO
|
|
550
|
-
import sys
|
|
551
548
|
old_stdout = sys.stdout
|
|
552
549
|
capture = StringIO()
|
|
553
550
|
try:
|
|
@@ -266,7 +266,7 @@ class PerfProfileManager:
|
|
|
266
266
|
|
|
267
267
|
if not coeffs:
|
|
268
268
|
all_cats = self.data.get(key, {}).get("categories", {})
|
|
269
|
-
for
|
|
269
|
+
for c_v in all_cats.values():
|
|
270
270
|
if c_v.get("coeffs"):
|
|
271
271
|
coeffs = c_v["coeffs"]
|
|
272
272
|
break
|
|
@@ -300,7 +300,7 @@ class PerfProfileManager:
|
|
|
300
300
|
chars_coeffs = cat_data.get("chars_coeffs")
|
|
301
301
|
if not chars_coeffs:
|
|
302
302
|
all_cats = self.data.get(key, {}).get("categories", {})
|
|
303
|
-
for
|
|
303
|
+
for c_v in all_cats.values():
|
|
304
304
|
if c_v.get("chars_coeffs"):
|
|
305
305
|
chars_coeffs = c_v["chars_coeffs"]
|
|
306
306
|
break
|
|
@@ -456,7 +456,7 @@ def locate_features(target_dir: Path, query: str, limit: int = 10) -> str:
|
|
|
456
456
|
(
|
|
457
457
|
uid, sname, skind, rpath,
|
|
458
458
|
lstart, lend, fqdn, sig,
|
|
459
|
-
|
|
459
|
+
_ref_funcs_raw, _callees_raw,
|
|
460
460
|
purp, ovw
|
|
461
461
|
) = row
|
|
462
462
|
sname_l = (sname or "").lower()
|
|
@@ -142,7 +142,9 @@ def generate_readme_doc(
|
|
|
142
142
|
cache_key = f"report::readme::{norm_lang}"
|
|
143
143
|
|
|
144
144
|
if db and not force and out_file.exists():
|
|
145
|
-
cached_content = db.load_design_cache(
|
|
145
|
+
cached_content = db.load_design_cache(
|
|
146
|
+
cache_key, input_hash, combined_input
|
|
147
|
+
)
|
|
146
148
|
if cached_content:
|
|
147
149
|
tracker.advance(1, extra="[Cached]: .pystdoc/README.md")
|
|
148
150
|
tracker.finish()
|
|
@@ -347,5 +349,7 @@ topological dependency mapping.
|
|
|
347
349
|
|
|
348
350
|
write_flushed_text(out_file, content.strip() + "\n")
|
|
349
351
|
if db:
|
|
350
|
-
db.save_design_cache(
|
|
352
|
+
db.save_design_cache(
|
|
353
|
+
cache_key, input_hash, content, combined_input
|
|
354
|
+
)
|
|
351
355
|
return content
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Lightweight, deterministic semantic-similarity helpers (no LLM calls).
|
|
2
|
+
|
|
3
|
+
Used to decide whether a regenerated description is "essentially the same"
|
|
4
|
+
as the previous one, so that change propagation can stop early.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Optional, Set
|
|
9
|
+
|
|
10
|
+
_PUNCT_RE = re.compile(r"[.,。、::;;!?!?\-_()()「」『』`\"'\[\]{}*#>|/\\\s]+")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_tokens(text: Optional[str]) -> Set[str]:
|
|
14
|
+
"""Tokenize into lowercase words plus character 2-grams (for CJK)."""
|
|
15
|
+
clean = _PUNCT_RE.sub(" ", (text or "").lower()).strip()
|
|
16
|
+
words = set(clean.split())
|
|
17
|
+
compact = clean.replace(" ", "")
|
|
18
|
+
for i in range(len(compact) - 1):
|
|
19
|
+
words.add(compact[i:i + 2])
|
|
20
|
+
return words
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def similarity(a: Optional[str], b: Optional[str]) -> float:
|
|
24
|
+
"""Return the max of Jaccard and overlap coefficient (0.0 - 1.0).
|
|
25
|
+
|
|
26
|
+
The overlap coefficient tolerates elaboration (one text containing the
|
|
27
|
+
other) while Jaccard penalizes unrelated text.
|
|
28
|
+
"""
|
|
29
|
+
t1, t2 = get_tokens(a), get_tokens(b)
|
|
30
|
+
if not t1 and not t2:
|
|
31
|
+
return 1.0
|
|
32
|
+
if not t1 or not t2:
|
|
33
|
+
return 0.0
|
|
34
|
+
inter = len(t1 & t2)
|
|
35
|
+
jaccard = inter / len(t1 | t2)
|
|
36
|
+
overlap = inter / min(len(t1), len(t2))
|
|
37
|
+
# overlap alone is too lenient for very different sizes; damp it.
|
|
38
|
+
size_ratio = min(len(t1), len(t2)) / max(len(t1), len(t2))
|
|
39
|
+
return max(jaccard, overlap * (0.5 + 0.5 * size_ratio))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_semantically_same(
|
|
43
|
+
a: Optional[str], b: Optional[str], threshold: float = 0.85
|
|
44
|
+
) -> bool:
|
|
45
|
+
return similarity(a, b) >= threshold
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pystdoc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -78,21 +78,25 @@ 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. **
|
|
81
|
+
2. **High-Speed Incremental Sync & Semantic Change Propagation (v0.8.0)**:
|
|
82
|
+
- **Selective Bottom-Up Propagation**: Detects AST and logic-level code differences. If regenerated specifications remain semantically unchanged (evaluated by deterministic Jaccard / token-overlap metrics), change propagation terminates immediately without triggering cascading LLM re-analysis across upper modules.
|
|
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
|
+
- **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%+.
|
|
85
|
+
3. **3-in-1 Unified Documentation Pipeline**:
|
|
82
86
|
- `docgen`: Bottom-up & top-down symbol-level documentation with SHA-256 and SQLite caching (`.pystdoc/documents/`).
|
|
83
87
|
- `designgen`: Map-Reduce architectural synthesis (`.pystdoc/design/`).
|
|
84
88
|
- `reportgen` / `pystdoc`: Executive summary README (`.pystdoc/README.md`) answering *"What does this project actually do?"*
|
|
85
|
-
|
|
89
|
+
4. **Multi-Language Codebase Deep Understanding (C/C++, Python, Java, Kotlin, Shell)**:
|
|
86
90
|
- **C/C++**: `compile_commands.json` integration, include path resolution, and macro expansion via `libclang`.
|
|
87
91
|
- **Java / Kotlin**: AST parsing via `javalang` & `kopyt` with class hierarchy, companion objects, and data classes.
|
|
88
92
|
- **Python & Shell**: AST visitor and generic regex parser with import/call extraction.
|
|
89
93
|
- **Fully Qualified Domain Names (FQDN)**: Disambiguates identical symbol names across large monorepos.
|
|
90
|
-
|
|
94
|
+
5. **Feature Location & UI Component Pinpointing (`locate` / `pystdoc_locate_feature`)**:
|
|
91
95
|
- Pinpoint exact files, functions, UI elements, and line ranges (`Lines X-Y`) from high-level natural language queries (e.g., *"where is the debug notification button"*, *"add auth header"*) without running brute-force `Glob`/`Grep` across the entire project.
|
|
92
96
|
- Built-in multi-lingual token scoring with automatic boosting for UI elements (`@Composable`, `Button`, `Dialog`, `Activity`, `Modifier`, etc.).
|
|
93
|
-
|
|
97
|
+
6. **Impact & Inbound/Outbound Dependency Analysis (`impact` / `pystdoc_trace_impact`)**:
|
|
94
98
|
- Instant 360-degree dependency tracing: inspect where a symbol is called from (*Inbound Callers / References*) and what it uses (*Outbound Dependencies*) before making breaking code changes or deletions.
|
|
95
|
-
|
|
99
|
+
7. **Standard LLM Options & Multi-Language Support**:
|
|
96
100
|
- Works with **Ollama, LiteRT-LM, vLLM, and OpenAI API**.
|
|
97
101
|
- Supports `--host`, `--model`, `--token` / `--api-key`, `--context-size`, and `--language` (defaults to terminal language, e.g. `Japanese`, `English`, `日本語`).
|
|
98
102
|
|
|
@@ -26,6 +26,7 @@ src/pystdoc/progress.py
|
|
|
26
26
|
src/pystdoc/query.py
|
|
27
27
|
src/pystdoc/report_engine.py
|
|
28
28
|
src/pystdoc/scanner.py
|
|
29
|
+
src/pystdoc/similarity.py
|
|
29
30
|
src/pystdoc/symbols.py
|
|
30
31
|
src/pystdoc/ui_detector.py
|
|
31
32
|
src/pystdoc/watcher.py
|
|
@@ -63,6 +64,7 @@ tests/test_pep8.py
|
|
|
63
64
|
tests/test_perf.py
|
|
64
65
|
tests/test_progress.py
|
|
65
66
|
tests/test_query.py
|
|
67
|
+
tests/test_reduce_cache.py
|
|
66
68
|
tests/test_reportgen.py
|
|
67
69
|
tests/test_ui_detector.py
|
|
68
70
|
tests/test_watcher.py
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Tests for group-level caching in hierarchical_reduce_summaries."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
import unittest
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from unittest.mock import MagicMock
|
|
7
|
+
|
|
8
|
+
from pystdoc.db import DocgenDB
|
|
9
|
+
from pystdoc.design_engine import (
|
|
10
|
+
chunk_list_stable,
|
|
11
|
+
hierarchical_reduce_summaries,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _items(n, changed=None):
|
|
16
|
+
out = []
|
|
17
|
+
for i in range(n):
|
|
18
|
+
text = f"- Function `mod{i:04d}.fn.f{i}.md`: purpose of item {i} " * 3
|
|
19
|
+
if changed is not None and i == changed:
|
|
20
|
+
text += " CHANGED"
|
|
21
|
+
out.append(text)
|
|
22
|
+
return out
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestReduceCache(unittest.TestCase):
|
|
26
|
+
def setUp(self):
|
|
27
|
+
self.tmp = Path(tempfile.mkdtemp())
|
|
28
|
+
self.db = DocgenDB(self.tmp / "index.db")
|
|
29
|
+
|
|
30
|
+
def tearDown(self):
|
|
31
|
+
self.db.close()
|
|
32
|
+
|
|
33
|
+
def test_stable_chunking_localizes_change(self):
|
|
34
|
+
a = chunk_list_stable(_items(300))
|
|
35
|
+
b = chunk_list_stable(_items(300, changed=150))
|
|
36
|
+
changed = sum(1 for c in b if c not in a)
|
|
37
|
+
self.assertGreater(len(a), 5)
|
|
38
|
+
self.assertLessEqual(changed, 2)
|
|
39
|
+
self.assertEqual(sum(len(c) for c in a), 300)
|
|
40
|
+
|
|
41
|
+
def test_group_cache_only_recomputes_changed_groups(self):
|
|
42
|
+
llm = MagicMock()
|
|
43
|
+
llm.chat_completion.side_effect = lambda m: "summary"
|
|
44
|
+
hierarchical_reduce_summaries(
|
|
45
|
+
_items(300), "Funcs", llm, language="Japanese", db=self.db
|
|
46
|
+
)
|
|
47
|
+
first_calls = llm.chat_completion.call_count
|
|
48
|
+
self.assertGreater(first_calls, 5)
|
|
49
|
+
|
|
50
|
+
llm.chat_completion.reset_mock()
|
|
51
|
+
hierarchical_reduce_summaries(
|
|
52
|
+
_items(300), "Funcs", llm, language="Japanese", db=self.db
|
|
53
|
+
)
|
|
54
|
+
self.assertEqual(llm.chat_completion.call_count, 0)
|
|
55
|
+
|
|
56
|
+
llm.chat_completion.reset_mock()
|
|
57
|
+
hierarchical_reduce_summaries(
|
|
58
|
+
_items(300, changed=150), "Funcs", llm,
|
|
59
|
+
language="Japanese", db=self.db,
|
|
60
|
+
)
|
|
61
|
+
self.assertLessEqual(llm.chat_completion.call_count, 4)
|
|
62
|
+
self.assertGreater(llm.chat_completion.call_count, 0)
|
|
63
|
+
|
|
64
|
+
def test_force_bypasses_group_cache(self):
|
|
65
|
+
llm = MagicMock()
|
|
66
|
+
llm.chat_completion.side_effect = lambda m: "summary"
|
|
67
|
+
hierarchical_reduce_summaries(
|
|
68
|
+
_items(100), "Funcs", llm, db=self.db
|
|
69
|
+
)
|
|
70
|
+
llm.chat_completion.reset_mock()
|
|
71
|
+
hierarchical_reduce_summaries(
|
|
72
|
+
_items(100), "Funcs", llm, db=self.db, force=True
|
|
73
|
+
)
|
|
74
|
+
self.assertGreater(llm.chat_completion.call_count, 0)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|