Skip to content

Commit d70d56f

Browse files
committed
Add scan explanations and confidence output
1 parent 915eb5a commit d70d56f

10 files changed

Lines changed: 327 additions & 26 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ jobs:
4141
- name: Generate draft change areas
4242
run: python tools/init_change_areas.py --target . --output CHANGE_AREAS.generated.md
4343

44+
- name: Explain scan
45+
run: python tools/explain_scan.py --target . --format markdown --output SCAN_EXPLANATION.md
46+
4447
- name: Copy canonical index for refresh smoke test
4548
run: >
4649
python -c

README.en.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ Codex should operate like this:
152152
- `init_test_index.py` — generate a draft `TEST_INDEX.md`
153153
- `init_change_areas.py` — generate a draft `CHANGE_AREAS.md`
154154
- `refresh_index.py` — refresh machine-generated sections of an existing `CODEBASE_INDEX.md`
155+
- `explain_scan.py` — explain what the scanner found and why
155156
- `bootstrap.py` — bootstrap the mandatory core into a new project
156157
- `scaffold_task.py` — create or replace `current-task.md`
157158
- `create_handoff.py` — append a structured handoff

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ python tools/bootstrap.py --target ..\my-project
160160
- `init_test_index.py` — сгенерировать черновик `TEST_INDEX.md`
161161
- `init_change_areas.py` — сгенерировать черновик `CHANGE_AREAS.md`
162162
- `refresh_index.py` — обновить машинные секции существующего `CODEBASE_INDEX.md`
163+
- `explain_scan.py` — объяснить, что именно сканер нашел и почему
163164
- `bootstrap.py` — развернуть mandatory core в новый проект
164165
- `scaffold_task.py` — создать или обновить `current-task.md`
165166
- `create_handoff.py` — добавить structured handoff

tools/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ python tools\validate_workflow.py
3939
python tools/scan_project.py --target .
4040
```
4141

42+
Можно писать в файл:
43+
44+
```powershell
45+
python tools/scan_project.py --target . --output scan-summary.json
46+
```
47+
4248
### init_index.py
4349

4450
Генерирует черновик `CODEBASE_INDEX` на основе structural scan.
@@ -47,6 +53,12 @@ python tools/scan_project.py --target .
4753
python tools/init_index.py --target . --output CODEBASE_INDEX.generated.md
4854
```
4955

56+
JSON-режим в stdout:
57+
58+
```powershell
59+
python tools/init_index.py --target . --format json --output -
60+
```
61+
5062
### init_test_index.py
5163

5264
Генерирует черновик `TEST_INDEX` на основе scan summary и команд проекта.
@@ -55,6 +67,12 @@ python tools/init_index.py --target . --output CODEBASE_INDEX.generated.md
5567
python tools/init_test_index.py --target . --output TEST_INDEX.generated.md
5668
```
5769

70+
Markdown в stdout:
71+
72+
```powershell
73+
python tools/init_test_index.py --target . --output -
74+
```
75+
5876
### init_change_areas.py
5977

6078
Генерирует черновик `CHANGE_AREAS` на основе directory и entrypoint heuristics.
@@ -63,6 +81,12 @@ python tools/init_test_index.py --target . --output TEST_INDEX.generated.md
6381
python tools/init_change_areas.py --target . --output CHANGE_AREAS.generated.md
6482
```
6583

84+
JSON-режим:
85+
86+
```powershell
87+
python tools/init_change_areas.py --target . --format json --output -
88+
```
89+
6690
### refresh_index.py
6791

