|
| 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