readmenator 1.0.0__py3-none-any.whl

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.

Potentially problematic release.


This version of readmenator might be problematic. Click here for more details.

@@ -0,0 +1,20 @@
1
+ """Public API for the readmenator knowledge graph generator.
2
+
3
+ Export the core types and application class used by external consumers:
4
+ - Config: immutable settings dataclass
5
+ - Symbol, Node, Edge: data model for codebase entities and relations
6
+ - readmenatorApplication: high-level orchestrator for scanning, querying,
7
+ and generating the KNOWLEDGE_BASE.md artifact
8
+ """
9
+
10
+ from readmenator._app import readmenatorApplication
11
+ from readmenator._config import Config
12
+ from readmenator._models import Edge, Node, Symbol
13
+
14
+ __all__ = [
15
+ "Config",
16
+ "Symbol",
17
+ "Node",
18
+ "Edge",
19
+ "readmenatorApplication",
20
+ ]
@@ -0,0 +1,125 @@
1
+ """CLI entry point for readmenator.
2
+
3
+ Parses command-line arguments, dispatches to the appropriate subcommand
4
+ (query, explain, path, summary, --rebuild, --test), and manages the
5
+ target directory analysis lifecycle.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import os
12
+ import sys
13
+ import unittest
14
+ from pathlib import Path
15
+
16
+ from readmenator._app import readmenatorApplication
17
+
18
+
19
+ def build_parser() -> argparse.ArgumentParser:
20
+ """Construct the argument parser with subcommand help and examples."""
21
+ parser = argparse.ArgumentParser(
22
+ description="ReadMenator: Zero-token polyglot codebase knowledge graph generator.",
23
+ formatter_class=argparse.RawDescriptionHelpFormatter,
24
+ epilog=(
25
+ "Subcommands:\n"
26
+ " query \"<question>\" Answer a question using the knowledge base\n"
27
+ " explain \"<symbol>\" Explain a symbol with relationships\n"
28
+ " path \"<A>\" \"<B>\" Trace dependency chain between two symbols\n"
29
+ "\n"
30
+ "Examples:\n"
31
+ " python -m readmenator /path/to/project\n"
32
+ " python -m readmenator /path/to/project explain ClassName\n"
33
+ " python -m readmenator . query \"What classes handle HTTP?\"\n"
34
+ ),
35
+ )
36
+ parser.add_argument(
37
+ "target",
38
+ nargs="?",
39
+ default=".",
40
+ help="Target directory to analyze (default: current directory)",
41
+ )
42
+ parser.add_argument(
43
+ "--rebuild",
44
+ action="store_true",
45
+ help="Force regeneration of KNOWLEDGE_BASE.md",
46
+ )
47
+ parser.add_argument(
48
+ "--test",
49
+ action="store_true",
50
+ help="Run the built-in test suite",
51
+ )
52
+ return parser
53
+
54
+
55
+ def _run_tests() -> None:
56
+ """Discover and run the full test suite from the tests/ directory."""
57
+ package_dir = Path(__file__).resolve().parent.parent
58
+ tests_dir = package_dir / "tests"
59
+ if tests_dir.is_dir():
60
+ sys.path.insert(0, str(package_dir))
61
+ loader = unittest.TestLoader()
62
+ suite = loader.discover(str(tests_dir), pattern="test_*.py")
63
+ runner = unittest.TextTestRunner(verbosity=2)
64
+ result = runner.run(suite)
65
+ sys.exit(0 if result.wasSuccessful() else 1)
66
+ else:
67
+ print("No tests directory found.", file=sys.stderr)
68
+ sys.exit(1)
69
+
70
+
71
+ def main() -> None:
72
+ """Primary CLI entry point invoked by ``python -m readmenator``.
73
+
74
+ Supports direct subcommand dispatch (query, explain, path, summary,
75
+ --rebuild, update) or falls back to the argument parser for the
76
+ default workflow: generate or summarise KNOWLEDGE_BASE.md.
77
+ """
78
+ if "--test" in sys.argv:
79
+ _run_tests()
80
+ return
81
+
82
+ if len(sys.argv) > 2 and sys.argv[1] != "--rebuild" and not sys.argv[1].startswith("-"):
83
+ target = sys.argv[1]
84
+ command = sys.argv[2]
85
+ app = readmenatorApplication()
86
+
87
+ if command == "query" and len(sys.argv) >= 4:
88
+ result = app.query(target, sys.argv[3])
89
+ print(result)
90
+ elif command == "explain" and len(sys.argv) >= 4:
91
+ result = app.explain(target, sys.argv[3])
92
+ print(result)
93
+ elif command == "path" and len(sys.argv) >= 5:
94
+ result = app.find_path(target, sys.argv[3], sys.argv[4])
95
+ print(result)
96
+ elif command in ("summary", "sum", "info"):
97
+ result = app.summary(target)
98
+ print(result)
99
+ elif command == "--rebuild":
100
+ app.rebuild(target)
101
+ elif command == "update":
102
+ app.rebuild(target)
103
+ else:
104
+ print(f"Unknown command: {command}", file=sys.stderr)
105
+ sys.exit(1)
106
+ return
107
+
108
+ parser = build_parser()
109
+ args = parser.parse_args()
110
+ target = args.target
111
+
112
+ app = readmenatorApplication()
113
+ output_path = Path(target) / "KNOWLEDGE_BASE.md"
114
+
115
+ if args.rebuild or not output_path.exists():
116
+ app.run(target)
117
+ else:
118
+ result = app.summary(target)
119
+ print(result)
120
+
121
+ print("\nRun with --rebuild to regenerate or use query/explain/path subcommands.")
122
+
123
+
124
+ if __name__ == "__main__":
125
+ main()
readmenator/_app.py ADDED
@@ -0,0 +1,108 @@
1
+ """Application orchestrator for the readmenator pipeline.
2
+
3
+ Wires together scanner, documentation generator, and query engine
4
+ into a single facade consumed by the CLI entry point (__main__) and
5
+ the public API (__init__).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import List, Optional, Tuple
12
+
13
+ from readmenator._config import Config
14
+ from readmenator._documentation import DocumentationGenerator
15
+ from readmenator._models import Edge, Node
16
+ from readmenator._query import QueryEngine
17
+ from readmenator._scanner import PolyglotScanner
18
+
19
+
20
+ class readmenatorApplication:
21
+ """High-level facade for readmenator operations.
22
+
23
+ Provides convenience methods for the full pipeline:
24
+ - ``run`` / ``rebuild``: scan + generate KNOWLEDGE_BASE.md
25
+ - ``query``, ``explain``, ``find_path``, ``summary``:
26
+ scan + query in a single call.
27
+ """
28
+
29
+ def __init__(self, config: Optional[Config] = None) -> None:
30
+ """Initialise the application with an optional custom config.
31
+
32
+ Args:
33
+ config: Application settings; defaults to Config() if omitted.
34
+ """
35
+ self._config = config or Config()
36
+ self._scanner = PolyglotScanner(self._config)
37
+ self._generator = DocumentationGenerator(self._config)
38
+ self._last_nodes: List[Node] = []
39
+ self._last_edges: List[Edge] = []
40
+
41
+ def _scan(self, target_dir: str) -> Tuple[List[Node], List[Edge]]:
42
+ """Resolve *target_dir* and run the scanner, caching results."""
43
+ root = Path(target_dir).resolve()
44
+ nodes, edges = self._scanner.scan(root)
45
+ self._last_nodes = nodes
46
+ self._last_edges = edges
47
+ return nodes, edges
48
+
49
+ def run(self, target_dir: str) -> None:
50
+ """Scan *target_dir* and write KNOWLEDGE_BASE.md to disk.
51
+
52
+ Prints a summary of files, symbols, and imports on completion.
53
+ """
54
+ root = Path(target_dir).resolve()
55
+ nodes, edges = self._scanner.scan(root)
56
+ content = self._generator.generate(nodes, edges)
57
+ output_path = root / self._config.OUTPUT_FILENAME
58
+ output_path.write_text(content, encoding="utf-8")
59
+ total_symbols = sum(len(n.symbols) for n in nodes)
60
+ print(f"[+] Knowledge base generated: {output_path}")
61
+ print(
62
+ f"[+] Files: {len(nodes)} | "
63
+ f"Symbols: {total_symbols} | "
64
+ f"Imports: {len(edges)}"
65
+ )
66
+
67
+ def query(self, target_dir: str, question: str) -> str:
68
+ """Scan *target_dir* and answer *question* using the query engine."""
69
+ nodes, edges = self._scan(target_dir)
70
+ engine = QueryEngine(nodes, edges)
71
+ return engine.query(question)
72
+
73
+ def explain(self, target_dir: str, symbol_name: str) -> str:
74
+ """Scan *target_dir* and return a detailed explanation of *symbol_name*."""
75
+ nodes, edges = self._scan(target_dir)
76
+ engine = QueryEngine(nodes, edges)
77
+ result = engine.explain(symbol_name)
78
+ if result is None:
79
+ return (
80
+ f"Symbol '{symbol_name}' not found in the knowledge base. "
81
+ f"Scanned {len(nodes)} files with "
82
+ f"{sum(len(n.symbols) for n in nodes)} total symbols."
83
+ )
84
+ return result
85
+
86
+ def find_path(self, target_dir: str, symbol_a: str, symbol_b: str) -> str:
87
+ """Scan *target_dir* and find the shortest import path between two symbols."""
88
+ nodes, edges = self._scan(target_dir)
89
+ engine = QueryEngine(nodes, edges)
90
+ result = engine.find_path(symbol_a, symbol_b)
91
+ if result is None:
92
+ return (
93
+ f"Could not find a dependency path between '{symbol_a}' "
94
+ f"and '{symbol_b}'. They may be in disconnected components "
95
+ f"or one of the symbols does not exist."
96
+ )
97
+ path_str = " --imports--> ".join(result)
98
+ return f"Dependency path: {path_str}"
99
+
100
+ def summary(self, target_dir: str) -> str:
101
+ """Scan *target_dir* and return a concise knowledge base overview."""
102
+ nodes, edges = self._scan(target_dir)
103
+ engine = QueryEngine(nodes, edges)
104
+ return engine.summary()
105
+
106
+ def rebuild(self, target_dir: str) -> None:
107
+ """Alias for ``run`` -- forces regeneration of KNOWLEDGE_BASE.md."""
108
+ self.run(target_dir)
readmenator/_config.py ADDED
@@ -0,0 +1,78 @@
1
+ """Immutable configuration dataclass for readmenator.
2
+
3
+ All tuneable parameters live here as frozen dataclass fields. No magic
4
+ numbers or hardcoded paths exist elsewhere in the codebase. Derived
5
+ consumers import Config and read values from an instance.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Tuple
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Config:
16
+ """Single source of truth for all readmenator settings.
17
+
18
+ Every tuneable constant -- file-size limits, directory depth,
19
+ supported extensions, symbol pluralisation map, and Mermaid style
20
+ tokens -- is defined here and consumed by reference elsewhere.
21
+ """
22
+ IGNORE_DIRS: Tuple[str, ...] = (
23
+ ".git", "__pycache__", "venv", ".venv", "env", ".env", "node_modules",
24
+ ".tox", ".eggs", ".pytest_cache", "build", "dist", ".idea", ".vscode",
25
+ "target", "bin", "obj", "out", "vendor", "third_party", "deps",
26
+ "third-party", "thirdparty", ".m2", ".gradle", ".nuget", "packages",
27
+ "Pods", ".dart_tool", ".pub-cache", "bower_components", ".yarn",
28
+ "Carthage", "node_packages", ".meteor", ".gitlab", ".github",
29
+ "htmlcov", ".coverage", "__pycache__",
30
+ )
31
+
32
+ OUTPUT_FILENAME: str = "KNOWLEDGE_BASE.md"
33
+
34
+ MAX_FILE_SIZE_MB: float = 10.0
35
+
36
+ MAX_DIRECTORY_DEPTH: int = 20
37
+
38
+ DOCSTRING_LOOKBACK_LINES: int = 15
39
+
40
+ DOCSTRING_MAX_LENGTH: int = 150
41
+
42
+ MERMAID_MAX_NODES: int = 300
43
+
44
+ MERMAID_MODULE_STYLE: str = "fill:#1e1e1e,stroke:#ff6666,stroke-width:2px,color:#fff"
45
+
46
+ MERMAID_CLASS_STYLE: str = "fill:#2d2d2d,stroke:#4ec9b0,stroke-width:2px,color:#fff"
47
+
48
+ MERMAID_FUNCTION_STYLE: str = "fill:#333,stroke:#dcdcaa,stroke-width:1px,color:#dcdcaa"
49
+
50
+ MERMAID_EXTERNAL_STYLE: str = "fill:#111,stroke:#666,stroke-dasharray:5 5,color:#aaa"
51
+
52
+ SUPPORTED_EXTENSIONS: Tuple[str, ...] = (
53
+ ".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".hxx",
54
+ ".py", ".go", ".rs",
55
+ ".js", ".ts", ".jsx", ".tsx",
56
+ ".java", ".cs",
57
+ ".sh", ".bash", ".zsh",
58
+ ".php", ".dart", ".gd", ".nim",
59
+ ".asm", ".s", ".S",
60
+ )
61
+
62
+ SYMBOL_TYPE_PLURALS: Tuple[Tuple[str, str], ...] = (
63
+ ("class", "classes"),
64
+ ("struct", "structs"),
65
+ ("function", "functions"),
66
+ ("method", "methods"),
67
+ ("macro", "macros"),
68
+ ("trait", "traits"),
69
+ ("enum", "enums"),
70
+ ("interface", "interfaces"),
71
+ ("record", "records"),
72
+ ("variable", "variables"),
73
+ ("constant", "constants"),
74
+ ("type_alias", "type_aliases"),
75
+ ("module", "modules"),
76
+ ("protocol", "protocols"),
77
+ ("extension", "extensions"),
78
+ )
@@ -0,0 +1,124 @@
1
+ """KNOWLEDGE_BASE.md generator for the readmenator project.
2
+
3
+ Produces the human-readable Markdown artifact that contains the
4
+ structural knowledge map (Mermaid graph), architecture reference
5
+ grouped by language, and per-file symbol listings with docstrings
6
+ and signatures.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Dict, List
12
+
13
+ from readmenator._config import Config
14
+ from readmenator._mermaid import MermaidRenderer
15
+ from readmenator._models import Edge, Node, Symbol, pluralize_symbol_kind
16
+
17
+
18
+ class DocumentationGenerator:
19
+ """Builds the KNOWLEDGE_BASE.md document from scanned nodes and edges.
20
+
21
+ Delegates graph rendering to MermaidRenderer and handles the
22
+ Markdown layout: header metadata, Mermaid block, and per-language
23
+ architecture sections with pluralised symbol kind headings.
24
+ """
25
+
26
+ def __init__(self, config: Config) -> None:
27
+ """Initialise with config and pre-compute the plural map.
28
+
29
+ Args:
30
+ config: Application settings including SYMBOL_TYPE_PLURALS.
31
+ """
32
+ self._config = config
33
+ self._mermaid = MermaidRenderer(config)
34
+ self._plural_map: Dict[str, str] = dict(config.SYMBOL_TYPE_PLURALS)
35
+
36
+ def generate(self, nodes: List[Node], edges: List[Edge]) -> str:
37
+ """Assemble the full KNOWLEDGE_BASE.md Markdown document.
38
+
39
+ Groups files by language, lists symbols per file under
40
+ pluralised kind headings (e.g. "Classes", "Functions"),
41
+ and includes a note when the Mermaid graph was pruned.
42
+
43
+ Returns:
44
+ Complete Markdown string ready to write to disk.
45
+ """
46
+ total_symbols = sum(len(n.symbols) for n in nodes)
47
+ graph_output, is_truncated = self._mermaid.render(nodes, edges)
48
+
49
+ sections: List[str] = [
50
+ "# Polyglot Codebase Knowledge Graph",
51
+ "",
52
+ "> Generated offline by **readmenator**. "
53
+ "Supports C, C++, Python, Go, Rust, JS/TS, Java, C#, Shell, PHP, "
54
+ "Dart, GDScript, Nim, ASM.",
55
+ "> No LLMs. No tokens. Pure static analysis. "
56
+ "See more [here](https://github.com/grisuno/ReadMenator)",
57
+ "",
58
+ f"**Total Files Parsed:** {len(nodes)} | "
59
+ f"**Total Symbols Extracted:** {total_symbols} | "
60
+ f"**Total Imports:** {len(edges)}",
61
+ "",
62
+ "## Structural Knowledge Map",
63
+ ]
64
+
65
+ if is_truncated:
66
+ sections.append(
67
+ "> **Note:** The visual graph below has been intelligently pruned "
68
+ f"to the top {self._config.MERMAID_MAX_NODES} most relevant nodes "
69
+ "to prevent rendering crashes. Full details of all "
70
+ f"{len(nodes)} files are documented below."
71
+ )
72
+ sections.append("")
73
+
74
+ sections.extend(
75
+ [
76
+ "```mermaid",
77
+ graph_output,
78
+ "```",
79
+ "",
80
+ "---",
81
+ "",
82
+ "## Architecture Reference",
83
+ "",
84
+ ]
85
+ )
86
+
87
+ files_by_lang: Dict[str, List[Node]] = {}
88
+ for node in nodes:
89
+ lang = node.language.upper() if node.language else "UNKNOWN"
90
+ if lang not in files_by_lang:
91
+ files_by_lang[lang] = []
92
+ files_by_lang[lang].append(node)
93
+
94
+ for lang, lang_nodes in sorted(files_by_lang.items()):
95
+ sections.append(f"### {lang} ({len(lang_nodes)} files)")
96
+ sections.append("")
97
+
98
+ for node in lang_nodes:
99
+ sections.append(f"#### `{node.label}`")
100
+ sections.append(f"**Path:** `{node.node_id}`")
101
+ sections.append("")
102
+
103
+ if node.symbols:
104
+ symbols_by_type: Dict[str, List[Symbol]] = {}
105
+ for symbol in node.symbols:
106
+ if symbol.kind not in symbols_by_type:
107
+ symbols_by_type[symbol.kind] = []
108
+ symbols_by_type[symbol.kind].append(symbol)
109
+
110
+ for sym_kind, symbols in sorted(symbols_by_type.items()):
111
+ plural = pluralize_symbol_kind(sym_kind, self._plural_map)
112
+ sections.append(f"**{plural.title()}:**")
113
+ for symbol in symbols:
114
+ doc_str = f" - *{symbol.doc}*" if symbol.doc else ""
115
+ sig_str = f" `{symbol.signature}`" if symbol.signature else ""
116
+ sections.append(
117
+ f"- `{symbol.name}` (line {symbol.line}){sig_str}{doc_str}"
118
+ )
119
+ sections.append("")
120
+ else:
121
+ sections.append("*No symbols extracted*")
122
+ sections.append("")
123
+
124
+ return "\n".join(sections)
@@ -0,0 +1,124 @@
1
+ """Mermaid graph renderer with intelligent pruning.
2
+
3
+ Converts the internal Node/Edge graph into a Mermaid flowchart
4
+ (string) suitable for embedding in Markdown. Handles node limits,
5
+ deduplication, and CSS-like class styling.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Dict, List, Set, Tuple
12
+
13
+ from readmenator._config import Config
14
+ from readmenator._models import Edge, Node
15
+
16
+
17
+ class MermaidRenderer:
18
+ """Renders a knowledge graph to Mermaid JS flowchart syntax.
19
+
20
+ Nodes are ordered by import count and symbol richness; the top
21
+ ``MERMAID_MAX_NODES`` entries are included. External dependencies
22
+ (import targets not matching any scanned file) appear as dashed
23
+ boxes.
24
+ """
25
+
26
+ def __init__(self, config: Config) -> None:
27
+ """Initialise with configuration for style tokens and node limits.
28
+
29
+ Args:
30
+ config: Provides MERMAID_* style strings and MERMAID_MAX_NODES.
31
+ """
32
+ self._config = config
33
+
34
+ def _sanitize_id(self, node_id: str) -> str:
35
+ """Convert *node_id* to a Mermaid-safe identifier.
36
+
37
+ Replaces non-alphanumeric characters with underscores and
38
+ prepends ``n_`` if the result starts with a digit.
39
+ """
40
+ sanitized = re.sub(r"[^a-zA-Z0-9]", "_", node_id)
41
+ if sanitized and sanitized[0].isdigit():
42
+ sanitized = "n_" + sanitized
43
+ return sanitized
44
+
45
+ def render(self, nodes: List[Node], edges: List[Edge]) -> Tuple[str, bool]:
46
+ """Produce a Mermaid flowchart string and a truncation flag.
47
+
48
+ Nodes are sorted by import popularity, then by symbol count.
49
+ At most ``MERMAID_MAX_NODES`` items (files + child symbols +
50
+ external deps) are emitted. External imports use dashed edges.
51
+
52
+ Returns:
53
+ Tuple of (Mermaid source string, is_truncated bool).
54
+ """
55
+ lines: List[str] = ["graph TD"]
56
+ lines.append(f" classDef mod {self._config.MERMAID_MODULE_STYLE};")
57
+ lines.append(f" classDef cls {self._config.MERMAID_CLASS_STYLE};")
58
+ lines.append(f" classDef fn {self._config.MERMAID_FUNCTION_STYLE};")
59
+ lines.append(f" classDef ext {self._config.MERMAID_EXTERNAL_STYLE};")
60
+
61
+ seen_ids: Set[str] = set()
62
+ node_count = 0
63
+ is_truncated = False
64
+ max_nodes = self._config.MERMAID_MAX_NODES
65
+
66
+ import_counts: Dict[str, int] = {node.node_id: 0 for node in nodes}
67
+ for edge in edges:
68
+ if edge.source in import_counts:
69
+ import_counts[edge.source] += 1
70
+
71
+ sorted_nodes = sorted(
72
+ nodes,
73
+ key=lambda n: (import_counts.get(n.node_id, 0), len(n.symbols)),
74
+ reverse=True,
75
+ )
76
+
77
+ for node in sorted_nodes:
78
+ safe_id = self._sanitize_id(node.node_id)
79
+ if safe_id in seen_ids:
80
+ continue
81
+ label = node.label.replace('"', '\\"')
82
+ lines.append(f' {safe_id}["{label} ({node.language})"]')
83
+ lines.append(f" class {safe_id} mod;")
84
+ seen_ids.add(safe_id)
85
+ node_count += 1
86
+
87
+ for symbol in node.symbols[:5]:
88
+ if node_count >= max_nodes:
89
+ is_truncated = True
90
+ break
91
+ symbol_id = f"{safe_id}_{self._sanitize_id(symbol.name)}"
92
+ symbol_label = symbol.name.replace('"', '\\"')
93
+ lines.append(f' {symbol_id}["{symbol_label}"]')
94
+ cls_types = {"class", "struct", "interface", "trait", "enum", "record"}
95
+ if symbol.kind in cls_types:
96
+ lines.append(f" class {symbol_id} cls;")
97
+ else:
98
+ lines.append(f" class {symbol_id} fn;")
99
+ lines.append(f" {safe_id} --> {symbol_id}")
100
+ node_count += 1
101
+
102
+ if node_count >= max_nodes:
103
+ is_truncated = True
104
+ break
105
+
106
+ for edge in edges:
107
+ if is_truncated:
108
+ break
109
+ src = self._sanitize_id(edge.source)
110
+ if src not in seen_ids:
111
+ continue
112
+ target_id = self._sanitize_id(f"ext_{edge.target}")
113
+ target_label = edge.target.split("/")[-1].replace('"', '\\"')
114
+ if target_id not in seen_ids:
115
+ if node_count >= max_nodes:
116
+ is_truncated = True
117
+ break
118
+ lines.append(f' {target_id}["{target_label}"]')
119
+ lines.append(f" class {target_id} ext;")
120
+ seen_ids.add(target_id)
121
+ node_count += 1
122
+ lines.append(f" {src} -.->|imports| {target_id}")
123
+
124
+ return "\n".join(lines), is_truncated
readmenator/_models.py ADDED
@@ -0,0 +1,75 @@
1
+ """Data model types for the readmenator knowledge graph.
2
+
3
+ Defines the three core entity types -- Symbol, Node, Edge -- plus a
4
+ utility function for pluralising symbol kind labels. Every parser,
5
+ scanner, renderer, and query engine depends on these definitions.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Dict, List
12
+
13
+
14
+ @dataclass
15
+ class Symbol:
16
+ """A single code symbol extracted from a source file.
17
+
18
+ Attributes:
19
+ name: Identifier of the symbol (class name, function name, etc.).
20
+ kind: Semantic type (class, function, struct, enum, ...).
21
+ line: One-based line number where the symbol is defined.
22
+ doc: Optional docstring or comment extracted from the source.
23
+ signature: Optional method or function signature snippet.
24
+ """
25
+
26
+ name: str
27
+ kind: str
28
+ line: int
29
+ doc: str = ""
30
+ signature: str = ""
31
+
32
+
33
+ @dataclass
34
+ class Node:
35
+ """A file node in the knowledge graph, containing its symbols.
36
+
37
+ Attributes:
38
+ node_id: Relative path of the file used as a unique identifier.
39
+ label: Base file name for display purposes.
40
+ kind: Type of node (typically "module").
41
+ language: Programming language derived from the file extension.
42
+ doc: Optional file-level documentation string.
43
+ symbols: List of Symbol instances defined in this file.
44
+ """
45
+
46
+ node_id: str
47
+ label: str
48
+ kind: str
49
+ language: str
50
+ doc: str = ""
51
+ symbols: List[Symbol] = field(default_factory=list)
52
+
53
+
54
+ @dataclass
55
+ class Edge:
56
+ """A directed relationship between two nodes in the knowledge graph.
57
+
58
+ Attributes:
59
+ source: Node ID of the source (dependent) file.
60
+ target: Node ID of the target (dependency) file or module.
61
+ relation: Semantic relation label (e.g. "imports").
62
+ """
63
+
64
+ source: str
65
+ target: str
66
+ relation: str
67
+
68
+
69
+ def pluralize_symbol_kind(kind: str, plural_map: Dict[str, str]) -> str:
70
+ """Return the plural form of *kind* according to *plural_map*.
71
+
72
+ Falls back to appending ``"s"`` when the kind is not found.
73
+ This prevents obvious misspellings like ``"Classs"``.
74
+ """
75
+ return plural_map.get(kind, kind + "s")