#!/usr/bin/env python3 """Collect project evidence and write a model-authored business context.""" from __future__ import annotations import argparse import json import re from pathlib import Path from typing import Any from common import ensure_dir, iter_project_files, read_json, read_text, rel, write_json DOC_EXTS = {".md", ".txt", ".rst", ".adoc"} MAX_DOC_CHARS = 80_000 MAX_DOCS = 40 def normalize_space(text: str) -> str: return re.sub(r"\s+", " ", text).strip() def strip_md(text: str) -> str: text = re.sub(r"`{3}.*?`{3}", " ", text, flags=re.S) text = re.sub(r"\[(.*?)\]\(.*?\)", r"\1", text) text = re.sub(r"[>#*_`|]", " ", text) return normalize_space(text) def skip_doc(path: Path, project: Path) -> bool: r = rel(path, project).lower() skip_parts = ( "node_modules", ".git/", "dist/", "build/", ".next/", "coverage/", "软件著作权申请资料", ) return any(part in r for part in skip_parts) def extract_headings(text: str, limit: int = 24) -> list[str]: headings: list[str] = [] for line in text.splitlines(): clean = line.strip() if clean.startswith("#"): title = clean.lstrip("#").strip() if title and title not in headings: headings.append(title[:120]) if len(headings) >= limit: break return headings def extract_opening(text: str, limit: int = 900) -> str: clean = strip_md(text) return clean[:limit].strip() def collect_documents(project: Path) -> list[dict[str, Any]]: docs: list[dict[str, Any]] = [] for path in iter_project_files(project, DOC_EXTS): if skip_doc(path, project): continue try: text = read_text(path, limit=MAX_DOC_CHARS) except Exception: continue if not text.strip(): continue docs.append( { "path": rel(path, project), "size": path.stat().st_size, "headings": extract_headings(text), "opening": extract_opening(text), } ) docs.sort(key=lambda item: (item["path"].count("/"), item["path"])) return docs[:MAX_DOCS] def collect_code_evidence(analysis: dict[str, Any]) -> dict[str, Any]: source = analysis.get("source") or {} categorized = source.get("categorized_files") or {} return { "project_name": analysis.get("project_name"), "software_name_candidate": analysis.get("software_name_candidate"), "frameworks": analysis.get("frameworks") or [], "language": analysis.get("language"), "routes": analysis.get("routes") or [], "feature_name_candidates": analysis.get("feature_candidates") or [], "entry_files": categorized.get("entry") or [], "page_files": categorized.get("page") or [], "component_files": categorized.get("component") or [], "api_files": categorized.get("api") or [], "run_command_candidates": analysis.get("run_command_candidates") or [], "package": analysis.get("package") or {}, } def build_evidence(project: Path, analysis: dict[str, Any], software_name: str, web_notes: str) -> dict[str, Any]: return { "software_name": software_name, "project_root": str(project.resolve()), "instruction": ( "本文件只收集证据,不决定行业、功能或手册结构。" "请由模型阅读这些证据以及必要的项目源码后,另行编写业务理解模型稿。" ), "documents": collect_documents(project), "code_evidence": collect_code_evidence(analysis), "external_research_notes": web_notes, } def write_evidence_md(path: Path, evidence: dict[str, Any]) -> None: lines = [ "# 业务理解证据", "", f"- 软件名称:{evidence['software_name']}", f"- 项目目录:`{evidence['project_root']}`", "", "本文件只列出可供模型研判的项目证据,不代表最终申报口径。", "模型需要自行判断应阅读哪些文档、抽取哪些功能、采用什么操作手册结构。", "", "## 代码与页面证据", "", ] code = evidence["code_evidence"] for key in ("frameworks", "language", "routes", "feature_name_candidates", "entry_files", "page_files", "component_files", "api_files"): value = code.get(key) if value: lines.append(f"- {key}:{value}") lines.extend(["", "## 文档证据", ""]) for doc in evidence["documents"]: lines.extend( [ f"### {doc['path']}", "", f"- 大小:{doc['size']} bytes", f"- 标题线索:{';'.join(doc['headings']) if doc['headings'] else '无'}", "", doc["opening"], "", ] ) if evidence.get("external_research_notes"): lines.extend(["## 外部调研摘要", "", evidence["external_research_notes"], ""]) path.write_text("\n".join(lines), encoding="utf-8") def write_model_template(path: Path, evidence: dict[str, Any]) -> None: template = { "software_name": evidence["software_name"], "product_positioning": "", "industry": "", "target_users": [], "core_value": "", "business_features": [], "business_feature_details": {}, "operation_flow": [], "application_purpose": "", "main_functions": "", "technical_characteristics": "", "software_technical_option": "应用软件", "software_category": "应用软件", "manual_sections": [ { "title": "模型自行命名章节", "intent": "说明该章节为什么适合当前项目。", "paragraphs": [], "include_feature_overview": False, "include_operation_modules": False, "include_operation_flow": False, } ], "model_review_notes": [ "不要照抄范本结构;按当前项目业务组织章节。", "不要用关键词表决定行业和功能;必须能从项目证据或用户补充中解释来源。", ], } write_json(path, template) def load_model_context(path: Path) -> dict[str, Any]: data = read_json(path) if not isinstance(data, dict): raise SystemExit(f"Invalid model context JSON: {path}") return data def required_list(value: Any, field: str) -> list[str]: if not isinstance(value, list): raise SystemExit(f"Model context field must be a list: {field}") items = [str(item).strip() for item in value if str(item).strip()] if not items: raise SystemExit(f"Model context field cannot be empty: {field}") return items def required_text(data: dict[str, Any], field: str) -> str: value = str(data.get(field) or "").strip() if not value: raise SystemExit(f"Model context field cannot be empty: {field}") return value def normalize_model_context(model: dict[str, Any], evidence: dict[str, Any], web_notes: str) -> dict[str, Any]: features = required_list(model.get("business_features"), "business_features") details = model.get("business_feature_details") or {} if not isinstance(details, dict): raise SystemExit("Model context field must be an object: business_feature_details") missing = [feature for feature in features if not str(details.get(feature) or "").strip()] if missing: raise SystemExit("Model context missing feature details: " + "、".join(missing[:12])) sections = model.get("manual_sections") or [] if sections and not isinstance(sections, list): raise SystemExit("Model context field must be a list: manual_sections") context = { "software_name": evidence["software_name"], "business_understanding_required": True, "source_documents": [{"path": doc["path"], "size": doc["size"]} for doc in evidence["documents"]], "project_evidence_file": "业务理解证据.md", "product_positioning": required_text(model, "product_positioning"), "industry": required_text(model, "industry"), "target_users": required_list(model.get("target_users"), "target_users"), "core_value": required_text(model, "core_value"), "business_features": features, "business_feature_details": {feature: str(details.get(feature)).strip() for feature in features}, "operation_flow": required_list(model.get("operation_flow"), "operation_flow"), "application_purpose": required_text(model, "application_purpose"), "main_functions": required_text(model, "main_functions"), "technical_characteristics": required_text(model, "technical_characteristics"), "software_technical_option": str(model.get("software_technical_option") or "应用软件"), "software_category": str(model.get("software_category") or "应用软件"), "manual_sections": sections, "model_authored": True, "external_research_notes": web_notes, "confirmation_required": True, "user_confirmed": False, "confirmation_stage": "business", "next_action": "请确认 草稿/业务理解.md 中的软件用途、行业、目标用户、核心功能、手册结构和申请口径;确认后运行 confirm_stage.py --stage business。", "review_notes": [ "请确认模型判断的行业领域、目标用户和主要功能是否符合实际申报口径。", "请确认操作手册结构是否适合当前项目,而不是套用范本文档。", ], } return context def write_context_md(path: Path, context: dict[str, Any]) -> None: lines = [ "# 业务理解", "", f"- 软件名称:{context['software_name']}", f"- 产品定位:{context['product_positioning']}", f"- 面向领域 / 行业:{context['industry']}", f"- 核心价值:{context['core_value']}", f"- 证据文件:`{context['project_evidence_file']}`", "", "## 目标用户", "", ] lines.extend(f"- {item}" for item in context["target_users"]) lines.extend(["", "## 主要业务功能", ""]) lines.extend(f"- {item}" for item in context["business_features"]) lines.extend(["", "## 功能说明", ""]) for item in context["business_features"]: lines.append(f"- {item}:{context['business_feature_details'].get(item, '')}") lines.extend(["", "## 典型操作流程", ""]) lines.extend(f"{i}. {item}" for i, item in enumerate(context["operation_flow"], start=1)) if context.get("manual_sections"): lines.extend(["", "## 操作手册结构建议", ""]) for i, section in enumerate(context["manual_sections"], start=1): if isinstance(section, dict): title = section.get("title") or f"章节 {i}" intent = section.get("intent") or "" else: title = str(section) intent = "" lines.append(f"{i}. {title}" + (f":{intent}" if intent else "")) lines.extend( [ "", "## 申请表建议口径", "", f"- 开发目的:{context['application_purpose']}", f"- 软件的主要功能:{context['main_functions']}", f"- 技术特点:{context['technical_characteristics']}", f"- 软件的技术特点选项:{context['software_technical_option']}", f"- 软件分类:{context['software_category']}", "", "## 证据来源", "", ] ) lines.extend(f"- `{item['path']}`" for item in context["source_documents"]) lines.extend(["", "## 待确认", ""]) lines.extend(f"- {item}" for item in context["review_notes"]) lines.extend( [ "", "```text", "STOP_FOR_USER", f"NEXT_ACTION: {context['next_action']}", "```", ] ) 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", required=True) parser.add_argument("--software-name", required=True) parser.add_argument("--out-dir", default="软件著作权申请资料/草稿") parser.add_argument("--web-notes", help="Optional plain-text notes from external/competitor research") parser.add_argument("--model-context", help="Model-authored business context JSON") args = parser.parse_args() project = Path(args.project) analysis = read_json(Path(args.analysis)) web_notes = read_text(Path(args.web_notes)) if args.web_notes else "" out_dir = ensure_dir(Path(args.out_dir)) evidence = build_evidence(project, analysis, args.software_name, web_notes) write_json(out_dir / "业务理解证据.json", evidence) write_evidence_md(out_dir / "业务理解证据.md", evidence) if not args.model_context: write_model_template(out_dir / "业务理解模型稿模板.json", evidence) print(f"OK business evidence: {out_dir / '业务理解证据.md'}") print(f"OK model template: {out_dir / '业务理解模型稿模板.json'}") print("NEXT_ACTION: 模型需要阅读业务理解证据和项目源码,自行编写业务理解模型稿 JSON,然后用 --model-context 生成业务理解.md/json。") return model = load_model_context(Path(args.model_context)) context = normalize_model_context(model, evidence, web_notes) write_json(out_dir / "业务理解.json", context) write_context_md(out_dir / "业务理解.md", context) print(f"OK business context: {out_dir / '业务理解.md'}") print(f"Features: {len(context['business_features'])}") print("STOP_FOR_USER") print(f"NEXT_ACTION: {context['next_action']}") if __name__ == "__main__": main()