prgenius-core 0.7.8__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.
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: prgenius-core
3
+ Version: 0.7.8
4
+ Summary: Evidence-backed lookup for big-repo PR contributions. Local-only, zero-runtime, stdlib-first. Renamed from `prgenius` to avoid PyPI name collision with an unrelated 2024 GPT-3 PR-description tool. Distribution renamed again in v0.7.8: `prgenius-kb` → `prgenius-core` (aligned with `misakanet-core` convention).
5
+ Author: pr-genius contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/zsxh1990/pr-genius
8
+ Project-URL: Issues, https://github.com/zsxh1990/pr-genius/issues
9
+ Keywords: okf,pr-knowledge,agent-readable,contribution-intelligence,evidence
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Software Development :: Documentation
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ Provides-Extra: mcp
21
+ Requires-Dist: mcp>=1.0; extra == "mcp"
22
+ Provides-Extra: dev
23
+ Requires-Dist: build; extra == "dev"
24
+ Requires-Dist: twine; extra == "dev"
25
+ Requires-Dist: pytest; extra == "dev"
26
+
27
+ ---
28
+ type: Schema Reference
29
+ title: prgenius
30
+ description: Evidence-backed lookup library for big-repo PR contributions — stdlib-first CLI + stdio MCP shell.
31
+ version: 0.7.0
32
+ created: 2026-07-04
33
+ updated: 2026-07-05
34
+ author: zsxh1990
35
+ ---
36
+
37
+ # prgenius
38
+
39
+ **Evidence-backed lookup for big-repo PR contributions. Local-only. Stdlib-first.**
40
+
41
+ A pure-Python (zero hard deps) library + CLI that reads the markdown knowledge
42
+ base in this repo and exposes it through structured tool calls.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install prgenius-core
48
+ ```
49
+
50
+ Or run from a checkout:
51
+
52
+ ```bash
53
+ cd prgenius
54
+ PYTHONPATH=src python3 -m prgenius --version
55
+ ```
56
+
57
+ ## Important: PyPI package is the *interface*, not the data
58
+
59
+ The PyPI wheel ships only the Python code (`prgenius/` package).
60
+ **The knowledge base (profile markdown, case studies, OKF schemas) lives
61
+ in the GitHub repo** at <https://github.com/zsxh1990/pr-genius> and is
62
+ **not** bundled into the wheel. To use `prgenius-core` after `pip install`,
63
+ point it at a checkout of the knowledge base:
64
+
65
+ ```bash
66
+ git clone https://github.com/zsxh1990/pr-genius
67
+ prgenius-core --repo-root ./pr-genius profile get astral-sh/uv
68
+ ```
69
+
70
+ If you cloned a specific tag/commit, the package's `__version__` should
71
+ match (e.g. v0.7.7 ↔ `prgenius-core==0.7.7`).
72
+
73
+ ### Optional: MCP server
74
+
75
+ The MCP entry point (`prgenius-core mcp serve`) requires the `mcp` package:
76
+
77
+ ```bash
78
+ pip install "prgenius-core[mcp]"
79
+ ```
80
+
81
+ ## Quick Start
82
+
83
+ ```bash
84
+ # 1) Schema reference
85
+ PYTHONPATH=src python3 -m prgenius schema info
86
+
87
+ # 2) Look up one repo
88
+ PYTHONPATH=src python3 -m prgenius profile get astral-sh/uv
89
+
90
+ # 3) List currently-open case studies
91
+ PYTHONPATH=src python3 -m prgenius case list --status=open
92
+
93
+ # 4) NDJSON dump of every case study (for benchmarks / agent context)
94
+ PYTHONPATH=src python3 -m prgenius dump > cases.ndjson
95
+
96
+ # 5) Run as stdio MCP server (for Cursor/Cline/Claude Code)
97
+ PYTHONPATH=src python3 -m prgenius mcp serve
98
+ ```
99
+
100
+ ## Programmatic use
101
+
102
+ ```python
103
+ from prgenius.parser import profile_get, iter_case_studies, schema_info
104
+
105
+ prof = profile_get("path/to/repo_root", "astral-sh/uv")
106
+ print(prof["frontmatter"]["title"])
107
+
108
+ for case in iter_case_studies("path/to/repo_root"):
109
+ fm = case["frontmatter"]
110
+ if fm.get("final_status") == "open":
111
+ print(fm.get("pr_number"), fm.get("repo"))
112
+ ```
113
+
114
+ ## What's exposed
115
+
116
+ | Tool | Description |
117
+ |---|---|
118
+ | `prgenius profile get <org/name>` | One Repo Profile (frontmatter + first lines) |
119
+ | `prgenius case list [--status=...]` | All PR Case Study rows |
120
+ | `prgenius schema info` | Supported schema versions + enums |
121
+ | `prgenius dump` | NDJSON dump (one case per line) |
122
+ | `prgenius mcp serve` | Stdio MCP server (4 tools) |
123
+
124
+ ## MCP surface (when `mcp` is installed)
125
+
126
+ The MCP shell exposes 4 tools to local agents:
127
+
128
+ - `get_repo_profile(repo)` — one Profile dict
129
+ - `list_open_prs()` — currently-open PRs
130
+ - `get_case_study(repo, pr_number)` — one Case Study
131
+ - `schema_info()` — schema versions + enums
132
+
133
+ No network, no auth, no rate-limiting. Agent calls go through stdio only.
134
+
135
+ ### `--repo-root` flag
136
+
137
+ The MCP server defaults to a path computed from its install location
138
+ (`<package>/../..` × 3 to reach the knowledge base). When that path is
139
+ wrong (editable install, venv, worktree, fork), pass `--repo-root`:
140
+
141
+ ```bash
142
+ python3 -m prgenius --repo-root /path/to/big-repo-pr-knowledge mcp serve
143
+ ```
144
+
145
+ ### Wiring into Cursor / Claude Code / Cline
146
+
147
+ These editors all read MCP server config from JSON. Point `command` at
148
+ `python3 -m prgenius` and `args` at `mcp serve` (add `--repo-root` if the
149
+ auto-detected path doesn't match your checkout). No `env` keys, no auth.
150
+
151
+ **Claude Code** (`~/.claude/mcp.json` or project-local `.mcp.json`):
152
+
153
+ ```json
154
+ {
155
+ "mcpServers": {
156
+ "pr-genius": {
157
+ "command": "python3",
158
+ "args": ["-m", "prgenius", "--repo-root", "/abs/path/to/big-repo-pr-knowledge", "mcp", "serve"]
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ **Cursor** (`~/.cursor/mcp.json`):
165
+
166
+ ```json
167
+ {
168
+ "mcpServers": {
169
+ "pr-genius": {
170
+ "command": "python3 -m prgenius",
171
+ "args": ["--repo-root", "/abs/path/to/big-repo-pr-knowledge", "mcp", "serve"]
172
+ }
173
+ }
174
+ }
175
+ ```
176
+
177
+ **Cline** (VS Code `cline_mcp_settings.json`):
178
+
179
+ ```json
180
+ {
181
+ "mcpServers": {
182
+ "pr-genius": {
183
+ "command": "python3",
184
+ "args": ["-m", "prgenius", "--repo-root", "/abs/path/to/big-repo-pr-knowledge", "mcp", "serve"],
185
+ "disabled": false
186
+ }
187
+ }
188
+ }
189
+ ```
190
+
191
+ Replace `/abs/path/to/big-repo-pr-knowledge` with the actual checkout.
192
+ Omit `--repo-root` only if you installed prgenius from this exact
193
+ checkout (then the default path resolves correctly).
194
+
195
+ ## Schema we honor
196
+
197
+ - **rounds v0.5.0** — `action` enum + `delta` object + case-level `close_decision`
198
+ - **rounds v0.7.0** — adds optional `verified_at`, `evidence_urls`, `confidence`
199
+ to round + case level (BC over v0.5.0)
200
+
201
+ See [../ROUNDS_SCHEMA.md](../ROUNDS_SCHEMA.md) for the canonical schema.
202
+
203
+ ## Why this exists
204
+
205
+ The repo is meant to be human-readable (`<org>-<repo>/index.md`) AND
206
+ machine-readable via this package. People get the narrative; agents get
207
+ structured tool calls. Both views share one source of truth.
208
+
209
+ ## License
210
+
211
+ MIT (same as parent repo).
@@ -0,0 +1,185 @@
1
+ ---
2
+ type: Schema Reference
3
+ title: prgenius
4
+ description: Evidence-backed lookup library for big-repo PR contributions — stdlib-first CLI + stdio MCP shell.
5
+ version: 0.7.0
6
+ created: 2026-07-04
7
+ updated: 2026-07-05
8
+ author: zsxh1990
9
+ ---
10
+
11
+ # prgenius
12
+
13
+ **Evidence-backed lookup for big-repo PR contributions. Local-only. Stdlib-first.**
14
+
15
+ A pure-Python (zero hard deps) library + CLI that reads the markdown knowledge
16
+ base in this repo and exposes it through structured tool calls.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install prgenius-core
22
+ ```
23
+
24
+ Or run from a checkout:
25
+
26
+ ```bash
27
+ cd prgenius
28
+ PYTHONPATH=src python3 -m prgenius --version
29
+ ```
30
+
31
+ ## Important: PyPI package is the *interface*, not the data
32
+
33
+ The PyPI wheel ships only the Python code (`prgenius/` package).
34
+ **The knowledge base (profile markdown, case studies, OKF schemas) lives
35
+ in the GitHub repo** at <https://github.com/zsxh1990/pr-genius> and is
36
+ **not** bundled into the wheel. To use `prgenius-core` after `pip install`,
37
+ point it at a checkout of the knowledge base:
38
+
39
+ ```bash
40
+ git clone https://github.com/zsxh1990/pr-genius
41
+ prgenius-core --repo-root ./pr-genius profile get astral-sh/uv
42
+ ```
43
+
44
+ If you cloned a specific tag/commit, the package's `__version__` should
45
+ match (e.g. v0.7.7 ↔ `prgenius-core==0.7.7`).
46
+
47
+ ### Optional: MCP server
48
+
49
+ The MCP entry point (`prgenius-core mcp serve`) requires the `mcp` package:
50
+
51
+ ```bash
52
+ pip install "prgenius-core[mcp]"
53
+ ```
54
+
55
+ ## Quick Start
56
+
57
+ ```bash
58
+ # 1) Schema reference
59
+ PYTHONPATH=src python3 -m prgenius schema info
60
+
61
+ # 2) Look up one repo
62
+ PYTHONPATH=src python3 -m prgenius profile get astral-sh/uv
63
+
64
+ # 3) List currently-open case studies
65
+ PYTHONPATH=src python3 -m prgenius case list --status=open
66
+
67
+ # 4) NDJSON dump of every case study (for benchmarks / agent context)
68
+ PYTHONPATH=src python3 -m prgenius dump > cases.ndjson
69
+
70
+ # 5) Run as stdio MCP server (for Cursor/Cline/Claude Code)
71
+ PYTHONPATH=src python3 -m prgenius mcp serve
72
+ ```
73
+
74
+ ## Programmatic use
75
+
76
+ ```python
77
+ from prgenius.parser import profile_get, iter_case_studies, schema_info
78
+
79
+ prof = profile_get("path/to/repo_root", "astral-sh/uv")
80
+ print(prof["frontmatter"]["title"])
81
+
82
+ for case in iter_case_studies("path/to/repo_root"):
83
+ fm = case["frontmatter"]
84
+ if fm.get("final_status") == "open":
85
+ print(fm.get("pr_number"), fm.get("repo"))
86
+ ```
87
+
88
+ ## What's exposed
89
+
90
+ | Tool | Description |
91
+ |---|---|
92
+ | `prgenius profile get <org/name>` | One Repo Profile (frontmatter + first lines) |
93
+ | `prgenius case list [--status=...]` | All PR Case Study rows |
94
+ | `prgenius schema info` | Supported schema versions + enums |
95
+ | `prgenius dump` | NDJSON dump (one case per line) |
96
+ | `prgenius mcp serve` | Stdio MCP server (4 tools) |
97
+
98
+ ## MCP surface (when `mcp` is installed)
99
+
100
+ The MCP shell exposes 4 tools to local agents:
101
+
102
+ - `get_repo_profile(repo)` — one Profile dict
103
+ - `list_open_prs()` — currently-open PRs
104
+ - `get_case_study(repo, pr_number)` — one Case Study
105
+ - `schema_info()` — schema versions + enums
106
+
107
+ No network, no auth, no rate-limiting. Agent calls go through stdio only.
108
+
109
+ ### `--repo-root` flag
110
+
111
+ The MCP server defaults to a path computed from its install location
112
+ (`<package>/../..` × 3 to reach the knowledge base). When that path is
113
+ wrong (editable install, venv, worktree, fork), pass `--repo-root`:
114
+
115
+ ```bash
116
+ python3 -m prgenius --repo-root /path/to/big-repo-pr-knowledge mcp serve
117
+ ```
118
+
119
+ ### Wiring into Cursor / Claude Code / Cline
120
+
121
+ These editors all read MCP server config from JSON. Point `command` at
122
+ `python3 -m prgenius` and `args` at `mcp serve` (add `--repo-root` if the
123
+ auto-detected path doesn't match your checkout). No `env` keys, no auth.
124
+
125
+ **Claude Code** (`~/.claude/mcp.json` or project-local `.mcp.json`):
126
+
127
+ ```json
128
+ {
129
+ "mcpServers": {
130
+ "pr-genius": {
131
+ "command": "python3",
132
+ "args": ["-m", "prgenius", "--repo-root", "/abs/path/to/big-repo-pr-knowledge", "mcp", "serve"]
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ **Cursor** (`~/.cursor/mcp.json`):
139
+
140
+ ```json
141
+ {
142
+ "mcpServers": {
143
+ "pr-genius": {
144
+ "command": "python3 -m prgenius",
145
+ "args": ["--repo-root", "/abs/path/to/big-repo-pr-knowledge", "mcp", "serve"]
146
+ }
147
+ }
148
+ }
149
+ ```
150
+
151
+ **Cline** (VS Code `cline_mcp_settings.json`):
152
+
153
+ ```json
154
+ {
155
+ "mcpServers": {
156
+ "pr-genius": {
157
+ "command": "python3",
158
+ "args": ["-m", "prgenius", "--repo-root", "/abs/path/to/big-repo-pr-knowledge", "mcp", "serve"],
159
+ "disabled": false
160
+ }
161
+ }
162
+ }
163
+ ```
164
+
165
+ Replace `/abs/path/to/big-repo-pr-knowledge` with the actual checkout.
166
+ Omit `--repo-root` only if you installed prgenius from this exact
167
+ checkout (then the default path resolves correctly).
168
+
169
+ ## Schema we honor
170
+
171
+ - **rounds v0.5.0** — `action` enum + `delta` object + case-level `close_decision`
172
+ - **rounds v0.7.0** — adds optional `verified_at`, `evidence_urls`, `confidence`
173
+ to round + case level (BC over v0.5.0)
174
+
175
+ See [../ROUNDS_SCHEMA.md](../ROUNDS_SCHEMA.md) for the canonical schema.
176
+
177
+ ## Why this exists
178
+
179
+ The repo is meant to be human-readable (`<org>-<repo>/index.md`) AND
180
+ machine-readable via this package. People get the narrative; agents get
181
+ structured tool calls. Both views share one source of truth.
182
+
183
+ ## License
184
+
185
+ MIT (same as parent repo).
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "prgenius-core"
3
+ version = "0.7.8"
4
+ description = "Evidence-backed lookup for big-repo PR contributions. Local-only, zero-runtime, stdlib-first. Renamed from `prgenius` to avoid PyPI name collision with an unrelated 2024 GPT-3 PR-description tool. Distribution renamed again in v0.7.8: `prgenius-kb` → `prgenius-core` (aligned with `misakanet-core` convention)."
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = "MIT"
8
+ authors = [{name = "pr-genius contributors"}]
9
+ keywords = ["okf", "pr-knowledge", "agent-readable", "contribution-intelligence", "evidence"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.9",
14
+ "Programming Language :: Python :: 3.10",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Software Development :: Documentation",
19
+ ]
20
+ dependencies = [] # stdlib only
21
+
22
+ [project.optional-dependencies]
23
+ mcp = ["mcp>=1.0"]
24
+ dev = ["build", "twine", "pytest"]
25
+
26
+ [project.scripts]
27
+ prgenius-core = "prgenius.cli:main"
28
+
29
+ [project.urls]
30
+ "Homepage" = "https://github.com/zsxh1990/pr-genius"
31
+ "Issues" = "https://github.com/zsxh1990/pr-genius/issues"
32
+
33
+ [build-system]
34
+ requires = ["setuptools>=61.0", "wheel"]
35
+ build-backend = "setuptools.build_meta"
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
39
+
40
+ [tool.setuptools.package-data]
41
+ prgenius = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,21 @@
1
+ """prgenius — Evidence-backed lookup for big-repo PR contributions.
2
+
3
+ Local-only. Zero runtime ops. Stdlib-only (PyYAML opt-in via extras).
4
+
5
+ Public surface:
6
+ - load() — parse one markdown file into a dict (frontmatter + body)
7
+ - iter_profiles(repo_root) — yield Repo Profile dicts
8
+ - iter_case_studies(repo_root) — yield PR Case Study dicts
9
+ - profile_get(repo_root, "<org>/<repo>") — look up a single profile
10
+ - schema_info() — return supported OKF schema versions
11
+ """
12
+
13
+ __version__ = "0.7.8"
14
+ __all__ = [
15
+ "__version__",
16
+ "load",
17
+ "iter_profiles",
18
+ "iter_case_studies",
19
+ "profile_get",
20
+ "schema_info",
21
+ ]
@@ -0,0 +1,3 @@
1
+ from prgenius.cli import main
2
+ import sys
3
+ sys.exit(main())
@@ -0,0 +1,149 @@
1
+ """CLI entry point for `prgenius`.
2
+
3
+ Usage examples (run from repo root):
4
+ python3 -m prgenius profile get astral-sh/uv
5
+ python3 -m prgenius case list --status=open
6
+ python3 -m prgenius schema info
7
+ python3 -m prgenius dump --format=ndjson > cases.ndjson
8
+ python3 -m prgenius mcp serve # stdio MCP shell
9
+ """
10
+ from __future__ import annotations
11
+ import argparse
12
+ import json
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ from . import __version__
17
+ from .parser import (
18
+ iter_profiles,
19
+ iter_case_studies,
20
+ profile_get,
21
+ schema_info,
22
+ )
23
+
24
+
25
+ REPO_ROOT_DEFAULT = Path(__file__).resolve().parents[3] # up to big-repo-pr-knowledge
26
+
27
+
28
+ def _get_repo_root(args) -> Path:
29
+ if args.repo_root:
30
+ return Path(args.repo_root).resolve()
31
+ return REPO_ROOT_DEFAULT
32
+
33
+
34
+ def cmd_profile_get(args) -> int:
35
+ p = profile_get(_get_repo_root(args), args.repo)
36
+ if p is None:
37
+ print(f"profile not found: {args.repo}", file=sys.stderr)
38
+ return 2
39
+ out = {
40
+ "path": p["path"],
41
+ "folder": p["folder"],
42
+ "frontmatter": p["frontmatter"],
43
+ "first_lines": p["body"].splitlines()[:30],
44
+ }
45
+ print(json.dumps(out, indent=2, ensure_ascii=False))
46
+ return 0
47
+
48
+
49
+ def cmd_case_list(args) -> int:
50
+ rows = []
51
+ for c in iter_case_studies(_get_repo_root(args)):
52
+ fm = c["frontmatter"]
53
+ fs = fm.get("final_status", "?")
54
+ if args.status and fs != args.status:
55
+ continue
56
+ rows.append({
57
+ "folder": c["folder"],
58
+ "pr_number": fm.get("pr_number"),
59
+ "pr_url": fm.get("pr_url"),
60
+ "final_status": fs,
61
+ "schema_version": fm.get("schema_version", "legacy v0.1"),
62
+ "verified_at": fm.get("verified_at"),
63
+ })
64
+ print(json.dumps(rows, indent=2, ensure_ascii=False))
65
+ return 0
66
+
67
+
68
+ def cmd_schema_info(_args) -> int:
69
+ print(json.dumps(schema_info(), indent=2, ensure_ascii=False))
70
+ return 0
71
+
72
+
73
+ def cmd_dump(args) -> int:
74
+ """Dump everything in NDJSON (one PR per line) for benchmark use."""
75
+ root = _get_repo_root(args)
76
+ for c in iter_case_studies(root):
77
+ fm = c["frontmatter"]
78
+ record = {
79
+ "folder": c["folder"],
80
+ "pr_file": c["pr_file"],
81
+ "pr_number": fm.get("pr_number"),
82
+ "pr_url": fm.get("pr_url"),
83
+ "repo": fm.get("repo"),
84
+ "final_status": fm.get("final_status"),
85
+ "opened_at": fm.get("opened_at"),
86
+ "merged_at": fm.get("merged_at"),
87
+ "closed_at": fm.get("closed_at"),
88
+ "schema_version": fm.get("schema_version", "legacy v0.1"),
89
+ "verified_at": fm.get("verified_at"),
90
+ "evidence_urls": fm.get("evidence_urls", []),
91
+ "confidence": fm.get("confidence"),
92
+ "rounds": fm.get("rounds", []),
93
+ "close_decision": fm.get("close_decision"),
94
+ }
95
+ print(json.dumps(record, ensure_ascii=False))
96
+ return 0
97
+
98
+
99
+ def cmd_mcp_serve(args) -> int:
100
+ """stdio MCP shell. See prgenius/mcp.py for the small facade.
101
+
102
+ Pass --repo-root to point the MCP server at a non-default location
103
+ (e.g. an isolated worktree or a forked copy of the knowledge base).
104
+ """
105
+ from .mcp import serve
106
+ repo_root = _get_repo_root(args)
107
+ return serve(repo_root=repo_root)
108
+
109
+
110
+ def main(argv: list[str] | None = None) -> int:
111
+ parser = argparse.ArgumentParser(
112
+ prog="prgenius",
113
+ description="Evidence-backed lookup for big-repo PR contributions.",
114
+ )
115
+ parser.add_argument("--repo-root", help="Path to big-repo-pr-knowledge (default: auto-detect)")
116
+ parser.add_argument("--version", action="version", version=f"prgenius {__version__}")
117
+ sub = parser.add_subparsers(dest="cmd", required=True)
118
+
119
+ p_get = sub.add_parser("profile", help="Profile operations")
120
+ p_get_sub = p_get.add_subparsers(dest="profile_cmd", required=True)
121
+ pp = p_get_sub.add_parser("get", help="Get one profile")
122
+ pp.add_argument("repo", help="org/name (e.g. astral-sh/uv)")
123
+ pp.set_defaults(func=cmd_profile_get)
124
+
125
+ c_list = sub.add_parser("case", help="Case study operations")
126
+ c_list_sub = c_list.add_subparsers(dest="case_cmd", required=True)
127
+ cl = c_list_sub.add_parser("list", help="List case studies")
128
+ cl.add_argument("--status", help="Filter by final_status (open/merged/...)")
129
+ cl.set_defaults(func=cmd_case_list)
130
+
131
+ s_info = sub.add_parser("schema", help="Schema info")
132
+ s_info_sub = s_info.add_subparsers(dest="schema_cmd", required=True)
133
+ si = s_info_sub.add_parser("info", help="Show supported schema versions")
134
+ si.set_defaults(func=cmd_schema_info)
135
+
136
+ dmp = sub.add_parser("dump", help="NDJSON dump of all cases (for benchmarks)")
137
+ dmp.set_defaults(func=cmd_dump)
138
+
139
+ m_serve = sub.add_parser("mcp", help="MCP server (stdio)")
140
+ m_serve_sub = m_serve.add_subparsers(dest="mcp_cmd", required=True)
141
+ ms = m_serve_sub.add_parser("serve", help="Run stdio MCP shell")
142
+ ms.set_defaults(func=cmd_mcp_serve)
143
+
144
+ args = parser.parse_args(argv)
145
+ return args.func(args)
146
+
147
+
148
+ if __name__ == "__main__":
149
+ sys.exit(main())
@@ -0,0 +1,103 @@
1
+ """stdio MCP shell for prgenius — a thin façade.
2
+
3
+ This module is optional: only loaded if `mcp` is installed (the only runtime
4
+ dependency we ever take). Calling `python3 -m prgenius mcp serve` starts a
5
+ stdio MCP server that exposes the prgenius lookup surface to local agents
6
+ (Cursor/Cline/Claude Code). No network, no auth, no rate-limiting.
7
+
8
+ Promoted tools (intentionally small — ACD rule "first to simplify wins"):
9
+ - get_repo_profile(repo) → returns one Repo Profile dict
10
+ - list_open_prs() → list of PRs with final_status=open
11
+ - get_case_study(repo, pr_number) → one PR Case Study
12
+ - schema_info() → enumerated schema versions
13
+
14
+ This file is ~70 lines (one per tool + boilerplate). Anyone reading it should
15
+ be able to verify the surface and the path-resolution logic in under 5 min.
16
+ """
17
+ from __future__ import annotations
18
+ import json
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ # mcp SDK: only imported at serve() time so the rest of the package stays stdlib.
23
+ REPO_ROOT_DEFAULT = Path(__file__).resolve().parents[3] # up to big-repo-pr-knowledge
24
+
25
+
26
+ def _load_tools(repo_root: Path | None = None):
27
+ from mcp.server.fastmcp import FastMCP
28
+ from .parser import (
29
+ iter_case_studies,
30
+ profile_get,
31
+ schema_info,
32
+ )
33
+
34
+ mcp = FastMCP(name="prgenius", instructions=(
35
+ "Evidence-backed lookup for big-repo PR contributions. "
36
+ "Local stdio; no network calls."
37
+ ))
38
+
39
+ rr = repo_root or REPO_ROOT_DEFAULT
40
+
41
+ @mcp.tool()
42
+ def get_repo_profile(repo: str) -> dict:
43
+ """Return one Repo Profile by `org/name` (e.g. astral-sh/uv)."""
44
+ p = profile_get(rr, repo)
45
+ if p is None:
46
+ return {"error": f"profile not found: {repo}"}
47
+ return p["frontmatter"]
48
+
49
+ @mcp.tool()
50
+ def list_open_prs() -> list:
51
+ """List every PR Case Study with final_status=open."""
52
+ out = []
53
+ for c in iter_case_studies(rr):
54
+ fm = c["frontmatter"]
55
+ if fm.get("final_status") == "open":
56
+ out.append({
57
+ "repo": fm.get("repo"),
58
+ "pr_number": fm.get("pr_number"),
59
+ "pr_url": fm.get("pr_url"),
60
+ "folder": c["folder"],
61
+ "schema_version": fm.get("schema_version", "legacy v0.1"),
62
+ "verified_at": fm.get("verified_at"),
63
+ })
64
+ return out
65
+
66
+ @mcp.tool()
67
+ def get_case_study(repo: str, pr_number: int) -> dict:
68
+ """Return one PR Case Study by `org/name` + `pr_number`."""
69
+ for c in iter_case_studies(rr):
70
+ fm = c["frontmatter"]
71
+ if (
72
+ fm.get("repo", "").strip("/").lower() == repo.strip("/").lower()
73
+ and str(fm.get("pr_number")) == str(pr_number)
74
+ ):
75
+ return {
76
+ "frontmatter": fm,
77
+ "body": c["body"],
78
+ "path": c["path"],
79
+ }
80
+ return {"error": f"case study not found: {repo}#{pr_number}"}
81
+
82
+ @mcp.tool()
83
+ def schema_info() -> dict:
84
+ """Return supported schema versions + enum values."""
85
+ return schema_info()
86
+
87
+ return mcp
88
+
89
+
90
+ def serve(repo_root: Path | None = None) -> int:
91
+ """Run stdio MCP server. Blocks until the host disconnects.
92
+
93
+ Args:
94
+ repo_root: Override the knowledge base location. If None, uses
95
+ the auto-detected default (parents[3] of this file).
96
+ """
97
+ mcp = _load_tools(repo_root=repo_root)
98
+ mcp.run(transport="stdio")
99
+ return 0
100
+
101
+
102
+ if __name__ == "__main__":
103
+ sys.exit(serve())
@@ -0,0 +1,225 @@
1
+ """Markdown frontmatter parsing — pure-stdlib YAML-subset.
2
+
3
+ Goals:
4
+ - Zero hard deps (no PyYAML) — package works after `pip install prgenius-core`
5
+ - Just enough fidelity for OKF v0.1 / rounds v0.5.0 / v0.7.0 frontmatter
6
+ - Honest: returns the AST we can parse; preserves raw text for the rest
7
+
8
+ Strategy: 2-pass indent-stack parser.
9
+ - Pass 1: collect (line, indent, raw_line)
10
+ - Pass 2: walk lines; push/pop stack based on indent depth
11
+
12
+ Limitations:
13
+ - top-level key: value (string/int/bool/null/list-inline)
14
+ - 2-space indent for nested mapping
15
+ - `- item` lists (with optional inline key: value on first line)
16
+ """
17
+ from __future__ import annotations
18
+ import re
19
+ from pathlib import Path
20
+ from typing import Iterator
21
+
22
+
23
+ _FM_RE = re.compile(r"^---\n(.*?)\n---", re.DOTALL)
24
+
25
+
26
+ def load(path: str | Path) -> dict:
27
+ """Load a markdown file: return {frontmatter, body, path}."""
28
+ p = Path(path)
29
+ text = p.read_text(encoding="utf-8")
30
+ fm_match = _FM_RE.match(text)
31
+ if not fm_match:
32
+ return {"frontmatter": {}, "body": text, "path": str(p)}
33
+ fm_text = fm_match.group(1)
34
+ body = text[fm_match.end():].lstrip("\n")
35
+ return {
36
+ "frontmatter": parse_frontmatter(fm_text),
37
+ "body": body,
38
+ "path": str(p),
39
+ }
40
+
41
+
42
+ def _unquote(s: str) -> str:
43
+ s = s.strip()
44
+ if len(s) >= 2 and s[0] == s[-1] and s[0] in ('"', "'"):
45
+ return s[1:-1]
46
+ return s
47
+
48
+
49
+ def _coerce(v: str):
50
+ """Coerce a scalar string to a Python value."""
51
+ if v == "" or v in ("null", "~"):
52
+ return None
53
+ if v == "true":
54
+ return True
55
+ if v == "false":
56
+ return False
57
+ if v.startswith("[") and v.endswith("]"):
58
+ inner = v[1:-1].strip()
59
+ if not inner:
60
+ return []
61
+ return [_coerce(x.strip()) for x in inner.split(",")]
62
+ if re.match(r"^-?\d+$", v):
63
+ return int(v)
64
+ if re.match(r"^-?\d+\.\d+$", v):
65
+ return float(v)
66
+ if len(v) >= 2 and v[0] == v[-1] and v[0] in ('"', "'"):
67
+ return v[1:-1]
68
+ return v
69
+
70
+
71
+ def _last_list_key(d: dict):
72
+ for k in reversed(d.keys()):
73
+ if isinstance(d[k], list):
74
+ return k
75
+ return None
76
+
77
+
78
+ def _normalize_lists(node):
79
+ """Recursively replace `{'_': [items]}` or `{'_items': [items]}` (parser quirk)
80
+ with the bare list.
81
+ """
82
+ if isinstance(node, dict):
83
+ keys = list(node.keys())
84
+ if (
85
+ len(keys) == 1
86
+ and keys[0] in ("_", "_items")
87
+ and isinstance(node[keys[0]], list)
88
+ ):
89
+ return [_normalize_lists(x) for x in node[keys[0]]]
90
+ return {k: _normalize_lists(v) for k, v in node.items()}
91
+ if isinstance(node, list):
92
+ return [_normalize_lists(x) for x in node]
93
+ return node
94
+
95
+
96
+ def parse_frontmatter(text: str) -> dict:
97
+ """Parse a YAML-subset frontmatter block.
98
+
99
+ Strategy: walk lines, push/pop an indent-aware stack. Lists get attached
100
+ to the most recent dict-key (the "current list"). After the walk we
101
+ normalize any `{'_': [...]}` quirk wrappers to plain lists.
102
+ """
103
+ tokens = []
104
+ for raw in text.splitlines():
105
+ if not raw.strip():
106
+ continue
107
+ if raw.lstrip().startswith("#"):
108
+ continue
109
+ indent = len(raw) - len(raw.lstrip(" "))
110
+ tokens.append((indent, raw.strip()))
111
+
112
+ root: dict = {}
113
+ # stack of (indent_level, container); root container indent = -1
114
+ stack = [(-1, root)]
115
+
116
+ for indent, line in tokens:
117
+ # pop until stack top has smaller indent than current
118
+ while len(stack) > 1 and stack[-1][0] >= indent:
119
+ stack.pop()
120
+ parent_indent, parent = stack[-1]
121
+
122
+ if line.startswith("- "):
123
+ content = line[2:].strip()
124
+ if not isinstance(parent, dict):
125
+ # shouldn't happen in our schema
126
+ continue
127
+ last_key = _last_list_key(parent)
128
+ if last_key is None:
129
+ parent["_items"] = []
130
+ current_list = parent["_items"]
131
+ last_key = "_items"
132
+ else:
133
+ current_list = parent[last_key]
134
+
135
+ if ":" in content and not content.startswith(('"', "'")):
136
+ k, _, v = content.partition(":")
137
+ k = _unquote(k.strip())
138
+ v = v.strip()
139
+ item: dict = {k: _coerce(v)}
140
+ current_list.append(item)
141
+ stack.append((indent, item))
142
+ else:
143
+ current_list.append(_coerce(content))
144
+ continue
145
+
146
+ if ":" not in line:
147
+ continue
148
+ k, _, v = line.partition(":")
149
+ k = _unquote(k.strip())
150
+ v = v.strip()
151
+
152
+ if not isinstance(parent, dict):
153
+ continue
154
+
155
+ if v == "":
156
+ new = {}
157
+ parent[k] = new
158
+ stack.append((indent, new))
159
+ else:
160
+ parent[k] = _coerce(v)
161
+
162
+ return _normalize_lists(root)
163
+
164
+
165
+ # ---------- higher-level iterators ----------
166
+
167
+ def iter_profiles(repo_root: str | Path) -> Iterator[dict]:
168
+ """Yield each Repo Profile dict under `<repo_root>/<folder>/index.md`."""
169
+ root = Path(repo_root)
170
+ skip = {
171
+ "anti-patterns", "misakanet-50", ".github", "docs",
172
+ "scripts", "prgenius", "__pycache__", ".git",
173
+ "federation.yaml",
174
+ }
175
+ for sub in sorted(root.iterdir()):
176
+ if not sub.is_dir() or sub.name in skip or sub.name.startswith("."):
177
+ continue
178
+ idx = sub / "index.md"
179
+ if not idx.exists():
180
+ continue
181
+ loaded = load(idx)
182
+ if loaded["frontmatter"].get("type") == "Repo Profile":
183
+ loaded["folder"] = sub.name
184
+ yield loaded
185
+
186
+
187
+ def iter_case_studies(repo_root: str | Path) -> Iterator[dict]:
188
+ """Yield each PR Case Study dict under repo root."""
189
+ root = Path(repo_root)
190
+ for path in sorted(root.rglob("pr-*.md")):
191
+ try:
192
+ loaded = load(path)
193
+ except Exception:
194
+ continue
195
+ if loaded["frontmatter"].get("type") == "PR Case Study":
196
+ loaded["folder"] = path.parent.name
197
+ loaded["pr_file"] = path.name
198
+ yield loaded
199
+
200
+
201
+ def profile_get(repo_root: str | Path, repo: str) -> dict | None:
202
+ """Look up a Repo Profile by `org/name`. None if missing."""
203
+ target = repo.strip("/").lower()
204
+ target_folder = target.replace("/", "-")
205
+ for profile in iter_profiles(repo_root):
206
+ if profile["folder"].lower() == target_folder:
207
+ return profile
208
+ if profile["frontmatter"].get("repo", "").strip("/").lower() == target:
209
+ return profile
210
+ return None
211
+
212
+
213
+ def schema_info() -> dict:
214
+ return {
215
+ "schema_versions": ["rounds v0.5.0", "rounds v0.7.0 (BC over v0.5.0)"],
216
+ "delta_kinds": ["code_change", "no_code_change", "unknown"],
217
+ "close_decision_status": ["pending", "close", "keep_open", "merged", "superseded"],
218
+ "evidence_fields_round_level": ["verified_at", "evidence_urls", "confidence"],
219
+ "evidence_fields_case_level": ["verified_at", "evidence_urls", "confidence"],
220
+ "confidence_values": ["high", "medium", "low"],
221
+ "action_enum": [
222
+ "open", "amend", "bot_review", "human_review",
223
+ "check_in", "bump", "close", "merge", "decision",
224
+ ],
225
+ }
File without changes
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: prgenius-core
3
+ Version: 0.7.8
4
+ Summary: Evidence-backed lookup for big-repo PR contributions. Local-only, zero-runtime, stdlib-first. Renamed from `prgenius` to avoid PyPI name collision with an unrelated 2024 GPT-3 PR-description tool. Distribution renamed again in v0.7.8: `prgenius-kb` → `prgenius-core` (aligned with `misakanet-core` convention).
5
+ Author: pr-genius contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/zsxh1990/pr-genius
8
+ Project-URL: Issues, https://github.com/zsxh1990/pr-genius/issues
9
+ Keywords: okf,pr-knowledge,agent-readable,contribution-intelligence,evidence
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Software Development :: Documentation
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ Provides-Extra: mcp
21
+ Requires-Dist: mcp>=1.0; extra == "mcp"
22
+ Provides-Extra: dev
23
+ Requires-Dist: build; extra == "dev"
24
+ Requires-Dist: twine; extra == "dev"
25
+ Requires-Dist: pytest; extra == "dev"
26
+
27
+ ---
28
+ type: Schema Reference
29
+ title: prgenius
30
+ description: Evidence-backed lookup library for big-repo PR contributions — stdlib-first CLI + stdio MCP shell.
31
+ version: 0.7.0
32
+ created: 2026-07-04
33
+ updated: 2026-07-05
34
+ author: zsxh1990
35
+ ---
36
+
37
+ # prgenius
38
+
39
+ **Evidence-backed lookup for big-repo PR contributions. Local-only. Stdlib-first.**
40
+
41
+ A pure-Python (zero hard deps) library + CLI that reads the markdown knowledge
42
+ base in this repo and exposes it through structured tool calls.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install prgenius-core
48
+ ```
49
+
50
+ Or run from a checkout:
51
+
52
+ ```bash
53
+ cd prgenius
54
+ PYTHONPATH=src python3 -m prgenius --version
55
+ ```
56
+
57
+ ## Important: PyPI package is the *interface*, not the data
58
+
59
+ The PyPI wheel ships only the Python code (`prgenius/` package).
60
+ **The knowledge base (profile markdown, case studies, OKF schemas) lives
61
+ in the GitHub repo** at <https://github.com/zsxh1990/pr-genius> and is
62
+ **not** bundled into the wheel. To use `prgenius-core` after `pip install`,
63
+ point it at a checkout of the knowledge base:
64
+
65
+ ```bash
66
+ git clone https://github.com/zsxh1990/pr-genius
67
+ prgenius-core --repo-root ./pr-genius profile get astral-sh/uv
68
+ ```
69
+
70
+ If you cloned a specific tag/commit, the package's `__version__` should
71
+ match (e.g. v0.7.7 ↔ `prgenius-core==0.7.7`).
72
+
73
+ ### Optional: MCP server
74
+
75
+ The MCP entry point (`prgenius-core mcp serve`) requires the `mcp` package:
76
+
77
+ ```bash
78
+ pip install "prgenius-core[mcp]"
79
+ ```
80
+
81
+ ## Quick Start
82
+
83
+ ```bash
84
+ # 1) Schema reference
85
+ PYTHONPATH=src python3 -m prgenius schema info
86
+
87
+ # 2) Look up one repo
88
+ PYTHONPATH=src python3 -m prgenius profile get astral-sh/uv
89
+
90
+ # 3) List currently-open case studies
91
+ PYTHONPATH=src python3 -m prgenius case list --status=open
92
+
93
+ # 4) NDJSON dump of every case study (for benchmarks / agent context)
94
+ PYTHONPATH=src python3 -m prgenius dump > cases.ndjson
95
+
96
+ # 5) Run as stdio MCP server (for Cursor/Cline/Claude Code)
97
+ PYTHONPATH=src python3 -m prgenius mcp serve
98
+ ```
99
+
100
+ ## Programmatic use
101
+
102
+ ```python
103
+ from prgenius.parser import profile_get, iter_case_studies, schema_info
104
+
105
+ prof = profile_get("path/to/repo_root", "astral-sh/uv")
106
+ print(prof["frontmatter"]["title"])
107
+
108
+ for case in iter_case_studies("path/to/repo_root"):
109
+ fm = case["frontmatter"]
110
+ if fm.get("final_status") == "open":
111
+ print(fm.get("pr_number"), fm.get("repo"))
112
+ ```
113
+
114
+ ## What's exposed
115
+
116
+ | Tool | Description |
117
+ |---|---|
118
+ | `prgenius profile get <org/name>` | One Repo Profile (frontmatter + first lines) |
119
+ | `prgenius case list [--status=...]` | All PR Case Study rows |
120
+ | `prgenius schema info` | Supported schema versions + enums |
121
+ | `prgenius dump` | NDJSON dump (one case per line) |
122
+ | `prgenius mcp serve` | Stdio MCP server (4 tools) |
123
+
124
+ ## MCP surface (when `mcp` is installed)
125
+
126
+ The MCP shell exposes 4 tools to local agents:
127
+
128
+ - `get_repo_profile(repo)` — one Profile dict
129
+ - `list_open_prs()` — currently-open PRs
130
+ - `get_case_study(repo, pr_number)` — one Case Study
131
+ - `schema_info()` — schema versions + enums
132
+
133
+ No network, no auth, no rate-limiting. Agent calls go through stdio only.
134
+
135
+ ### `--repo-root` flag
136
+
137
+ The MCP server defaults to a path computed from its install location
138
+ (`<package>/../..` × 3 to reach the knowledge base). When that path is
139
+ wrong (editable install, venv, worktree, fork), pass `--repo-root`:
140
+
141
+ ```bash
142
+ python3 -m prgenius --repo-root /path/to/big-repo-pr-knowledge mcp serve
143
+ ```
144
+
145
+ ### Wiring into Cursor / Claude Code / Cline
146
+
147
+ These editors all read MCP server config from JSON. Point `command` at
148
+ `python3 -m prgenius` and `args` at `mcp serve` (add `--repo-root` if the
149
+ auto-detected path doesn't match your checkout). No `env` keys, no auth.
150
+
151
+ **Claude Code** (`~/.claude/mcp.json` or project-local `.mcp.json`):
152
+
153
+ ```json
154
+ {
155
+ "mcpServers": {
156
+ "pr-genius": {
157
+ "command": "python3",
158
+ "args": ["-m", "prgenius", "--repo-root", "/abs/path/to/big-repo-pr-knowledge", "mcp", "serve"]
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ **Cursor** (`~/.cursor/mcp.json`):
165
+
166
+ ```json
167
+ {
168
+ "mcpServers": {
169
+ "pr-genius": {
170
+ "command": "python3 -m prgenius",
171
+ "args": ["--repo-root", "/abs/path/to/big-repo-pr-knowledge", "mcp", "serve"]
172
+ }
173
+ }
174
+ }
175
+ ```
176
+
177
+ **Cline** (VS Code `cline_mcp_settings.json`):
178
+
179
+ ```json
180
+ {
181
+ "mcpServers": {
182
+ "pr-genius": {
183
+ "command": "python3",
184
+ "args": ["-m", "prgenius", "--repo-root", "/abs/path/to/big-repo-pr-knowledge", "mcp", "serve"],
185
+ "disabled": false
186
+ }
187
+ }
188
+ }
189
+ ```
190
+
191
+ Replace `/abs/path/to/big-repo-pr-knowledge` with the actual checkout.
192
+ Omit `--repo-root` only if you installed prgenius from this exact
193
+ checkout (then the default path resolves correctly).
194
+
195
+ ## Schema we honor
196
+
197
+ - **rounds v0.5.0** — `action` enum + `delta` object + case-level `close_decision`
198
+ - **rounds v0.7.0** — adds optional `verified_at`, `evidence_urls`, `confidence`
199
+ to round + case level (BC over v0.5.0)
200
+
201
+ See [../ROUNDS_SCHEMA.md](../ROUNDS_SCHEMA.md) for the canonical schema.
202
+
203
+ ## Why this exists
204
+
205
+ The repo is meant to be human-readable (`<org>-<repo>/index.md`) AND
206
+ machine-readable via this package. People get the narrative; agents get
207
+ structured tool calls. Both views share one source of truth.
208
+
209
+ ## License
210
+
211
+ MIT (same as parent repo).
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/prgenius/__init__.py
4
+ src/prgenius/__main__.py
5
+ src/prgenius/cli.py
6
+ src/prgenius/mcp.py
7
+ src/prgenius/parser.py
8
+ src/prgenius/py.typed
9
+ src/prgenius_core.egg-info/PKG-INFO
10
+ src/prgenius_core.egg-info/SOURCES.txt
11
+ src/prgenius_core.egg-info/dependency_links.txt
12
+ src/prgenius_core.egg-info/entry_points.txt
13
+ src/prgenius_core.egg-info/requires.txt
14
+ src/prgenius_core.egg-info/top_level.txt
15
+ tests/test_parser_smoke.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ prgenius-core = prgenius.cli:main
@@ -0,0 +1,8 @@
1
+
2
+ [dev]
3
+ build
4
+ twine
5
+ pytest
6
+
7
+ [mcp]
8
+ mcp>=1.0
@@ -0,0 +1,163 @@
1
+ """Smoke tests for prgenius parser + CLI.
2
+
3
+ These tests deliberately avoid framework/fixture infrastructure
4
+ (per ponytail "lazy code ≠ no check" rule). Each test creates
5
+ minimal temp inputs and asserts the public API behavior.
6
+
7
+ Run from repo root:
8
+
9
+ cd prgenius
10
+ PYTHONPATH=src python3 -m pytest tests/ -v
11
+ # or
12
+ PYTHONPATH=src python3 tests/test_parser_smoke.py
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import subprocess
18
+ import sys
19
+ import tempfile
20
+ import unittest
21
+ from pathlib import Path
22
+
23
+ # Allow running without pytest installed (stdlib unittest)
24
+ try:
25
+ import pytest # noqa: F401
26
+ HAS_PYTEST = True
27
+ except ImportError:
28
+ HAS_PYTEST = False
29
+
30
+ from prgenius.parser import (
31
+ iter_profiles,
32
+ iter_case_studies,
33
+ load,
34
+ parse_frontmatter,
35
+ profile_get,
36
+ schema_info,
37
+ )
38
+ from prgenius import __version__
39
+
40
+
41
+ class TestVersion(unittest.TestCase):
42
+ def test_version_is_string(self):
43
+ self.assertIsInstance(__version__, str)
44
+ # semver-ish: X.Y.Z
45
+ parts = __version__.split(".")
46
+ self.assertEqual(len(parts), 3, f"version not semver: {__version__}")
47
+ for p in parts:
48
+ self.assertTrue(p.isdigit(), f"non-numeric version part: {p}")
49
+
50
+
51
+ class TestParseFrontmatter(unittest.TestCase):
52
+ def test_basic_yaml(self):
53
+ text = """---
54
+ type: Profile
55
+ title: Test
56
+ tags: [a, b]
57
+ ---
58
+ # Body here
59
+ """
60
+ fm = parse_frontmatter(text)
61
+ self.assertEqual(fm.get("type"), "Profile")
62
+ self.assertEqual(fm.get("title"), "Test")
63
+ # tags normalized to list
64
+ self.assertEqual(fm.get("tags"), ["a", "b"])
65
+
66
+ def test_empty_frontmatter(self):
67
+ text = "# Body only, no frontmatter\n"
68
+ fm = parse_frontmatter(text)
69
+ # Should not raise — returns empty dict or default
70
+ self.assertIsInstance(fm, dict)
71
+
72
+
73
+ class TestLoadAndIter(unittest.TestCase):
74
+ def setUp(self):
75
+ self.tmp = tempfile.mkdtemp()
76
+ self.root = Path(self.tmp)
77
+ # Create one profile + one case study
78
+ (self.root / "fake-org-fake-repo").mkdir()
79
+ (self.root / "fake-org-fake-repo" / "index.md").write_text(
80
+ "---\n"
81
+ "type: Repo Profile\n"
82
+ "title: fake-org/fake-repo\n"
83
+ "agent_guidelines:\n"
84
+ " default_branch: main\n"
85
+ "---\n"
86
+ "# Profile body\n",
87
+ encoding="utf-8",
88
+ )
89
+ (self.root / "fake-org-fake-repo" / "pr-1-test.md").write_text(
90
+ "---\n"
91
+ "type: PR Case Study\n"
92
+ "title: Test case\n"
93
+ "rounds:\n"
94
+ " - action: open\n"
95
+ " at: 2026-01-01\n"
96
+ "---\n"
97
+ "# Case body\n",
98
+ encoding="utf-8",
99
+ )
100
+
101
+ def test_load_single_file(self):
102
+ profile_md = self.root / "fake-org-fake-repo" / "index.md"
103
+ result = load(profile_md)
104
+ self.assertEqual(result["frontmatter"]["title"], "fake-org/fake-repo")
105
+ self.assertIn("Profile body", result["body"])
106
+
107
+ def test_iter_profiles_finds_one(self):
108
+ profiles = list(iter_profiles(self.root))
109
+ self.assertEqual(len(profiles), 1)
110
+ self.assertEqual(profiles[0]["frontmatter"]["type"], "Repo Profile")
111
+
112
+ def test_iter_case_studies_finds_one(self):
113
+ cases = list(iter_case_studies(self.root))
114
+ self.assertEqual(len(cases), 1)
115
+ self.assertEqual(cases[0]["frontmatter"]["type"], "PR Case Study")
116
+
117
+ def test_profile_get_hit(self):
118
+ prof = profile_get(self.root, "fake-org/fake-repo")
119
+ self.assertIsNotNone(prof)
120
+ self.assertEqual(prof["frontmatter"]["title"], "fake-org/fake-repo")
121
+
122
+ def test_profile_get_miss(self):
123
+ prof = profile_get(self.root, "nonexistent/repo")
124
+ self.assertIsNone(prof)
125
+
126
+
127
+ class TestSchemaInfo(unittest.TestCase):
128
+ def test_returns_dict(self):
129
+ info = schema_info()
130
+ self.assertIsInstance(info, dict)
131
+ # Should advertise at least one OKF version
132
+ self.assertTrue(any("okf" in str(k).lower() for k in info.keys())
133
+ or "version" in info
134
+ or len(info) >= 1)
135
+
136
+
137
+ class TestCliSmoke(unittest.TestCase):
138
+ """Verify the CLI entry point actually launches."""
139
+
140
+ def test_cli_version(self):
141
+ repo_root = Path(__file__).resolve().parents[2] # up to pr-genius/
142
+ env_python = sys.executable
143
+ # Use `python -m prgenius` with PYTHONPATH pointing at src/
144
+ env_add = str(repo_root / "prgenius" / "src")
145
+ import os
146
+ env = os.environ.copy()
147
+ env["PYTHONPATH"] = (
148
+ env_add + os.pathsep + env.get("PYTHONPATH", "")
149
+ )
150
+ result = subprocess.run(
151
+ [env_python, "-m", "prgenius", "--version"],
152
+ capture_output=True, text=True, env=env, timeout=15,
153
+ )
154
+ self.assertEqual(result.returncode, 0,
155
+ f"stderr={result.stderr!r}")
156
+ self.assertIn(__version__, result.stdout)
157
+
158
+
159
+ if __name__ == "__main__":
160
+ if HAS_PYTEST:
161
+ sys.exit(pytest.main([__file__, "-v"])) # noqa
162
+ else:
163
+ unittest.main(verbosity=2)