#!/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()