repokg 0.1.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.
- repokg-0.1.0/LICENSE +21 -0
- repokg-0.1.0/PKG-INFO +160 -0
- repokg-0.1.0/README.md +141 -0
- repokg-0.1.0/pyproject.toml +30 -0
- repokg-0.1.0/setup.cfg +4 -0
- repokg-0.1.0/src/repokg/__init__.py +3 -0
- repokg-0.1.0/src/repokg/__main__.py +6 -0
- repokg-0.1.0/src/repokg/cli.py +144 -0
- repokg-0.1.0/src/repokg/code.py +104 -0
- repokg-0.1.0/src/repokg/deps.py +166 -0
- repokg-0.1.0/src/repokg/github.py +66 -0
- repokg-0.1.0/src/repokg/gitinfo.py +124 -0
- repokg-0.1.0/src/repokg/inject.py +84 -0
- repokg-0.1.0/src/repokg/markdown.py +319 -0
- repokg-0.1.0/src/repokg/ops.py +80 -0
- repokg-0.1.0/src/repokg/prompts.py +61 -0
- repokg-0.1.0/src/repokg.egg-info/PKG-INFO +160 -0
- repokg-0.1.0/src/repokg.egg-info/SOURCES.txt +22 -0
- repokg-0.1.0/src/repokg.egg-info/dependency_links.txt +1 -0
- repokg-0.1.0/src/repokg.egg-info/entry_points.txt +2 -0
- repokg-0.1.0/src/repokg.egg-info/top_level.txt +1 -0
- repokg-0.1.0/tests/test_classify.py +52 -0
- repokg-0.1.0/tests/test_inject.py +73 -0
- repokg-0.1.0/tests/test_render.py +80 -0
repokg-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nihar Shah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
repokg-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: repokg
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate an AI-ready knowledge graph (KNOWLEDGE_GRAPH.md + kg.json) of any codebase: modules, dependency edges, branches, PRs, ops surface.
|
|
5
|
+
Author: Nihar Shah
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/NehharShah/repokg
|
|
8
|
+
Keywords: knowledge-graph,codebase,ai,agents,documentation,repo-map
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# repokg
|
|
21
|
+
|
|
22
|
+
Generate an **AI-ready knowledge graph** of any codebase — so an AI agent (or a new
|
|
23
|
+
developer) can read one file and start building immediately.
|
|
24
|
+
|
|
25
|
+
`repokg` extracts everything that can be known *deterministically* about a repo —
|
|
26
|
+
module inventory, internal import graph, every branch classified against every PR
|
|
27
|
+
(merged / squash-merged / abandoned / stale), contributor stats, CI/Docker/Helm/Make
|
|
28
|
+
surface — and renders it as:
|
|
29
|
+
|
|
30
|
+
- **`KNOWLEDGE_GRAPH.md`** — a single human/AI-readable document with a mermaid architecture
|
|
31
|
+
graph, module tables, branch & PR catalog, timeline, and ops inventory.
|
|
32
|
+
- **`.repokg/kg.json`** — the same graph, machine-readable.
|
|
33
|
+
|
|
34
|
+
The semantic layer (module purposes, data-flow narratives, project eras, gotchas)
|
|
35
|
+
can't be produced by static analysis without guessing — so repokg is
|
|
36
|
+
**agent-first**: it emits `.repokg/prompts/enrich.md`, a rigorous prompt any AI coding
|
|
37
|
+
agent (Claude Code, Cursor, Copilot Workspace…) executes to verify-and-fill the
|
|
38
|
+
narrative sections, writing `.repokg/narratives.json`. Re-render and the knowledge graph is
|
|
39
|
+
complete. No API keys, no LLM dependency in the tool itself.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pipx install repokg # or: pip install repokg
|
|
45
|
+
# from source:
|
|
46
|
+
pipx install git+https://github.com/NehharShah/repokg
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Requirements: Python ≥ 3.9, `git`. Optional: [`gh`](https://cli.github.com) (logged
|
|
50
|
+
in) for the PR/branch cross-reference — without it the knowledge graph still builds, minus PR data.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
cd your-repo
|
|
56
|
+
repokg # = generate: scan + prompts + render
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Output:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
.repokg/kg.json # machine-readable knowledge graph
|
|
63
|
+
.repokg/prompts/enrich.md # hand this to your AI agent
|
|
64
|
+
KNOWLEDGE_GRAPH.md # the knowledge graph document
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then, in your AI agent of choice:
|
|
68
|
+
|
|
69
|
+
> Follow the instructions in .repokg/prompts/enrich.md
|
|
70
|
+
|
|
71
|
+
The agent explores the code, writes `.repokg/narratives.json`, and runs
|
|
72
|
+
`repokg render` — KNOWLEDGE_GRAPH.md now carries verified purposes, data flows,
|
|
73
|
+
timeline eras, and gotchas alongside the deterministic structure.
|
|
74
|
+
|
|
75
|
+
### Commands
|
|
76
|
+
|
|
77
|
+
| Command | Effect |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `repokg scan [path]` | Extract structure → `.repokg/kg.json` |
|
|
80
|
+
| `repokg prompts [path]` | Write the enrichment prompt |
|
|
81
|
+
| `repokg render [path]` | `kg.json` (+ `narratives.json`) → `KNOWLEDGE_GRAPH.md` |
|
|
82
|
+
| `repokg generate [path]` | All three (default) |
|
|
83
|
+
| `repokg inject [path]` | Wire the knowledge graph into `CLAUDE.md` / `AGENTS.md` / Cursor rules |
|
|
84
|
+
| `repokg check [path]` | Exit 1 if the knowledge graph is stale vs `HEAD` (CI-friendly) |
|
|
85
|
+
|
|
86
|
+
Flags: `--out DIR` (default `<repo>/.repokg`), `--md FILE` (default `<repo>/KNOWLEDGE_GRAPH.md`),
|
|
87
|
+
`--no-github`, `--pr-limit N`.
|
|
88
|
+
|
|
89
|
+
## Agent integration
|
|
90
|
+
|
|
91
|
+
`repokg inject` adds a **managed block** (delimited by
|
|
92
|
+
`<!-- repokg:begin/end -->`, idempotent, never touches your hand-written
|
|
93
|
+
content) pointing agents at KNOWLEDGE_GRAPH.md:
|
|
94
|
+
|
|
95
|
+
- **`CLAUDE.md`** (Claude Code) — updated if present
|
|
96
|
+
- **`AGENTS.md`** (the cross-tool agent standard) — updated if present, created if
|
|
97
|
+
no agent file exists at all
|
|
98
|
+
- **`.github/copilot-instructions.md`** (Copilot) — updated if present
|
|
99
|
+
- **`.cursor/rules/repokg.mdc`** (Cursor, with `alwaysApply: true`) — created
|
|
100
|
+
if `.cursor/rules/` exists; falls back to legacy `.cursorrules`
|
|
101
|
+
|
|
102
|
+
Keep it fresh in CI:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
- run: pipx run repokg check . || echo "::warning::KNOWLEDGE_GRAPH.md is stale"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
KNOWLEDGE_GRAPH.md itself also lists any agent-context files it found, so an agent landing
|
|
109
|
+
on the knowledge graph discovers your rules — and vice versa.
|
|
110
|
+
|
|
111
|
+
## What gets extracted (all verified, never guessed)
|
|
112
|
+
|
|
113
|
+
| Area | How |
|
|
114
|
+
|---|---|
|
|
115
|
+
| Branch classification | `git for-each-ref` + `--merged` ancestry vs the integration branch (auto-detects `staging`/`develop`), cross-referenced with every PR's head ref via `gh` — distinguishes true merges from squash-merges from abandoned work |
|
|
116
|
+
| PR catalog | `gh pr list --state all` — open / merged / closed-unmerged, full appendix table |
|
|
117
|
+
| Module inventory | Filesystem walk with LOC per directory, language detection, generated-code flagging |
|
|
118
|
+
| Import graph | Go: `import` blocks resolved against `go.mod` module paths · Python: stdlib `ast` incl. relative imports · JS/TS: relative `import`/`require` resolution. Directory→directory edges with counts |
|
|
119
|
+
| Ops surface | CI workflow names, Dockerfiles, compose files, Helm charts, Makefile targets, config/docs/test/migration dirs |
|
|
120
|
+
| Timeline | Merged PRs grouped by month with conventional-commit scope frequencies (replaced by agent-written eras after enrichment) |
|
|
121
|
+
|
|
122
|
+
## Why agent-first instead of calling an LLM API?
|
|
123
|
+
|
|
124
|
+
Because the enrichment quality depends on *reading the code*, and your coding agent
|
|
125
|
+
already has the repo open, tools to search it, and your permission model. A prompt it
|
|
126
|
+
can execute beats a second LLM integration with its own keys, costs, and context limits.
|
|
127
|
+
The contract between tool and agent is one JSON file (`narratives.json`) with a fixed
|
|
128
|
+
schema — everything else stays deterministic and reproducible.
|
|
129
|
+
|
|
130
|
+
## Known limitations
|
|
131
|
+
|
|
132
|
+
- **JS/TS**: only relative imports are resolved; alias imports (`@/…`,
|
|
133
|
+
tsconfig `paths`) are ignored.
|
|
134
|
+
- **Fork PRs**: a fork PR whose head branch name matches a local branch will be
|
|
135
|
+
linked to it (GitHub's API reports bare head refs).
|
|
136
|
+
- **Python**: packages are discovered at the repo root and under `src/`;
|
|
137
|
+
deeper monorepo layouts (`packages/*/src/…`) get file-level edges only.
|
|
138
|
+
- Branch `ahead` counts use one batched git call on git ≥ 2.41, with a
|
|
139
|
+
per-branch fallback on older git.
|
|
140
|
+
|
|
141
|
+
## Roadmap
|
|
142
|
+
|
|
143
|
+
- [ ] Rust / Java / Kotlin import graphs
|
|
144
|
+
- [ ] `--exclude` glob patterns
|
|
145
|
+
- [ ] `llms.txt` emission alongside KNOWLEDGE_GRAPH.md
|
|
146
|
+
- [ ] tsconfig `paths` alias resolution
|
|
147
|
+
- [ ] PyPI release + prebuilt GitHub Action
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
pip install -e .
|
|
153
|
+
python -m unittest discover -s tests -v
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
No runtime dependencies — stdlib only.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
repokg-0.1.0/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# repokg
|
|
2
|
+
|
|
3
|
+
Generate an **AI-ready knowledge graph** of any codebase — so an AI agent (or a new
|
|
4
|
+
developer) can read one file and start building immediately.
|
|
5
|
+
|
|
6
|
+
`repokg` extracts everything that can be known *deterministically* about a repo —
|
|
7
|
+
module inventory, internal import graph, every branch classified against every PR
|
|
8
|
+
(merged / squash-merged / abandoned / stale), contributor stats, CI/Docker/Helm/Make
|
|
9
|
+
surface — and renders it as:
|
|
10
|
+
|
|
11
|
+
- **`KNOWLEDGE_GRAPH.md`** — a single human/AI-readable document with a mermaid architecture
|
|
12
|
+
graph, module tables, branch & PR catalog, timeline, and ops inventory.
|
|
13
|
+
- **`.repokg/kg.json`** — the same graph, machine-readable.
|
|
14
|
+
|
|
15
|
+
The semantic layer (module purposes, data-flow narratives, project eras, gotchas)
|
|
16
|
+
can't be produced by static analysis without guessing — so repokg is
|
|
17
|
+
**agent-first**: it emits `.repokg/prompts/enrich.md`, a rigorous prompt any AI coding
|
|
18
|
+
agent (Claude Code, Cursor, Copilot Workspace…) executes to verify-and-fill the
|
|
19
|
+
narrative sections, writing `.repokg/narratives.json`. Re-render and the knowledge graph is
|
|
20
|
+
complete. No API keys, no LLM dependency in the tool itself.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pipx install repokg # or: pip install repokg
|
|
26
|
+
# from source:
|
|
27
|
+
pipx install git+https://github.com/NehharShah/repokg
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Requirements: Python ≥ 3.9, `git`. Optional: [`gh`](https://cli.github.com) (logged
|
|
31
|
+
in) for the PR/branch cross-reference — without it the knowledge graph still builds, minus PR data.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
cd your-repo
|
|
37
|
+
repokg # = generate: scan + prompts + render
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Output:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
.repokg/kg.json # machine-readable knowledge graph
|
|
44
|
+
.repokg/prompts/enrich.md # hand this to your AI agent
|
|
45
|
+
KNOWLEDGE_GRAPH.md # the knowledge graph document
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then, in your AI agent of choice:
|
|
49
|
+
|
|
50
|
+
> Follow the instructions in .repokg/prompts/enrich.md
|
|
51
|
+
|
|
52
|
+
The agent explores the code, writes `.repokg/narratives.json`, and runs
|
|
53
|
+
`repokg render` — KNOWLEDGE_GRAPH.md now carries verified purposes, data flows,
|
|
54
|
+
timeline eras, and gotchas alongside the deterministic structure.
|
|
55
|
+
|
|
56
|
+
### Commands
|
|
57
|
+
|
|
58
|
+
| Command | Effect |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `repokg scan [path]` | Extract structure → `.repokg/kg.json` |
|
|
61
|
+
| `repokg prompts [path]` | Write the enrichment prompt |
|
|
62
|
+
| `repokg render [path]` | `kg.json` (+ `narratives.json`) → `KNOWLEDGE_GRAPH.md` |
|
|
63
|
+
| `repokg generate [path]` | All three (default) |
|
|
64
|
+
| `repokg inject [path]` | Wire the knowledge graph into `CLAUDE.md` / `AGENTS.md` / Cursor rules |
|
|
65
|
+
| `repokg check [path]` | Exit 1 if the knowledge graph is stale vs `HEAD` (CI-friendly) |
|
|
66
|
+
|
|
67
|
+
Flags: `--out DIR` (default `<repo>/.repokg`), `--md FILE` (default `<repo>/KNOWLEDGE_GRAPH.md`),
|
|
68
|
+
`--no-github`, `--pr-limit N`.
|
|
69
|
+
|
|
70
|
+
## Agent integration
|
|
71
|
+
|
|
72
|
+
`repokg inject` adds a **managed block** (delimited by
|
|
73
|
+
`<!-- repokg:begin/end -->`, idempotent, never touches your hand-written
|
|
74
|
+
content) pointing agents at KNOWLEDGE_GRAPH.md:
|
|
75
|
+
|
|
76
|
+
- **`CLAUDE.md`** (Claude Code) — updated if present
|
|
77
|
+
- **`AGENTS.md`** (the cross-tool agent standard) — updated if present, created if
|
|
78
|
+
no agent file exists at all
|
|
79
|
+
- **`.github/copilot-instructions.md`** (Copilot) — updated if present
|
|
80
|
+
- **`.cursor/rules/repokg.mdc`** (Cursor, with `alwaysApply: true`) — created
|
|
81
|
+
if `.cursor/rules/` exists; falls back to legacy `.cursorrules`
|
|
82
|
+
|
|
83
|
+
Keep it fresh in CI:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
- run: pipx run repokg check . || echo "::warning::KNOWLEDGE_GRAPH.md is stale"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
KNOWLEDGE_GRAPH.md itself also lists any agent-context files it found, so an agent landing
|
|
90
|
+
on the knowledge graph discovers your rules — and vice versa.
|
|
91
|
+
|
|
92
|
+
## What gets extracted (all verified, never guessed)
|
|
93
|
+
|
|
94
|
+
| Area | How |
|
|
95
|
+
|---|---|
|
|
96
|
+
| Branch classification | `git for-each-ref` + `--merged` ancestry vs the integration branch (auto-detects `staging`/`develop`), cross-referenced with every PR's head ref via `gh` — distinguishes true merges from squash-merges from abandoned work |
|
|
97
|
+
| PR catalog | `gh pr list --state all` — open / merged / closed-unmerged, full appendix table |
|
|
98
|
+
| Module inventory | Filesystem walk with LOC per directory, language detection, generated-code flagging |
|
|
99
|
+
| Import graph | Go: `import` blocks resolved against `go.mod` module paths · Python: stdlib `ast` incl. relative imports · JS/TS: relative `import`/`require` resolution. Directory→directory edges with counts |
|
|
100
|
+
| Ops surface | CI workflow names, Dockerfiles, compose files, Helm charts, Makefile targets, config/docs/test/migration dirs |
|
|
101
|
+
| Timeline | Merged PRs grouped by month with conventional-commit scope frequencies (replaced by agent-written eras after enrichment) |
|
|
102
|
+
|
|
103
|
+
## Why agent-first instead of calling an LLM API?
|
|
104
|
+
|
|
105
|
+
Because the enrichment quality depends on *reading the code*, and your coding agent
|
|
106
|
+
already has the repo open, tools to search it, and your permission model. A prompt it
|
|
107
|
+
can execute beats a second LLM integration with its own keys, costs, and context limits.
|
|
108
|
+
The contract between tool and agent is one JSON file (`narratives.json`) with a fixed
|
|
109
|
+
schema — everything else stays deterministic and reproducible.
|
|
110
|
+
|
|
111
|
+
## Known limitations
|
|
112
|
+
|
|
113
|
+
- **JS/TS**: only relative imports are resolved; alias imports (`@/…`,
|
|
114
|
+
tsconfig `paths`) are ignored.
|
|
115
|
+
- **Fork PRs**: a fork PR whose head branch name matches a local branch will be
|
|
116
|
+
linked to it (GitHub's API reports bare head refs).
|
|
117
|
+
- **Python**: packages are discovered at the repo root and under `src/`;
|
|
118
|
+
deeper monorepo layouts (`packages/*/src/…`) get file-level edges only.
|
|
119
|
+
- Branch `ahead` counts use one batched git call on git ≥ 2.41, with a
|
|
120
|
+
per-branch fallback on older git.
|
|
121
|
+
|
|
122
|
+
## Roadmap
|
|
123
|
+
|
|
124
|
+
- [ ] Rust / Java / Kotlin import graphs
|
|
125
|
+
- [ ] `--exclude` glob patterns
|
|
126
|
+
- [ ] `llms.txt` emission alongside KNOWLEDGE_GRAPH.md
|
|
127
|
+
- [ ] tsconfig `paths` alias resolution
|
|
128
|
+
- [ ] PyPI release + prebuilt GitHub Action
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
pip install -e .
|
|
134
|
+
python -m unittest discover -s tests -v
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
No runtime dependencies — stdlib only.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "repokg"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Generate an AI-ready knowledge graph (KNOWLEDGE_GRAPH.md + kg.json) of any codebase: modules, dependency edges, branches, PRs, ops surface."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Nihar Shah" }]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
keywords = ["knowledge-graph", "codebase", "ai", "agents", "documentation", "repo-map"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Software Development :: Documentation",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/NehharShah/repokg"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
repokg = "repokg.cli:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
repokg-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""repokg CLI: scan | prompts | render | generate."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import datetime
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from . import __version__, code, deps, github, gitinfo, inject, markdown, ops, prompts
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def scan(repo, out, no_github, pr_limit):
|
|
13
|
+
info, branches = gitinfo.collect(repo)
|
|
14
|
+
if no_github:
|
|
15
|
+
prs, note = [], "GitHub lookup disabled (--no-github)"
|
|
16
|
+
else:
|
|
17
|
+
prs, note = github.collect(repo, pr_limit)
|
|
18
|
+
github.classify(branches, prs, info["trunk"], info["integration"])
|
|
19
|
+
tree = dict(code.walk(repo)) # single filesystem walk, shared by all collectors
|
|
20
|
+
languages, modules = code.collect(repo, tree)
|
|
21
|
+
kg = {
|
|
22
|
+
"repokg_version": 1,
|
|
23
|
+
"generated_at": datetime.date.today().isoformat(),
|
|
24
|
+
"repo": info,
|
|
25
|
+
"languages": languages,
|
|
26
|
+
"modules": modules,
|
|
27
|
+
"edges": deps.collect(repo, tree),
|
|
28
|
+
"branches": branches,
|
|
29
|
+
"prs": prs,
|
|
30
|
+
"github_note": note,
|
|
31
|
+
"ops": ops.collect(repo, tree),
|
|
32
|
+
}
|
|
33
|
+
os.makedirs(out, exist_ok=True)
|
|
34
|
+
path = os.path.join(out, "kg.json")
|
|
35
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
36
|
+
json.dump(kg, f, indent=1)
|
|
37
|
+
print("wrote %s (%d modules, %d edges, %d branches, %d PRs)" %
|
|
38
|
+
(path, len(modules), len(kg["edges"]), len(branches), len(prs)))
|
|
39
|
+
return kg
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def write_prompts(repo, out, md):
|
|
43
|
+
pdir = os.path.join(out, "prompts")
|
|
44
|
+
os.makedirs(pdir, exist_ok=True)
|
|
45
|
+
path = os.path.join(pdir, "enrich.md")
|
|
46
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
47
|
+
f.write(prompts.render(repo, out, md))
|
|
48
|
+
print("wrote %s (hand this to your AI agent)" % path)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def render(out, md):
|
|
52
|
+
with open(os.path.join(out, "kg.json"), encoding="utf-8") as f:
|
|
53
|
+
kg = json.load(f)
|
|
54
|
+
narratives = {}
|
|
55
|
+
npath = os.path.join(out, "narratives.json")
|
|
56
|
+
if os.path.isfile(npath):
|
|
57
|
+
with open(npath, encoding="utf-8") as f:
|
|
58
|
+
narratives = json.load(f)
|
|
59
|
+
doc = markdown.render(kg, narratives)
|
|
60
|
+
with open(md, "w", encoding="utf-8") as f:
|
|
61
|
+
f.write(doc)
|
|
62
|
+
state = "enriched" if narratives else "structure-only; run .repokg/prompts/enrich.md to enrich"
|
|
63
|
+
print("wrote %s (%s)" % (md, state))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def do_inject(repo, md):
|
|
67
|
+
for path, status in inject.run(repo, md).items():
|
|
68
|
+
print("%s: %s" % (path, status))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def check(repo, out, md):
|
|
72
|
+
"""Exit 0 if the knowledge graph matches HEAD, 1 if stale/missing. CI-friendly."""
|
|
73
|
+
apath = os.path.join(out, "kg.json")
|
|
74
|
+
if not os.path.isfile(apath) or not os.path.isfile(md):
|
|
75
|
+
print("stale: knowledge graph not generated (run `repokg generate`)")
|
|
76
|
+
return 1
|
|
77
|
+
with open(apath, encoding="utf-8") as f:
|
|
78
|
+
stored = json.load(f).get("repo", {}).get("head", "")
|
|
79
|
+
head = gitinfo.try_run(repo, "rev-parse", "HEAD")
|
|
80
|
+
if stored and head and stored != head:
|
|
81
|
+
print("stale: knowledge graph at %s, HEAD is %s (run `repokg generate`)"
|
|
82
|
+
% (stored[:12], head[:12]))
|
|
83
|
+
return 1
|
|
84
|
+
print("fresh: knowledge graph matches HEAD %s" % (head[:12] or "(unknown)"))
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main(argv=None):
|
|
89
|
+
ap = argparse.ArgumentParser(
|
|
90
|
+
prog="repokg",
|
|
91
|
+
description="Generate an AI-ready knowledge graph of a codebase.")
|
|
92
|
+
ap.add_argument("command", nargs="?", default="generate",
|
|
93
|
+
choices=["scan", "prompts", "render", "generate", "inject",
|
|
94
|
+
"check", "version"],
|
|
95
|
+
help="scan: extract structure to .repokg/kg.json | "
|
|
96
|
+
"prompts: write the AI enrichment prompt | "
|
|
97
|
+
"render: kg.json (+narratives.json) -> KNOWLEDGE_GRAPH.md | "
|
|
98
|
+
"generate: scan + prompts + render (default) | "
|
|
99
|
+
"inject: add knowledge-graph pointer to CLAUDE.md/AGENTS.md/cursor rules | "
|
|
100
|
+
"check: exit 1 if knowledge graph is stale vs HEAD")
|
|
101
|
+
ap.add_argument("path", nargs="?", default=".", help="repository path (default: .)")
|
|
102
|
+
ap.add_argument("--out", default=None, help="output dir (default: <repo>/.repokg)")
|
|
103
|
+
ap.add_argument("--md", default=None, help="markdown output (default: <repo>/KNOWLEDGE_GRAPH.md)")
|
|
104
|
+
ap.add_argument("--no-github", action="store_true", help="skip gh PR lookup")
|
|
105
|
+
ap.add_argument("--pr-limit", type=int, default=1000, help="max PRs to fetch (default 1000)")
|
|
106
|
+
args = ap.parse_args(argv)
|
|
107
|
+
|
|
108
|
+
if args.command == "version":
|
|
109
|
+
print("repokg %s" % __version__)
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
repo = os.path.abspath(args.path)
|
|
113
|
+
if not os.path.isdir(repo):
|
|
114
|
+
print("error: %s is not a directory" % repo, file=sys.stderr)
|
|
115
|
+
return 2
|
|
116
|
+
out = args.out or os.path.join(repo, ".repokg")
|
|
117
|
+
md = args.md or os.path.join(repo, "KNOWLEDGE_GRAPH.md")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
if args.command == "scan":
|
|
121
|
+
scan(repo, out, args.no_github, args.pr_limit)
|
|
122
|
+
elif args.command == "prompts":
|
|
123
|
+
write_prompts(repo, out, md)
|
|
124
|
+
elif args.command == "render":
|
|
125
|
+
render(out, md)
|
|
126
|
+
elif args.command == "inject":
|
|
127
|
+
do_inject(repo, md)
|
|
128
|
+
elif args.command == "check":
|
|
129
|
+
return check(repo, out, md)
|
|
130
|
+
else: # generate
|
|
131
|
+
scan(repo, out, args.no_github, args.pr_limit)
|
|
132
|
+
write_prompts(repo, out, md)
|
|
133
|
+
render(out, md)
|
|
134
|
+
except RuntimeError as e:
|
|
135
|
+
print("error: %s" % e, file=sys.stderr)
|
|
136
|
+
return 1
|
|
137
|
+
except FileNotFoundError as e:
|
|
138
|
+
print("error: %s (run `repokg scan` first?)" % e, file=sys.stderr)
|
|
139
|
+
return 1
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
sys.exit(main())
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Code collectors: language breakdown, module discovery, LOC."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
SKIP_DIRS = {
|
|
7
|
+
".git", "node_modules", "vendor", "dist", "build", "out", "target", ".next",
|
|
8
|
+
".venv", "venv", "__pycache__", ".idea", ".vscode", ".repokg", "coverage",
|
|
9
|
+
".terraform", "third_party", ".tox", ".mypy_cache", ".ruff_cache",
|
|
10
|
+
"site-packages", ".pytest_cache", ".cache",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
LANG_BY_EXT = {
|
|
14
|
+
".go": "Go", ".py": "Python", ".ts": "TypeScript", ".tsx": "TypeScript",
|
|
15
|
+
".js": "JavaScript", ".jsx": "JavaScript", ".mjs": "JavaScript",
|
|
16
|
+
".rs": "Rust", ".java": "Java", ".kt": "Kotlin", ".rb": "Ruby",
|
|
17
|
+
".php": "PHP", ".cs": "C#", ".c": "C", ".cpp": "C++", ".cc": "C++",
|
|
18
|
+
".h": "C/C++", ".hpp": "C++", ".sol": "Solidity", ".swift": "Swift",
|
|
19
|
+
".scala": "Scala", ".ex": "Elixir", ".exs": "Elixir", ".zig": "Zig",
|
|
20
|
+
".lua": "Lua", ".dart": "Dart", ".vue": "Vue", ".svelte": "Svelte",
|
|
21
|
+
".sql": "SQL", ".sh": "Shell", ".proto": "Protobuf", ".tf": "Terraform",
|
|
22
|
+
".yaml": "YAML", ".yml": "YAML", ".html": "HTML", ".css": "CSS",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Languages whose presence makes a directory a "module" worth graphing.
|
|
26
|
+
MODULE_LANGS = {
|
|
27
|
+
"Go", "Python", "TypeScript", "JavaScript", "Rust", "Java", "Kotlin",
|
|
28
|
+
"Ruby", "PHP", "C#", "C", "C++", "Solidity", "Swift", "Scala", "Elixir",
|
|
29
|
+
"Zig", "Lua", "Dart", "Vue", "Svelte",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ROOT_MARKERS = {
|
|
33
|
+
"go.mod", "package.json", "pyproject.toml", "setup.py", "Cargo.toml",
|
|
34
|
+
"pom.xml", "build.gradle", "composer.json", "Gemfile",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
GENERATED_RE = re.compile(r"(generated|sqlcgen|_pb2|\.pb\.|/pb$|/pb/|bindings)")
|
|
38
|
+
MAX_FILE_BYTES = 2_000_000
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def walk(repo):
|
|
42
|
+
"""os.walk with skip-dir pruning; yields (rel_dir, filenames)."""
|
|
43
|
+
for dirpath, dirnames, filenames in os.walk(repo):
|
|
44
|
+
dirnames[:] = sorted(
|
|
45
|
+
d for d in dirnames
|
|
46
|
+
if d not in SKIP_DIRS and not (d.startswith(".") and d != ".github")
|
|
47
|
+
)
|
|
48
|
+
rel = os.path.relpath(dirpath, repo)
|
|
49
|
+
yield ("" if rel == "." else rel.replace(os.sep, "/")), filenames
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def count_loc(path):
|
|
53
|
+
try:
|
|
54
|
+
if os.path.getsize(path) > MAX_FILE_BYTES:
|
|
55
|
+
return 0
|
|
56
|
+
with open(path, "rb") as f:
|
|
57
|
+
data = f.read()
|
|
58
|
+
if b"\0" in data[:1024]: # binary
|
|
59
|
+
return 0
|
|
60
|
+
return data.count(b"\n") + (1 if data and not data.endswith(b"\n") else 0)
|
|
61
|
+
except OSError:
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def collect(repo, tree=None):
|
|
66
|
+
"""Return (languages, modules).
|
|
67
|
+
|
|
68
|
+
languages: [{lang, files, loc}] sorted by loc desc
|
|
69
|
+
modules: [{path, lang, files, loc, root, generated}] one per dir containing code
|
|
70
|
+
tree: optional pre-built {rel_dir: filenames} to avoid re-walking the repo.
|
|
71
|
+
"""
|
|
72
|
+
lang_stats = {}
|
|
73
|
+
modules = []
|
|
74
|
+
for rel, files in (tree.items() if tree is not None else walk(repo)):
|
|
75
|
+
if rel.startswith(".github"):
|
|
76
|
+
continue
|
|
77
|
+
per_lang = {}
|
|
78
|
+
is_root = any(f in ROOT_MARKERS for f in files)
|
|
79
|
+
for f in files:
|
|
80
|
+
ext = os.path.splitext(f)[1].lower()
|
|
81
|
+
lang = LANG_BY_EXT.get(ext)
|
|
82
|
+
if not lang:
|
|
83
|
+
continue
|
|
84
|
+
loc = count_loc(os.path.join(repo, rel, f))
|
|
85
|
+
ls = lang_stats.setdefault(lang, {"lang": lang, "files": 0, "loc": 0})
|
|
86
|
+
ls["files"] += 1
|
|
87
|
+
ls["loc"] += loc
|
|
88
|
+
if lang in MODULE_LANGS:
|
|
89
|
+
pl = per_lang.setdefault(lang, [0, 0])
|
|
90
|
+
pl[0] += 1
|
|
91
|
+
pl[1] += loc
|
|
92
|
+
if per_lang:
|
|
93
|
+
dominant = max(per_lang.items(), key=lambda kv: kv[1][1])
|
|
94
|
+
modules.append({
|
|
95
|
+
"path": rel or "(root)",
|
|
96
|
+
"lang": dominant[0],
|
|
97
|
+
"files": sum(v[0] for v in per_lang.values()),
|
|
98
|
+
"loc": sum(v[1] for v in per_lang.values()),
|
|
99
|
+
"root": is_root,
|
|
100
|
+
"generated": bool(GENERATED_RE.search("/" + rel)),
|
|
101
|
+
})
|
|
102
|
+
languages = sorted(lang_stats.values(), key=lambda x: -x["loc"])
|
|
103
|
+
modules.sort(key=lambda m: -m["loc"])
|
|
104
|
+
return languages, modules
|