Skip to content

Commit 820ffae

Browse files
committed
Add project scanning and index generation
1 parent 380c149 commit 820ffae

6 files changed

Lines changed: 686 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ jobs:
2929
- name: Validate workflow templates
3030
run: python tools/validate_workflow.py --memory-dir memory
3131

32+
- name: Scan project
33+
run: python tools/scan_project.py --target . --output scan-summary.json
34+
35+
- name: Generate draft index
36+
run: python tools/init_index.py --target . --output CODEBASE_INDEX.generated.md
37+
3238
- name: Scaffold task
3339
run: >
3440
python tools/scaffold_task.py

README.en.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ Codex should operate like this:
147147

148148
`tools/` already includes CLI commands that enforce the workflow:
149149

150+
- `scan_project.py` — produce a structured JSON summary of a repository
151+
- `init_index.py` — generate a draft `CODEBASE_INDEX.md`
150152
- `bootstrap.py` — bootstrap the mandatory core into a new project
151153
- `scaffold_task.py` — create or replace `current-task.md`
152154
- `create_handoff.py` — append a structured handoff

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ python tools/bootstrap.py --target ..\my-project
155155

156156
В `tools/` уже есть CLI для принуждения к процессу:
157157

158+
- `scan_project.py` — собрать структурированный JSON summary проекта
159+
- `init_index.py` — сгенерировать черновик `CODEBASE_INDEX.md`
158160
- `bootstrap.py` — развернуть mandatory core в новый проект
159161
- `scaffold_task.py` — создать или обновить `current-task.md`
160162
- `create_handoff.py` — добавить structured handoff

tools/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@ python tools\validate_workflow.py
3131

3232
## Commands
3333

34+
### scan_project.py
35+
36+
Сканирует репозиторий и выводит структурированный JSON summary.
37+
38+
```powershell
39+
python tools/scan_project.py --target .
40+
```
41+
42+
### init_index.py
43+
44+
Генерирует черновик `CODEBASE_INDEX` на основе structural scan.
45+
46+
```powershell
47+
python tools/init_index.py --target . --output CODEBASE_INDEX.generated.md
48+
```
49+
3450
### bootstrap.py
3551

3652
Разворачивает mandatory core в новый проект.
@@ -103,3 +119,4 @@ python tools/acceptance_check.py --scope --behavior --verification --regression
103119
- Они не пытаются анализировать архитектуру за тебя.
104120
- Их цель: сделать нарушение процесса более заметным и дорогим.
105121
- `bootstrap.py` копирует только mandatory core, а не весь kit целиком.
122+
- `scan_project.py` и `init_index.py` строят черновик архитектурной карты, но не заменяют ручной review.

