-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathproject_facts.py
More file actions
225 lines (177 loc) · 7.06 KB
/
project_facts.py
File metadata and controls
225 lines (177 loc) · 7.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
#!/usr/bin/env python3
"""Canonical public facts for Nexum Graph.
Reviewing this file:
- This is the source of truth for the README facts block and public repo stats.
- If the counting rules change, update the README block through this tool
rather than hand-editing the generated section.
- Social copy, release notes, and review comments should prefer values emitted
here over ad hoc manual counts.
"""
from __future__ import annotations
import argparse
import json
import re
from dataclasses import asdict, dataclass
from pathlib import Path
README_FACTS_START = "<!-- project-facts:start -->"
README_FACTS_END = "<!-- project-facts:end -->"
RUST_TEST_RE = re.compile(
r"#\[(?:tokio::)?test(?:\([^\]]*?\))?\]\s*"
r"(?:#\[[^\]]+\]\s*)*"
r"(?:pub\s+)?(?:async\s+)?fn\s+[A-Za-z_][A-Za-z0-9_]*",
re.MULTILINE,
)
PYTHON_TEST_RE = re.compile(
r"^\s*def\s+test_[A-Za-z_][A-Za-z0-9_]*\s*\(",
re.MULTILINE,
)
COMMAND_VARIANT_RE = re.compile(r"^([A-Z][A-Za-z0-9_]*)\b")
class ProjectFactsError(RuntimeError):
"""Raised when project facts cannot be generated or validated."""
@dataclass(frozen=True)
class ProjectFacts:
project_name: str
license_name: str
rust_crates: int
cli_commands: int
rust_tests: int
python_tests: int
total_tests: int
def repo_root() -> Path:
return Path(__file__).resolve().parent.parent
def license_path(root: Path | None = None) -> Path:
return (root or repo_root()) / "LICENSE"
def cli_path(root: Path | None = None) -> Path:
return (root or repo_root()) / "crates" / "nex-cli" / "src" / "cli.rs"
def readme_path(root: Path | None = None) -> Path:
return (root or repo_root()) / "README.md"
def detect_license(root: Path | None = None) -> str:
first_line = license_path(root).read_text(encoding="utf-8").splitlines()[0].strip()
if first_line == "MIT License":
return "MIT"
if "Apache License" in first_line:
return "Apache-2.0"
raise ProjectFactsError(f"unsupported license heading: {first_line!r}")
def count_workspace_crates(root: Path | None = None) -> int:
crates_dir = (root or repo_root()) / "crates"
return sum(1 for path in crates_dir.iterdir() if (path / "Cargo.toml").exists())
def count_cli_commands(root: Path | None = None) -> int:
lines = cli_path(root).read_text(encoding="utf-8").splitlines()
in_enum = False
depth = 0
count = 0
for line in lines:
if not in_enum:
if "pub enum Commands {" in line:
in_enum = True
depth = line.count("{") - line.count("}")
continue
if depth == 1:
stripped = line.strip()
match = COMMAND_VARIANT_RE.match(stripped)
if match:
name = match.group(1)
if stripped == f"{name}," or stripped.startswith(f"{name} {{"):
count += 1
depth += line.count("{") - line.count("}")
if depth <= 0:
break
if count == 0:
raise ProjectFactsError("failed to count CLI commands from crates/nex-cli/src/cli.rs")
return count
def count_rust_tests(root: Path | None = None) -> int:
total = 0
for path in ((root or repo_root()) / "crates").rglob("*.rs"):
total += len(RUST_TEST_RE.findall(path.read_text(encoding="utf-8")))
return total
def count_python_tests(root: Path | None = None) -> int:
total = 0
for path in ((root or repo_root()) / "tools").glob("test_*.py"):
total += len(PYTHON_TEST_RE.findall(path.read_text(encoding="utf-8")))
return total
def gather_project_facts(root: Path | None = None) -> ProjectFacts:
root = root or repo_root()
rust_tests = count_rust_tests(root)
python_tests = count_python_tests(root)
return ProjectFacts(
project_name="Nexum Graph",
license_name=detect_license(root),
rust_crates=count_workspace_crates(root),
cli_commands=count_cli_commands(root),
rust_tests=rust_tests,
python_tests=python_tests,
total_tests=rust_tests + python_tests,
)
def render_readme_facts(facts: ProjectFacts) -> str:
return "\n".join(
[
README_FACTS_START,
"<!-- Generated by tools/project_facts.py. Do not edit this block by hand. -->",
f"- {facts.rust_crates} Rust crates in one workspace",
f"- {facts.cli_commands} CLI commands",
"- Guided first run via `nex start`",
"- TypeScript, Python, and Rust semantic extraction",
"- Semantic diff, coordination, validation, event log, HTTP server, and LSP integration",
(
f"- {facts.total_tests} source-defined automated tests "
f"({facts.rust_tests} Rust, {facts.python_tests} Python)"
),
f"- {facts.license_name} licensed",
README_FACTS_END,
]
)
def replace_readme_facts(readme_text: str, facts_block: str) -> str:
pattern = re.compile(
rf"{re.escape(README_FACTS_START)}.*?{re.escape(README_FACTS_END)}",
re.DOTALL,
)
if not pattern.search(readme_text):
raise ProjectFactsError("README facts block markers are missing")
return pattern.sub(facts_block, readme_text, count=1)
def sync_readme(root: Path | None = None) -> Path:
root = root or repo_root()
readme = readme_path(root)
updated = replace_readme_facts(
readme.read_text(encoding="utf-8"),
render_readme_facts(gather_project_facts(root)),
)
readme.write_text(updated, encoding="utf-8")
return readme
def check_readme(root: Path | None = None) -> None:
root = root or repo_root()
readme = readme_path(root)
expected = replace_readme_facts(
readme.read_text(encoding="utf-8"),
render_readme_facts(gather_project_facts(root)),
)
actual = readme.read_text(encoding="utf-8")
if actual != expected:
raise ProjectFactsError(
"README facts block is out of date; run `python tools/project_facts.py sync-readme`"
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate canonical Nexum Graph public facts.")
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("print-json", help="Print facts as JSON.")
subparsers.add_parser("print-markdown", help="Print the README facts block.")
subparsers.add_parser("sync-readme", help="Rewrite the README facts block.")
subparsers.add_parser("check-readme", help="Fail if the README facts block is stale.")
return parser.parse_args()
def main() -> int:
args = parse_args()
facts = gather_project_facts()
if args.command == "print-json":
print(json.dumps(asdict(facts), indent=2))
return 0
if args.command == "print-markdown":
print(render_readme_facts(facts))
return 0
if args.command == "sync-readme":
sync_readme()
return 0
if args.command == "check-readme":
check_readme()
return 0
raise ProjectFactsError(f"unsupported command: {args.command}")
if __name__ == "__main__":
raise SystemExit(main())