6892
Обновляет машинные секции существующего `CODEBASE_INDEX.md` без полного перетирания файла.
@@ -71,6 +95,20 @@ python tools/init_change_areas.py --target . --output CHANGE_AREAS.generated.md
7195
python tools/refresh_index.py --target . --index CODEBASE_INDEX.md
7296
```
7397

98+
### explain_scan.py
99+
100+
Кратко объясняет, что scanner обнаружил и почему.
101+
102+
```powershell
103+
python tools/explain_scan.py --target .
104+
```
105+
106+
Markdown-режим:
107+
108+
```powershell
109+
python tools/explain_scan.py --target . --format markdown
110+
```
111+
74112
### bootstrap.py
75113

76114
Разворачивает mandatory core в новый проект.
@@ -146,3 +184,4 @@ python tools/acceptance_check.py --scope --behavior --verification --regression
146184
- `scan_project.py` и `init_index.py` строят черновик архитектурной карты, но не заменяют ручной review.
147185
- `init_test_index.py` и `init_change_areas.py` дают черновики для test map и change areas, а не финальную архитектурную истину.
148186
- `refresh_index.py` обновляет только машинные секции и должен использоваться поверх уже просмотренного человеком индекса.
187+
- Генераторы поддерживают `--output -` для stdout и `--format json` там, где это имеет смысл.

tools/common.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4+
import json
45
import os
56
from pathlib import Path
67
import sys
@@ -90,3 +91,15 @@ def configure_stdout() -> None:
9091
if hasattr(stream, "reconfigure"):
9192
# Windows CI can default to cp1252 and crash on Cyrillic output.
9293
stream.reconfigure(encoding="utf-8", errors="backslashreplace")
94+
95+
96+
def emit_output(output: str, text: str) -> None:
97+
if output == "-":
98+
print(text)
99+
else:
100+
write_text(Path(output), text)
101+
102+
103+
def emit_json(output: str, payload: object) -> None:
104+
text = json.dumps(payload, ensure_ascii=False, indent=2)
105+
emit_output(output, text)

tools/explain_scan.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
from pathlib import Path
5+
6+
from common import configure_stdout, emit_json, emit_output
7+
from scan_project import build_summary
8+
9+
10+
def build_parser() -> argparse.ArgumentParser:
11+
parser = argparse.ArgumentParser(description="Explain what the repository scanner detected and why")
12+
parser.add_argument("--target", default=".", help="Target project directory")
13+
parser.add_argument("--output", default="-", help="Output path, or - for stdout")
14+
parser.add_argument("--format", choices=["text", "markdown", "json"], default="text", help="Output format")
15+
return parser
16+
17+
18+
def build_payload(summary: dict[str, object]) -> dict[str, object]:
19+
confidence = summary.get("confidence_notes", {})
20+
return {
21+
"project_name": summary.get("project_name"),
22+
"primary_language": {
23+
"value": summary.get("primary_language"),
24+
**confidence.get("primary_language", {}),
25+
},
26+
"entrypoints": summary.get("entrypoints", []),
27+
"tests": {
28+
**summary.get("tests", {}),
29+
**({"confidence": confidence.get("tests", {}).get("confidence"), "reason": confidence.get("tests", {}).get("reason")} if confidence.get("tests") else {}),
30+
},
31+
"commands": {
32+
"items": summary.get("commands", []),
33+
**({"confidence": confidence.get("commands", {}).get("confidence"), "reason": confidence.get("commands", {}).get("reason")} if confidence.get("commands") else {}),
34+
},
35+
"areas": {
36+
"items": summary.get("area_hints", []),
37+
**({"confidence": confidence.get("areas", {}).get("confidence"), "reason": confidence.get("areas", {}).get("reason")} if confidence.get("areas") else {}),
38+
},
39+
}
40+
41+
42+
def render_text(summary: dict[str, object]) -> str:
43+
confidence = summary.get("confidence_notes", {})
44+
entrypoints = summary.get("entrypoints", [])
45+
commands = summary.get("commands", [])
46+
area_hints = summary.get("area_hints", [])
47+
tests = summary.get("tests", {})
48+
49+
lines = [
50+
f"Project: {summary.get('project_name')}",
51+
f"Primary language: {summary.get('primary_language') or 'unknown'}",
52+
f" Confidence: {confidence.get('primary_language', {}).get('confidence', 'unknown')}",
53+
f" Reason: {confidence.get('primary_language', {}).get('reason', 'Needs review')}",
54+
"",
55+
"Entrypoints:",
56+
]
57+
if entrypoints:
58+
for item in entrypoints[:8]:
59+
reasons = ", ".join(item.get("reasons", [])) or "Needs review"
60+
lines.append(f"- {item['path']} [{item.get('confidence', 'unknown')}]")
61+
lines.append(f" Reasons: {reasons}")
62+
else:
63+
lines.append("- none detected")
64+
65+
lines.extend(
66+
[
67+
"",
68+
f"Tests: {tests.get('count', 0) if isinstance(tests, dict) else 0} detected",
69+
f" Confidence: {confidence.get('tests', {}).get('confidence', 'unknown')}",
70+
f" Reason: {confidence.get('tests', {}).get('reason', 'Needs review')}",
71+
"",
72+
f"Commands: {len(commands)} detected",
73+
f" Confidence: {confidence.get('commands', {}).get('confidence', 'unknown')}",
74+
f" Reason: {confidence.get('commands', {}).get('reason', 'Needs review')}",
75+
"",
76+
"Area hints:",
77+
]
78+
)
79+
if area_hints:
80+
for item in area_hints:
81+
lines.append(f"- {item['area']} [{item.get('confidence', 'unknown')}] -> {', '.join(item.get('matches', []))}")
82+
else:
83+
lines.append("- none detected")
84+
return "\n".join(lines)
85+
86+
87+
def render_markdown(summary: dict[str, object]) -> str:
88+
payload = build_payload(summary)
89+
entrypoints = payload["entrypoints"]
90+
tests = payload["tests"]
91+
commands = payload["commands"]
92+
areas = payload["areas"]
93+
94+
lines = [
95+
"# Scan Explanation",
96+
"",
97+
f"- Project: {payload['project_name']}",
98+
f"- Primary language: {payload['primary_language'].get('value') or 'unknown'}",
99+
f"- Language confidence: {payload['primary_language'].get('confidence', 'unknown')}",
100+
f"- Language reason: {payload['primary_language'].get('reason', 'Needs review')}",
101+
"",
102+
"## Entrypoints",
103+
]
104+
if entrypoints:
105+
for item in entrypoints[:8]:
106+
lines.append(f"- `{item['path']}`")
107+
lines.append(f" - confidence: {item.get('confidence', 'unknown')}")
108+
lines.append(f" - reasons: {', '.join(item.get('reasons', [])) or 'Needs review'}")
109+
else:
110+
lines.append("- none detected")
111+
112+
lines.extend(
113+
[
114+
"",
115+
"## Tests",
116+
f"- count: {tests.get('count', 0)}",
117+
f"- confidence: {tests.get('confidence', 'unknown')}",
118+
f"- reason: {tests.get('reason', 'Needs review')}",
119+
"",
120+
"## Commands",
121+
f"- count: {len(commands.get('items', []))}",
122+
f"- confidence: {commands.get('confidence', 'unknown')}",
123+
f"- reason: {commands.get('reason', 'Needs review')}",
124+
"",
125+
"## Areas",
126+
f"- confidence: {areas.get('confidence', 'unknown')}",
127+
f"- reason: {areas.get('reason', 'Needs review')}",
128+
]
129+
)
130+
if areas.get("items"):
131+
for item in areas["items"]:
132+
lines.append(f"- {item['area']}: {', '.join(item.get('matches', []))}")
133+
else:
134+
lines.append("- none detected")
135+
return "\n".join(lines)
136+
137+
138+
def main() -> int:
139+
configure_stdout()
140+
parser = build_parser()
141+
args = parser.parse_args()
142+
summary = build_summary(Path(args.target).resolve())
143+
output = args.output
144+
if args.format == "json":
145+
emit_json(output, build_payload(summary))
146+
elif args.format == "markdown":
147+
emit_output(output, render_markdown(summary))
148+
else:
149+
emit_output(output, render_text(summary))
150+
return 0
151+
152+
153+
if __name__ == "__main__":
154+
raise SystemExit(main())

tools/init_change_areas.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
import argparse
44
from pathlib import Path
55

6-
from common import configure_stdout, write_text
6+
from common import configure_stdout, emit_json, emit_output
77
from scan_project import build_change_area_summary, build_summary
88

99

1010
def build_parser() -> argparse.ArgumentParser:
1111
parser = argparse.ArgumentParser(description="Generate a draft CHANGE_AREAS.md from repository structure")
1212
parser.add_argument("--target", default=".", help="Target project directory")
1313
parser.add_argument("--output", default="CHANGE_AREAS.generated.md", help="Output file path")
14+
parser.add_argument("--format", choices=["markdown", "json"], default="markdown", help="Output format")
1415
return parser
1516

1617

@@ -21,13 +22,15 @@ def render_area(area: dict[str, object]) -> str:
2122
return f"""- {area['area']}
2223
- Основные директории: {directories}
2324
- Главные entrypoints: {entrypoints}
25+
- Confidence: {area.get('confidence', 'unknown')}
2426
- Основные риски: Needs review
2527
- Типичные тесты: {tests}"""
2628

2729

2830
def render(summary: dict[str, object]) -> str:
2931
areas = build_change_area_summary(summary)
3032
area_text = "\n\n".join(render_area(area) for area in areas) if areas else "- No change areas detected automatically"
33+
confidence_notes = summary.get("confidence_notes", {})
3134
return f"""# CHANGE_AREAS.md
3235
3336
Generated draft. Review and refine before using it for strict delegation.
@@ -38,6 +41,7 @@ def render(summary: dict[str, object]) -> str:
3841
3942
## Delegation Rule
4043
44+
- Confidence: {confidence_notes.get('areas', {}).get('confidence', 'unknown')} ({confidence_notes.get('areas', {}).get('reason', 'Needs review')})
4145
- Один агент должен владеть одной зоной записи.
4246
- Если задача затрагивает две зоны, критический путь лучше держать локально.
4347
- Если зона не определена, сначала отправляй `explorer`.
@@ -49,15 +53,25 @@ def main() -> int:
4953
parser = build_parser()
5054
args = parser.parse_args()
5155
target = Path(args.target).resolve()
52-
output = Path(args.output)
53-
if not output.is_absolute():
54-
output = target / output
5556
summary = build_summary(target)
56-
write_text(output, render(summary))
57-
print(f"Generated {output}")
57+
output = args.output
58+
if output != "-":
59+
output_path = Path(output)
60+
if not output_path.is_absolute():
61+
output_path = target / output_path
62+
output = str(output_path)
63+
payload = {
64+
"areas": build_change_area_summary(summary),
65+
"confidence_notes": summary.get("confidence_notes", {}).get("areas", {}),
66+
}
67+
if args.format == "json":
68+
emit_json(output, payload)
69+
else:
70+
emit_output(output, render(summary))
71+
if output != "-":
72+
print(f"Generated {output}")
5873
return 0
5974

6075

6176
if __name__ == "__main__":
6277
raise SystemExit(main())
63-

0 commit comments

Comments
 (0)