tools/init_index.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
from pathlib import Path
5+
6+
from common import configure_stdout, write_text
7+
from scan_project import build_summary
8+
9+
10+
def build_parser() -> argparse.ArgumentParser:
11+
parser = argparse.ArgumentParser(description="Generate a draft CODEBASE_INDEX.md from a structural project scan")
12+
parser.add_argument("--target", default=".", help="Target project directory")
13+
parser.add_argument(
14+
"--output",
15+
default="CODEBASE_INDEX.generated.md",
16+
help="Output file path for the generated index draft",
17+
)
18+
return parser
19+
20+
21+
def bullet_lines(items: list[str], placeholder: str) -> str:
22+
if not items:
23+
return f"- {placeholder}"
24+
return "\n".join(f"- {item}" for item in items)
25+
26+
27+
def render_commands(commands: list[dict[str, str]]) -> str:
28+
if not commands:
29+
return "# No commands detected automatically"
30+
lines = []
31+
for item in commands[:12]:
32+
lines.append(f"# {item['name']}")
33+
lines.append(item["command"])
34+
lines.append("")
35+
return "\n".join(lines).rstrip()
36+
37+
38+
def render_directories(entries: list[dict[str, str]]) -> str:
39+
if not entries:
40+
return "- No top-level directories detected"
41+
lines: list[str] = []
42+
for item in entries:
43+
lines.append(f"- `{item['path']}/`")
44+
lines.append(f" - {item['description']}")
45+
return "\n".join(lines)
46+
47+
48+
def render_entrypoints(entries: list[dict[str, object]]) -> str:
49+
if not entries:
50+
return "- No entrypoint candidates detected"
51+
lines: list[str] = []
52+
for item in entries[:8]:
53+
reasons = ", ".join(item.get("reasons", [])) or "needs review"
54+
lines.append(f"- {item['path']} ({reasons})")
55+
return "\n".join(lines)
56+
57+
58+
def render_area_hints(entries: list[dict[str, object]]) -> str:
59+
if not entries:
60+
return "- No area hints detected automatically"
61+
lines: list[str] = []
62+
for item in entries:
63+
matches = ", ".join(item.get("matches", []))
64+
lines.append(f"- {item['area']}: {matches}")
65+
return "\n".join(lines)
66+
67+
68+
def render(summary: dict[str, object]) -> str:
69+
languages = summary.get("languages", [])
70+
frameworks = summary.get("frameworks", [])
71+
important_files = summary.get("important_files", [])
72+
ci_files = summary.get("ci_files", [])
73+
tests = summary.get("tests", {})
74+
commands = summary.get("commands", [])
75+
76+
primary_language = summary.get("primary_language") or "Needs review"
77+
frameworks_text = ", ".join(frameworks) if frameworks else "Needs review"
78+
package_manager = summary.get("package_manager") or "Needs review"
79+
test_dirs = tests.get("directories", []) if isinstance(tests, dict) else []
80+
test_samples = tests.get("sample_files", []) if isinstance(tests, dict) else []
81+
test_count = tests.get("count", 0) if isinstance(tests, dict) else 0
82+
83+
return f"""# CODEBASE_INDEX.md
84+
85+
Generated draft. Review and refine before treating it as canonical.
86+
87+
## Project Summary
88+
89+
- Project name: {summary.get("project_name", "Needs review")}
90+
- Main purpose: Needs review
91+
- Primary language: {primary_language}
92+
- Frameworks: {frameworks_text}
93+
- Package manager: {package_manager}
94+
- Test runner: Needs review
95+
96+
## Entry Points
97+
98+
{render_entrypoints(summary.get("entrypoints", []))}
99+
100+
## Main Directories
101+
102+
{render_directories(summary.get("main_directories", []))}
103+
104+
## Important Files
105+
106+
{bullet_lines(important_files[:20], "No important config or manifest files detected")}
107+
108+
## CI Files
109+
110+
{bullet_lines(ci_files[:10], "No CI files detected")}
111+
112+
## Area Hints
113+
114+
{render_area_hints(summary.get("area_hints", []))}
115+
116+
## Test Map
117+
118+
- Test directories: {", ".join(test_dirs) if test_dirs else "Needs review"}
119+
- Detected test files: {test_count}
120+
- Sample test files:
121+
{bullet_lines(test_samples[:10], "No test files detected automatically")}
122+
123+
## Common Commands
124+
125+
```text
126+
{render_commands(commands)}
127+
```
128+
129+
## Notes For Codex
130+
131+
- This file was generated from structural scanning and heuristics.
132+
- Verify entry points, test runner, and business-purpose descriptions manually.
133+
- Update this file when architecture or repository structure changes.
134+
"""
135+
136+
137+
def main() -> int:
138+
configure_stdout()
139+
parser = build_parser()
140+
args = parser.parse_args()
141+
target = Path(args.target).resolve()
142+
summary = build_summary(target)
143+
output = Path(args.output)
144+
if not output.is_absolute():
145+
output = target / output
146+
write_text(output, render(summary))
147+
print(f"Generated {output}")
148+
return 0
149+
150+
151+
if __name__ == "__main__":
152+
raise SystemExit(main())
153+

0 commit comments

Comments
 (0)