181 lines
8.0 KiB
Python
181 lines
8.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Create an editable source-file evidence list before code extraction."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from common import COPYRIGHT_CODE_EXTS, FRONTEND_EXTS, ensure_dir, is_known_config_file, iter_project_files, read_json, rel, write_json
|
|
from extract_code_material import LINES_PER_PAGE, SPLIT_THRESHOLD_PAGES, category_weight, material_code_lines, should_skip_file
|
|
|
|
|
|
DEFAULT_MAX_FILES = 0
|
|
|
|
|
|
def evidence_for(path: Path, project: Path) -> str:
|
|
priority, _ = category_weight(path, project)
|
|
if priority == 0:
|
|
return "入口文件证据"
|
|
if priority == 10:
|
|
return "路由文件证据"
|
|
if priority == 20:
|
|
return "页面文件证据"
|
|
if priority == 30:
|
|
return "数据交互文件证据"
|
|
if priority == 40:
|
|
return "状态或数据文件证据"
|
|
if priority == 50:
|
|
return "页面组成文件证据"
|
|
if priority == 60:
|
|
return "通用能力文件证据"
|
|
if priority == 90:
|
|
return "样式文件证据"
|
|
if path.suffix.lower() not in FRONTEND_EXTS:
|
|
return "补充源码证据"
|
|
return "普通源码文件"
|
|
|
|
|
|
def build_candidates(project: Path) -> list[dict[str, Any]]:
|
|
files = [p for p in iter_project_files(project, COPYRIGHT_CODE_EXTS) if not should_skip_file(p) and not is_known_config_file(p)]
|
|
files.sort(key=lambda p: category_weight(p, project))
|
|
candidates: list[dict[str, Any]] = []
|
|
for path in files:
|
|
try:
|
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
line_count = len(text.splitlines())
|
|
material_line_count = len(material_code_lines(text))
|
|
except Exception:
|
|
line_count = 0
|
|
material_line_count = 0
|
|
priority, _ = category_weight(path, project)
|
|
candidates.append(
|
|
{
|
|
"path": rel(path, project),
|
|
"selected": False,
|
|
"line_count": line_count,
|
|
"material_line_count": material_line_count,
|
|
"priority": priority,
|
|
"selection_tier": "frontend" if path.suffix.lower() in FRONTEND_EXTS else "supplement",
|
|
"evidence": evidence_for(path, project),
|
|
"model_reason": "",
|
|
}
|
|
)
|
|
return candidates
|
|
|
|
|
|
def selected_line_estimate(item: dict[str, Any]) -> int:
|
|
total = int(item.get("material_line_count") or item.get("line_count") or 0)
|
|
return total + 1 if total > 0 else 0
|
|
|
|
|
|
def selection_stats(candidates: list[dict[str, Any]]) -> dict[str, int]:
|
|
selected_items = [item for item in candidates if item.get("selected")]
|
|
return {
|
|
"selected_count": len(selected_items),
|
|
"selected_lines": sum(selected_line_estimate(item) for item in selected_items),
|
|
}
|
|
|
|
|
|
def all_candidate_lines(candidates: list[dict[str, Any]]) -> int:
|
|
return sum(selected_line_estimate(item) for item in candidates)
|
|
|
|
|
|
def write_selection_md(path: Path, data: dict[str, Any]) -> None:
|
|
lines = [
|
|
"# 代码文件候选清单",
|
|
"",
|
|
"请先确认要抽取哪些源码文件,再运行代码材料抽取。",
|
|
"",
|
|
"本清单只列出候选源码证据,不默认决定抽取文件。",
|
|
"模型需要先理解项目业务、页面入口和源码职责,再填写 `selected/model_reason`。",
|
|
f"当前已选约 {data['estimated_selected_pages']} 页,全部候选源码约 {data['estimated_all_candidate_pages']} 页。",
|
|
"",
|
|
"```text",
|
|
"STOP_FOR_USER",
|
|
"NEXT_ACTION: 请由模型先填写 草稿/代码文件选择.json 的抽取选择和选择理由,再让用户确认;确认后运行 confirm_stage.py --stage code-selection。",
|
|
"```",
|
|
"",
|
|
"确认方式:",
|
|
"",
|
|
"1. 模型根据项目业务和代码入口选择最能体现软件功能的文件。",
|
|
"2. 把需要抽取的文件设为 `selected: true`,并填写 `model_reason`。",
|
|
"3. 代码材料按完整文件抽取并去除纯空行,不支持只抽取某个文件的中间行段。",
|
|
"4. 用户确认模型选择后,再记录 `code-selection` 门禁。",
|
|
"",
|
|
"## 默认选中文件",
|
|
"",
|
|
"| 文件 | 行数 | 模型选择理由 |",
|
|
"| --- | ---: | --- |",
|
|
]
|
|
for item in data["files"]:
|
|
if item.get("selected"):
|
|
lines.append(f"| `{item['path']}` | {item['line_count']} | {item.get('model_reason') or '待模型填写'} |")
|
|
|
|
lines.extend(["", "## 未选候选文件", "", "| 文件 | 行数 | 证据类型 |", "| --- | ---: | --- |"])
|
|
for item in data["files"]:
|
|
if not item.get("selected"):
|
|
lines.append(f"| `{item['path']}` | {item['line_count']} | {item['evidence']} |")
|
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--project", required=True)
|
|
parser.add_argument("--analysis", help="Optional project analysis JSON; retained for workflow traceability")
|
|
parser.add_argument("--out-dir", default="软件著作权申请资料/草稿")
|
|
parser.add_argument("--max-files", type=int, default=DEFAULT_MAX_FILES, help="Only limits candidate inventory size; does not auto-select files")
|
|
parser.add_argument("--target-pages", type=int, default=SPLIT_THRESHOLD_PAGES)
|
|
parser.add_argument("--lines-per-page", type=int, default=LINES_PER_PAGE)
|
|
args = parser.parse_args()
|
|
|
|
project = Path(args.project)
|
|
if not project.exists():
|
|
raise SystemExit(f"Project not found: {project}")
|
|
if args.analysis and not Path(args.analysis).exists():
|
|
raise SystemExit(f"Analysis JSON not found: {args.analysis}")
|
|
|
|
out_dir = ensure_dir(Path(args.out_dir))
|
|
candidates = build_candidates(project)
|
|
target_lines = max(1, args.target_pages) * max(1, args.lines_per_page)
|
|
if args.max_files:
|
|
candidates = candidates[: args.max_files]
|
|
stats = selection_stats(candidates)
|
|
candidate_lines = all_candidate_lines(candidates)
|
|
selected_pages = (stats["selected_lines"] + args.lines_per_page - 1) // args.lines_per_page if stats["selected_lines"] else 0
|
|
all_pages = (candidate_lines + args.lines_per_page - 1) // args.lines_per_page if candidate_lines else 0
|
|
data = {
|
|
"project_root": str(project.resolve()),
|
|
"selection_required": True,
|
|
"model_selection_required": True,
|
|
"confirmation_required": True,
|
|
"user_confirmed": False,
|
|
"target_pages": args.target_pages,
|
|
"lines_per_page": args.lines_per_page,
|
|
"target_lines": target_lines,
|
|
"estimated_selected_lines": stats["selected_lines"],
|
|
"estimated_selected_pages": selected_pages,
|
|
"estimated_all_candidate_lines": candidate_lines,
|
|
"estimated_all_candidate_pages": all_pages,
|
|
"supplement_rule": "模型优先选择能体现软件核心功能和真实运行逻辑的源码;不足60页时再从其他相关源码中补充;候选源码仍不足时才生成全部代码材料。",
|
|
"confirmation_stage": "code-selection",
|
|
"next_action": "请由模型填写 草稿/代码文件选择.json 的抽取选择和选择理由,再让用户确认;确认后运行 confirm_stage.py --stage code-selection。",
|
|
"instructions": "The script only inventories source files. The model must choose selected/model_reason before user confirmation. Selected files are extracted in full with blank-only lines removed.",
|
|
"files": candidates,
|
|
}
|
|
write_json(out_dir / "代码文件选择.json", data)
|
|
write_selection_md(out_dir / "代码文件候选清单.md", data)
|
|
selected_count = sum(1 for item in candidates if item.get("selected"))
|
|
print(f"OK code selection draft: {out_dir}")
|
|
print(f"Candidates: {len(candidates)}")
|
|
print(f"Model selected: {selected_count}")
|
|
print(f"Estimated selected pages: {selected_pages}")
|
|
print(f"Estimated all candidate pages: {all_pages}")
|
|
print("STOP_FOR_USER")
|
|
print(f"NEXT_ACTION: {data['next_action']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|