reponexia 0.1.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.
reponexia/pipeline.py ADDED
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import replace
4
+ from pathlib import Path
5
+
6
+ from .cache import cache_key
7
+ from .redact import redact
8
+ from .summarize.base import ModuleContext, RepoContext, Summarizer
9
+ from .summarize.context import build_module_contexts
10
+ from .summarize.skeleton import MAX_LINES_FOR_LLM, skeletonize
11
+
12
+ # Teto de linhas de um snippet enviado ao LLM (mesmo abaixo do limite de esqueleto).
13
+ MAX_SNIPPET_LINES = 200
14
+
15
+
16
+ def _symbols_by_file(graph: dict) -> dict[str, list[dict]]:
17
+ result: dict[str, list[dict]] = {}
18
+ for symbol in graph.get("symbols", []):
19
+ result.setdefault(symbol["file"], []).append(symbol)
20
+ return result
21
+
22
+
23
+ # Lê o arquivo do disco, esqueletiza se for grande, fatia e redige segredos.
24
+ # O LLM nunca vê o repositório inteiro.
25
+ def _snippet(root: Path, path: str, symbols: list[dict]) -> str | None:
26
+ try:
27
+ text = (root / path).read_text(encoding="utf-8", errors="replace")
28
+ except OSError:
29
+ return None
30
+
31
+ if text.count("\n") + 1 > MAX_LINES_FOR_LLM:
32
+ text = skeletonize(text, symbols)
33
+
34
+ lines = text.splitlines()[:MAX_SNIPPET_LINES]
35
+ return redact("\n".join(lines))
36
+
37
+
38
+ def attach_snippets(contexts: list[ModuleContext], graph: dict) -> list[ModuleContext]:
39
+ root = Path(graph["root"])
40
+ by_file = _symbols_by_file(graph)
41
+
42
+ attached = []
43
+ for ctx in contexts:
44
+ snippets = []
45
+ for path in ctx.files:
46
+ snippet = _snippet(root, path, by_file.get(path, []))
47
+ if snippet:
48
+ snippets.append(f"# {path}\n{snippet}")
49
+ attached.append(replace(ctx, snippets=tuple(snippets)))
50
+ return attached
51
+
52
+
53
+ def _module_hash(graph: dict, files: tuple[str, ...]) -> str:
54
+ hashes = {file["path"]: file.get("hash", "") for file in graph.get("files", [])}
55
+ return cache_key(*[hashes.get(path, "") for path in files])
56
+
57
+
58
+ # Pipeline hierárquico: sumariza módulo a módulo e depois o repo a partir desses
59
+ # resumos. Cache por hash de módulo evita re-chamar o LLM sem mudança.
60
+ def build_narrative(
61
+ graph: dict,
62
+ summarizer: Summarizer,
63
+ *,
64
+ cache=None,
65
+ verbose: bool = False,
66
+ ) -> dict:
67
+ contexts = attach_snippets(build_module_contexts(graph), graph)
68
+ model = getattr(summarizer, "model", "")
69
+
70
+ modules: dict[str, str] = {}
71
+ for ctx in contexts:
72
+ key = cache_key("module", model, _module_hash(graph, ctx.files))
73
+ cached = cache.get(key) if cache else None
74
+ if cached is not None:
75
+ if verbose:
76
+ print(f"[cache] modulo {ctx.name}")
77
+ modules[ctx.name] = cached
78
+ continue
79
+
80
+ text = summarizer.summarize_module(ctx)
81
+ modules[ctx.name] = text
82
+ if cache is not None:
83
+ cache.set(key, text)
84
+
85
+ repo_context = RepoContext(
86
+ name=Path(graph["root"]).name or graph["root"],
87
+ languages=tuple(graph.get("languages", [])),
88
+ file_count=len(graph.get("files", [])),
89
+ module_summaries=tuple(
90
+ f"## {name}\n{text}" for name, text in modules.items()
91
+ ),
92
+ )
93
+
94
+ repo_key = cache_key("repo", model, graph.get("repo_hash", ""))
95
+ cached_repo = cache.get(repo_key) if cache else None
96
+ if cached_repo is None:
97
+ cached_repo = summarizer.summarize_repo(repo_context)
98
+ if cache is not None:
99
+ cache.set(repo_key, cached_repo)
100
+
101
+ return {"repo": cached_repo, "modules": modules}
reponexia/redact.py ADDED
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ PLACEHOLDER = "[REDACTED]"
6
+
7
+ # Regex de padrões comuns de segredo. Configuráveis via o parâmetro `patterns`,
8
+ # para quando existir um arquivo de config (por ora o default cobre os casos do DESIGN).
9
+ PATTERNS: tuple[re.Pattern[str], ...] = (
10
+ re.compile(r"AKIA[0-9A-Z]{16}"), # AWS access key
11
+ re.compile(r"sk-[A-Za-z0-9]{20,}"), # chave estilo OpenAI
12
+ re.compile(r"ghp_[A-Za-z0-9]{36}"), # GitHub token
13
+ re.compile(r"xox[baprs]-[A-Za-z0-9-]+"), # Slack token
14
+ re.compile( # bloco de chave privada
15
+ r"-----BEGIN[^-]*PRIVATE KEY-----.*?-----END[^-]*PRIVATE KEY-----",
16
+ re.DOTALL, # o corpo tem quebras de linha
17
+ ),
18
+ re.compile( # connection string com senha
19
+ r"\b(?:postgres(?:ql)?|mysql|mongodb|redis)://[^:\s]+:[^@\s]+@[^\s]+"
20
+ ),
21
+ )
22
+
23
+
24
+ # Substitui todo segredo detectado por um marcador fixo.
25
+ def redact(text: str, patterns: tuple[re.Pattern[str], ...] = PATTERNS) -> str:
26
+ for pattern in patterns:
27
+ text = pattern.sub(PLACEHOLDER, text)
28
+ return text
@@ -0,0 +1,4 @@
1
+ from .markdown import render_markdown
2
+ from .mermaid import render_mermaid
3
+
4
+ __all__ = ["render_markdown", "render_mermaid"]
@@ -0,0 +1,144 @@
1
+ from __future__ import annotations
2
+ from collections import defaultdict
3
+ from pathlib import PurePosixPath
4
+
5
+ TOP_FILES=10
6
+
7
+ def render_markdown(graph:dict, narrative: dict | None = None) -> str:
8
+ sections=[
9
+ _header(graph),
10
+ _entry_points(graph),
11
+ _top_files(graph),
12
+ _modules(graph),
13
+ _dependencies(graph),
14
+ _warnings(graph),
15
+ ]
16
+ # O bloco do LLM é opcional: no --dry-run não existe.
17
+ if narrative:
18
+ sections.append(_narrative(narrative))
19
+ return "\n\n".join(section for section in sections if section) + "\n"
20
+
21
+ def _header(graph: dict) -> str:
22
+ name = PurePosixPath(graph["root"]).name or graph["root"]
23
+ languages = ", ".join(graph.get("languages", [])) or "nenhuma"
24
+
25
+ return "\n".join([
26
+ f"# Onboarding: {name}",
27
+ "",
28
+ f"- schema: {graph['schema_version']}",
29
+ f"- repo_hash: {graph['repo_hash'][:12]}" if graph.get("repo_hash") else "",
30
+ f"- linguagens: {languages}",
31
+ f"- arquivos: {len(graph.get('files', []))}",
32
+ f"- símbolos: {len(graph.get('symbols', []))}",
33
+ f"- dependências internas: {len(graph.get('edges', []))}",
34
+ f"- entry points: {len(graph.get('entry_points', []))}",
35
+ ])
36
+
37
+ def _entry_points(graph: dict) -> str:
38
+ entries = graph.get("entry_points", [])
39
+ if not entries:
40
+ return ""
41
+
42
+ lines = ["## Entry points", "", "| path | kind | evidência |", "|---|---|---|"]
43
+ for entry in sorted(entries, key=lambda e: (e["path"], e["kind"])):
44
+ lines.append(f"| `{entry['path']}` | {entry['kind']} | `{entry['evidence']}` |")
45
+ return "\n".join(lines)
46
+
47
+
48
+ # Mais centrais primeiro, desempate por path.
49
+ def _top_files(graph: dict) -> str:
50
+ metrics = graph.get("metrics", [])
51
+ if not metrics:
52
+ return ""
53
+
54
+ top = sorted(metrics, key=lambda m: (-m["page_rank"], m["path"]))[:TOP_FILES]
55
+
56
+ lines = [
57
+ "## Arquivos centrais",
58
+ "",
59
+ "| path | fan_in | fan_out | page_rank |",
60
+ "|---|---|---|---|",
61
+ ]
62
+ for metric in top:
63
+ lines.append(
64
+ f"| `{metric['path']}` | {metric['fan_in']} | {metric['fan_out']} | "
65
+ f"{metric['page_rank']:.6f} |"
66
+ )
67
+ return "\n".join(lines)
68
+
69
+
70
+ def _modules(graph: dict) -> str:
71
+ files = graph.get("files", [])
72
+ if not files:
73
+ return ""
74
+
75
+ symbols_by_file: dict[str, int] = defaultdict(int)
76
+ for symbol in graph.get("symbols", []):
77
+ symbols_by_file[symbol["file"]] += 1
78
+
79
+ groups: dict[str, list[dict]] = defaultdict(list)
80
+ for file in files:
81
+ parent = str(PurePosixPath(file["path"]).parent)
82
+ groups["(raiz)" if parent == "." else parent].append(file)
83
+
84
+ lines = ["## Módulos"]
85
+ for module in sorted(groups):
86
+ members = sorted(groups[module], key=lambda f: f["path"])
87
+ total_symbols = sum(symbols_by_file[file["path"]] for file in members)
88
+ lines.append("")
89
+ lines.append(
90
+ f"### `{module}` ({_count(len(members), 'arquivo', 'arquivos')}, "
91
+ f"{_count(total_symbols, 'símbolo', 'símbolos')})"
92
+ )
93
+ for file in members:
94
+ lines.append(f"- `{file['path']}` — {file['loc']} linhas")
95
+ return "\n".join(lines)
96
+
97
+
98
+ # Concordância simples para os contadores do cabeçalho de módulo.
99
+ def _count(n: int, singular: str, plural: str) -> str:
100
+ return f"{n} {singular if n == 1 else plural}"
101
+
102
+
103
+ # Arestas agrupadas por origem.
104
+ def _dependencies(graph: dict) -> str:
105
+ edges = graph.get("edges", [])
106
+ if not edges:
107
+ return ""
108
+
109
+ by_source: dict[str, list[dict]] = defaultdict(list)
110
+ for edge in edges:
111
+ by_source[edge["from"]].append(edge)
112
+
113
+ lines = ["## Dependências internas"]
114
+ for source in sorted(by_source):
115
+ lines.append("")
116
+ lines.append(f"### `{source}`")
117
+ for edge in sorted(by_source[source], key=lambda e: (e["to"], e["line"])):
118
+ lines.append(f"- linha {edge['line']} → `{edge['to']}`")
119
+ return "\n".join(lines)
120
+
121
+ def _warnings(graph: dict) -> str:
122
+ warnings = graph.get("warnings", [])
123
+ if not warnings:
124
+ return ""
125
+
126
+ lines = ["## Avisos"]
127
+ for warning in warnings:
128
+ path = warning.get("path")
129
+ where = f"`{path}`: " if path else ""
130
+ lines.append(f"- {where}{warning['message']}")
131
+ return "\n".join(lines)
132
+
133
+
134
+ # Seções narrativas produzidas pelo LLM (repo + módulos).
135
+ def _narrative(narrative: dict) -> str:
136
+ lines = ["## Visão geral (LLM)", "", (narrative.get("repo") or "").strip()]
137
+
138
+ modules = narrative.get("modules") or {}
139
+ if modules:
140
+ lines += ["", "## Módulos (resumo LLM)"]
141
+ for name in sorted(modules):
142
+ lines += ["", f"### `{name}`", "", (modules[name] or "").strip()]
143
+
144
+ return "\n".join(lines)
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ # Evita espaguete: só os N arquivos mais centrais (PageRank) + entry points.
4
+ DEFAULT_TOP_N = 50
5
+
6
+
7
+ # Renderiza as dependências internas como um grafo Mermaid.
8
+ def render_mermaid(graph: dict, top_n: int = DEFAULT_TOP_N) -> str:
9
+ edges = graph.get("edges", [])
10
+ entry_points = {entry["path"] for entry in graph.get("entry_points", [])}
11
+
12
+ # Nós = arquivos ligados por arestas + entry points.
13
+ paths = {edge["from"] for edge in edges} | {edge["to"] for edge in edges}
14
+ paths |= entry_points
15
+ if not paths:
16
+ return "graph LR\n %% nenhuma dependencia interna detectada\n"
17
+
18
+ # Filtro top-N por PageRank; entry points sempre ficam.
19
+ metrics = graph.get("metrics", [])
20
+ if metrics and top_n > 0:
21
+ ranked = sorted(metrics, key=lambda m: (-m["page_rank"], m["path"]))
22
+ keep = {m["path"] for m in ranked[:top_n]} | entry_points
23
+ paths &= keep
24
+ edges = [e for e in edges if e["from"] in paths and e["to"] in paths]
25
+ if not paths:
26
+ return "graph LR\n %% nenhuma dependencia interna detectada\n"
27
+
28
+ # IDs determinísticos: ordem alfabética, sem depender do path no nó.
29
+ node_id = {path: f"n{index}" for index, path in enumerate(sorted(paths))}
30
+
31
+ lines = ["graph LR"]
32
+ for path in sorted(paths):
33
+ # Entry points ganham formato de estádio para se destacarem.
34
+ if path in entry_points:
35
+ lines.append(f' {node_id[path]}(["{path}"])')
36
+ else:
37
+ lines.append(f' {node_id[path]}["{path}"]')
38
+
39
+ # Uma seta por par origem/destino, em ordem estável.
40
+ pairs = {(edge["from"], edge["to"]) for edge in edges}
41
+ for source, target in sorted(pairs):
42
+ lines.append(f" {node_id[source]} --> {node_id[target]}")
43
+
44
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,3 @@
1
+ from .base import ModuleContext, RepoContext, Summarizer, SymbolRef
2
+
3
+ __all__ = ["ModuleContext", "RepoContext", "Summarizer", "SymbolRef"]
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Protocol
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class SymbolRef:
9
+ name: str
10
+ kind: str
11
+ line: int
12
+ file: str
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ModuleContext:
17
+ name: str
18
+ files: tuple[str, ...]
19
+ symbols: tuple[SymbolRef, ...]
20
+ imports_out: tuple[str, ...]
21
+ imports_in: tuple[str, ...]
22
+ # Snippets já redigidos/esqueletizados; vazio no modo estrutural.
23
+ snippets: tuple[str, ...] = ()
24
+
25
+
26
+ # Resumo do repositório é montado a partir dos resumos de módulo (hierárquico),
27
+ # por isso carrega os textos e não o código bruto.
28
+ @dataclass(frozen=True)
29
+ class RepoContext:
30
+ name: str
31
+ languages: tuple[str, ...]
32
+ file_count: int
33
+ module_summaries: tuple[str, ...]
34
+
35
+
36
+ class Summarizer(Protocol):
37
+ def summarize_module(self, module: ModuleContext) -> str: ...
38
+
39
+ def summarize_repo(self, ctx: RepoContext) -> str: ...
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+
5
+ from ..modules import group_files_by_module
6
+ from .base import ModuleContext, SymbolRef
7
+
8
+
9
+ # Monta o contexto resumido por módulo a partir do grafo.
10
+ # Por quê: o LLM recebe resumo estrutural (símbolos/imports), nunca o código inteiro.
11
+ def build_module_contexts(graph: dict) -> list[ModuleContext]:
12
+ files_by_module = group_files_by_module(graph.get("files", []))
13
+
14
+ # Mapa arquivo -> módulo, para atribuir símbolos e arestas ao módulo certo.
15
+ module_of_file = {
16
+ file["path"]: name
17
+ for name, members in files_by_module.items()
18
+ for file in members
19
+ }
20
+
21
+ # Um balde por módulo. Sets evitam import duplicado (A importa B 2x).
22
+ symbols_by_module: dict[str, list[SymbolRef]] = defaultdict(list)
23
+ out_by_module: dict[str, set[str]] = defaultdict(set)
24
+ in_by_module: dict[str, set[str]] = defaultdict(set)
25
+
26
+ for symbol in graph.get("symbols", []):
27
+ module = module_of_file.get(symbol["file"])
28
+ if module is None:
29
+ continue
30
+ symbols_by_module[module].append(
31
+ SymbolRef(
32
+ name=symbol["name"],
33
+ kind=symbol["kind"],
34
+ line=symbol["line"],
35
+ file=symbol["file"],
36
+ )
37
+ )
38
+
39
+ for edge in graph.get("edges", []):
40
+ source = module_of_file.get(edge["from"])
41
+ target = module_of_file.get(edge["to"])
42
+ # Import dentro do mesmo módulo não é dependência entre módulos.
43
+ if source is None or target is None or source == target:
44
+ continue
45
+ out_by_module[source].add(target)
46
+ in_by_module[target].add(source)
47
+
48
+ return [
49
+ ModuleContext(
50
+ name=name,
51
+ files=tuple(file["path"] for file in members),
52
+ symbols=tuple(symbols_by_module[name]),
53
+ imports_out=tuple(sorted(out_by_module[name])),
54
+ imports_in=tuple(sorted(in_by_module[name])),
55
+ )
56
+ for name, members in sorted(files_by_module.items())
57
+ ]
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from collections.abc import Callable
5
+
6
+ from .base import ModuleContext, RepoContext
7
+
8
+
9
+ class SummarizerError(RuntimeError):
10
+ """Falha ao obter resumo do LLM (após os retries)."""
11
+
12
+
13
+ class BudgetExceeded(SummarizerError):
14
+ """O orçamento de tokens configurado seria estourado."""
15
+
16
+
17
+ def _estimate_tokens(text: str) -> int:
18
+ # ponytail: estimativa chars/4; trocar por tokenizer real se o custo precisar ser exato
19
+ return max(1, len(text) // 4)
20
+
21
+
22
+ # Implementação única sobre o litellm, que já abstrai OpenAI/Anthropic/Ollama.
23
+ # O Protocol Summarizer existe só para permitir mock/offline nos testes.
24
+ class LiteLLMSummarizer:
25
+ def __init__(
26
+ self,
27
+ model: str,
28
+ *,
29
+ max_tokens: int = 1_000_000,
30
+ retries: int = 3,
31
+ call: Callable[[str], str] | None = None,
32
+ sleep: Callable[[float], None] = time.sleep,
33
+ ) -> None:
34
+ self.model = model
35
+ self.max_tokens = max_tokens
36
+ self.retries = retries
37
+ self.spent = 0
38
+ self._call = call or self._litellm_call
39
+ self._sleep = sleep
40
+
41
+ # Chamada real; o import fica lazy para testar sem o litellm instalado.
42
+ def _litellm_call(self, prompt: str) -> str:
43
+ import litellm
44
+
45
+ response = litellm.completion(
46
+ model=self.model,
47
+ messages=[{"role": "user", "content": prompt}],
48
+ max_tokens=1024,
49
+ )
50
+ return response.choices[0].message.content or ""
51
+
52
+ # Executa com retry/backoff e contabiliza o orçamento antes de devolver.
53
+ def _complete(self, prompt: str) -> str:
54
+ for attempt in range(self.retries + 1):
55
+ try:
56
+ text = self._call(prompt)
57
+ except Exception as err: # noqa: BLE001 - qualquer falha de provider
58
+ if attempt == self.retries:
59
+ raise SummarizerError(
60
+ f"LLM falhou apos {self.retries + 1} tentativas: {err}"
61
+ ) from err
62
+ self._sleep(2**attempt)
63
+ continue
64
+
65
+ self.spent += _estimate_tokens(prompt) + _estimate_tokens(text)
66
+ if self.spent > self.max_tokens:
67
+ raise BudgetExceeded(
68
+ f"orcamento de {self.max_tokens} tokens estourado "
69
+ f"(~{self.spent} usados)"
70
+ )
71
+ return text
72
+
73
+ raise AssertionError("inalcancavel")
74
+
75
+ def _module_prompt(self, module: ModuleContext) -> str:
76
+ parts = [f"Modulo: {module.name}"]
77
+ parts.append("Arquivos:\n" + "\n".join(f"- {path}" for path in module.files))
78
+ if module.symbols:
79
+ parts.append(
80
+ "Simbolos:\n"
81
+ + "\n".join(
82
+ f"- {symbol.name} ({symbol.kind}) em {symbol.file}:{symbol.line}"
83
+ for symbol in module.symbols
84
+ )
85
+ )
86
+ if module.imports_out:
87
+ parts.append("Depende de: " + ", ".join(module.imports_out))
88
+ if module.imports_in:
89
+ parts.append("E usado por: " + ", ".join(module.imports_in))
90
+ if module.snippets:
91
+ parts.append("Snippets (redigidos):\n" + "\n\n".join(module.snippets))
92
+ parts.append(
93
+ "Explique o proposito deste modulo em 3 a 6 frases. "
94
+ "Cite arquivo:linha ao afirmar algo sobre o codigo."
95
+ )
96
+ return "\n\n".join(parts)
97
+
98
+ def summarize_module(self, module: ModuleContext) -> str:
99
+ return self._complete(self._module_prompt(module))
100
+
101
+ def _repo_prompt(self, ctx: RepoContext) -> str:
102
+ parts = [f"Repositorio: {ctx.name}"]
103
+ parts.append(f"Arquivos: {ctx.file_count}. Linguagens: {', '.join(ctx.languages)}.")
104
+ parts.append("Resumos por modulo:\n\n" + "\n\n".join(ctx.module_summaries))
105
+ parts.append(
106
+ "Com base apenas nos resumos acima, escreva uma visao geral da arquitetura "
107
+ "em 2 a 4 paragrafos. Nao invente fatos nem arquivos."
108
+ )
109
+ return "\n\n".join(parts)
110
+
111
+ def summarize_repo(self, ctx: RepoContext) -> str:
112
+ return self._complete(self._repo_prompt(ctx))
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ # Acima disso o arquivo não vai inteiro para o LLM: vira esqueleto.
4
+ MAX_LINES_FOR_LLM = 1500
5
+
6
+
7
+ # Reduz um arquivo grande às suas "costuras": imports + linhas de declaração de
8
+ # símbolos (que o grafo já conhece), omitindo corpos.
9
+ # Por quê: mandar 5k linhas estoura a context window e encarece a chamada.
10
+ def skeletonize(text: str, symbols: list[dict]) -> str:
11
+ lines = text.splitlines()
12
+ if len(lines) <= MAX_LINES_FOR_LLM:
13
+ return text
14
+
15
+ kept: dict[int, str] = {}
16
+ for number, line in enumerate(lines, start=1):
17
+ stripped = line.lstrip()
18
+ # Imports dão o "de onde vem" sem custo de corpo.
19
+ if stripped.startswith(("import ", "from ")):
20
+ kept[number] = line
21
+
22
+ # Assinaturas vêm do grafo (line já é 1-based).
23
+ for symbol in symbols:
24
+ number = symbol.get("line", 0)
25
+ if 1 <= number <= len(lines):
26
+ kept[number] = lines[number - 1]
27
+
28
+ return "\n".join(kept[number] for number in sorted(kept))
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: reponexia
3
+ Version: 0.1.0
4
+ Summary: CLI que explica um repositório desconhecido
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/rhyan05/reponexia
7
+ Project-URL: Repository, https://github.com/rhyan05/reponexia
8
+ Project-URL: Issues, https://github.com/rhyan05/reponexia/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Environment :: Console
11
+ Classifier: Topic :: Software Development :: Documentation
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: jsonschema>=4.0
17
+ Requires-Dist: reponexia-core==0.1.*
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest; extra == "dev"
20
+ Provides-Extra: llm
21
+ Requires-Dist: litellm; extra == "llm"
22
+ Dynamic: license-file
23
+
24
+ # RepoNexia
25
+
26
+ CLI que explica um repositório desconhecido: mapeia arquitetura, entry points e
27
+ dependências críticas, e gera um onboarding doc.
28
+
29
+ Motor em Rust (`reponexia-core`) faz o scan/parse e monta o grafo; o orquestrador
30
+ em Python chama o LLM e renderiza a documentação. A fronteira é um JSON
31
+ versionado (`src/reponexia/graph.schema.json`).
32
+
33
+ ## Build e instalação
34
+
35
+ **Via PyPI** (após o release):
36
+ ```bash
37
+ pip install reponexia
38
+ pip install "reponexia[llm]" # LLM real (litellm)
39
+ ```
40
+
41
+ **A partir do código-fonte** (clone):
42
+ ```bash
43
+ python -m venv .venv
44
+ # .venv\Scripts\Activate.ps1 (Windows) | source .venv/bin/activate (Unix)
45
+
46
+ pip install -e . # usa o wheel publicado do core (sem Rust)
47
+ # ou, para compilar/testar o core local:
48
+ pip install ./core
49
+ pip install -e ".[llm]"
50
+ ```
51
+
52
+ O Python descobre o binário nesta ordem: `REPONEXIA_CORE` → `Scripts/bin` do
53
+ ambiente → `PATH` → `core/target/{release,debug}`.
54
+
55
+ ## Uso
56
+
57
+ ```bash
58
+ reponexia guia # guia de uso no terminal
59
+ reponexia map <path> # só o core; imprime o graph.json
60
+ reponexia graph <path> --format mermaid # exporta o grafo (json|mermaid)
61
+ reponexia explain <path> --dry-run # esqueleto estrutural, sem LLM
62
+ reponexia explain <path> --model openai/gpt-4o-mini
63
+ reponexia ask <path> "onde fica a autenticação?"
64
+ ```
65
+
66
+ O mesmo guia está em [`docs/USAGE.md`](docs/USAGE.md) (gerado por
67
+ `reponexia guia --markdown`).
68
+
69
+ No modo real o modelo vem de `--model` ou `REPONEXIA_MODEL`. `--max-tokens`
70
+ limita o orçamento; sem mudança no repo, o cache evita novas chamadas LLM.
71
+
72
+ Saída padrão em `<path>/.reponexia/`: `graph.json`, `ONBOARDING.md` e
73
+ `architecture.mmd` (`--out` sobrescreve o diretório).
74
+
75
+ Exemplo de saída: [`examples/ONBOARDING.md`](examples/ONBOARDING.md).
76
+
77
+ ## Segurança
78
+
79
+ Antes de qualquer chamada LLM, snippets passam por `redact.py` (AWS, OpenAI,
80
+ GitHub, Slack, chave privada e connection strings). O scan ignora `.gitignore`,
81
+ dotfiles e `.reponexia`.
82
+
83
+ ## Testes
84
+
85
+ ```bash
86
+ cargo test --manifest-path core/Cargo.toml
87
+ pytest
88
+ ```
@@ -0,0 +1,26 @@
1
+ reponexia/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ reponexia/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
3
+ reponexia/ask.py,sha256=MmNwvOHu8IRHiCAl2pxWDFhvuUHhl-JQOVzk-E11i7M,1661
4
+ reponexia/cache.py,sha256=3aPatv0hTMQd-Gl7R8vYgDdjR2-72bXJ4eSdicnWpaQ,1200
5
+ reponexia/cli.py,sha256=NR277b0LW17L7N4H-YcdGEghrPgif81VDFYQAY-VpvA,6224
6
+ reponexia/contract.py,sha256=7uZNdHGJYmQA-Kz8fNtznbvO16HV0omdOAHoXZ9H4Ag,1224
7
+ reponexia/core_runner.py,sha256=UJ0QgrUoqQJtnM2_ZRjYSC3Ltmat8wqjHE8i23GuBJc,1788
8
+ reponexia/graph.schema.json,sha256=f0seWkYZaML5uBrw3KVsz5fzolDe586hJjWVAYsZ_U8,3686
9
+ reponexia/guide.py,sha256=UupeNAVSzUn7W1Rs1eSGrXSiqjDK1EMM8Tj0CsKIxOk,3545
10
+ reponexia/modules.py,sha256=2pBDeh4mZLk7AzVDJdMEx6YLl8t99pMq3-acwvCNujo,804
11
+ reponexia/pipeline.py,sha256=D_wCblplFWQCgwZMM-zQIPLlue3LuEJYpch1POBZo5g,3408
12
+ reponexia/redact.py,sha256=sPeN5INPgKjXSVcYrHcjXsfSmzLJERzkaTN6zpSmSVM,1235
13
+ reponexia/render/__init__.py,sha256=LYnbbSoB_oZMK53csmve1YuadzHLYg-vXe253T2VCTA,123
14
+ reponexia/render/markdown.py,sha256=hetup0KN8W2IG76jzr8OIb-R9UhLrHpczx6KIoclyVQ,4906
15
+ reponexia/render/mermaid.py,sha256=C54G1uNEMOF5U4S9iuUo0AJEcn_eeV00IUXDJ_tToy4,1818
16
+ reponexia/summarize/__init__.py,sha256=IDsQ85V8Z0VSAUy8YEfyEjmguExsyY29HxMc-FyTD5E,139
17
+ reponexia/summarize/base.py,sha256=eP4H2OjsVRBrG306vAJH3pnf5P4CKBY60pvXDJlYKtE,932
18
+ reponexia/summarize/context.py,sha256=MiUHqUqFB3cCSf-T7jBcDus5YXp9kQvcGlBShGfOOJU,2057
19
+ reponexia/summarize/litellm_impl.py,sha256=X8o3hZI1QTKaqw49Uyy7AcCamfgHewWzecPo5BLdT_8,4153
20
+ reponexia/summarize/skeleton.py,sha256=nynCSQah8M3xcqzeidWGzpM1VHuLOX5uWikOKfMU9_o,1021
21
+ reponexia-0.1.0.dist-info/licenses/LICENSE,sha256=FZArt7fexl-UElv3m5k5vXGZU4AdMbvLJNL82DLXTIQ,1100
22
+ reponexia-0.1.0.dist-info/METADATA,sha256=44pHszslHckiwWCWhQu7KBFtrSAvgiWPjXTsMhNqulE,2971
23
+ reponexia-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
24
+ reponexia-0.1.0.dist-info/entry_points.txt,sha256=gYlw4Dspp5OaIuFWnrQc9gxclHE3Dl0A9_ea-SLpJrI,49
25
+ reponexia-0.1.0.dist-info/top_level.txt,sha256=mMigYfHgWU2gwKnE1APLF3YVVy12gqXO4hoPL5LMAHw,10
26
+ reponexia-0.1.0.dist-info/RECORD,,