from __future__ import annotations from .shared import * class TaskBriefTemplatesMixin: @classmethod def _project_root_candidates(cls) -> list[Path]: service_file = Path(__file__).resolve() cwd = Path.cwd().resolve() raw_candidates = [ service_file.parents[3], service_file.parents[2], cwd, cwd.parent, ] deduped: list[Path] = [] seen: set[str] = set() for path in raw_candidates: key = str(path) if key in seen: continue seen.add(key) deduped.append(path) return deduped @staticmethod def _safe_dict(value: Any) -> dict[str, Any]: if isinstance(value, dict): try: # 深拷贝,避免浅拷贝触发 JSON 嵌套对象原地修改导致的“提交不落库”。 return json.loads(json.dumps(value)) except Exception: return dict(value) if isinstance(value, str): try: parsed = json.loads(value) return parsed if isinstance(parsed, dict) else {} except Exception: return {} return {} @classmethod def _project_root(cls) -> Path: for root in cls._project_root_candidates(): if (root / "docs" / "architecture").exists(): return root return cls._project_root_candidates()[0] @staticmethod def _normalize_template_text(content: str) -> str: return "\n".join(line.rstrip() for line in str(content or "").strip().splitlines()).strip() @classmethod def _is_default_template_fallback(cls, content: str) -> bool: return cls._normalize_template_text(content) == cls._normalize_template_text(DEFAULT_TEMPLATE_FALLBACK) @classmethod def _load_default_template_content(cls) -> str: template_path = cls._project_root() / "docs/architecture/TASK_BRIEF_TEMPLATE.md" if template_path.exists(): try: return template_path.read_text(encoding="utf-8").strip() or DEFAULT_TEMPLATE_FALLBACK except Exception: return DEFAULT_TEMPLATE_FALLBACK return DEFAULT_TEMPLATE_FALLBACK @classmethod def _resolve_git_ref(cls, ref: str) -> str: try: output = subprocess.check_output( ["git", "rev-parse", "--short", ref], cwd=str(cls._project_root()), stderr=subprocess.DEVNULL, text=True, timeout=1.5, ).strip() return output or "-" except Exception: return "-" @staticmethod def _to_markdown_list(items: list[str], *, empty_text: str) -> str: cleaned = [str(item).strip() for item in items if str(item).strip()] if not cleaned: return f"- {empty_text}" return "\n".join(f"- {item}" for item in cleaned) @classmethod def _render_template_content( cls, *, template_content: str, generated_at: str, base_ref: str, head_ref: str, changed_files: list[str], scope_summary: str, risks: list[str], ) -> str: content = str(template_content or "").strip() or cls._load_default_template_content() replacements = { "{{GENERATED_AT}}": str(generated_at or "-"), "{{BASE_REF}}": str(base_ref or "-"), "{{HEAD_REF}}": str(head_ref or "-"), "{{CHANGED_FILES}}": cls._to_markdown_list(changed_files, empty_text="(none)"), "{{SCOPE_SUMMARY}}": str(scope_summary or "-"), "{{RISKS}}": cls._to_markdown_list(risks, empty_text="待补充风险评估"), } rendered = content for token, value in replacements.items(): rendered = rendered.replace(token, value) return rendered.strip() @classmethod def _build_default_template(cls) -> dict[str, Any]: now_text = now_cn_naive().isoformat() return { "id": DEFAULT_TEMPLATE_ID, "name": DEFAULT_TEMPLATE_NAME, "description": DEFAULT_TEMPLATE_DESCRIPTION, "content": cls._load_default_template_content(), "updated_at": now_text, "updated_by": 0, "created_at": now_text, } @staticmethod def _build_template_item( *, template_id: str, name: str, description: str, content: str, now_text: str, ) -> dict[str, Any]: return { "id": template_id, "name": name, "description": description, "content": content, "updated_at": now_text, "updated_by": 0, "created_at": now_text, } @classmethod def _build_system_templates(cls) -> list[dict[str, Any]]: now_text = now_cn_naive().isoformat() return [ cls._build_default_template(), cls._build_template_item( template_id=LOW_TEMPLATE_ID, name=LOW_TEMPLATE_NAME, description=LOW_TEMPLATE_DESCRIPTION, content=LOW_TEMPLATE_CONTENT, now_text=now_text, ), cls._build_template_item( template_id=HIGH_TEMPLATE_ID, name=HIGH_TEMPLATE_NAME, description=HIGH_TEMPLATE_DESCRIPTION, content=HIGH_TEMPLATE_CONTENT, now_text=now_text, ), cls._build_template_item( template_id=HIGH_CONTRACT_TEMPLATE_ID, name=HIGH_CONTRACT_TEMPLATE_NAME, description=HIGH_CONTRACT_TEMPLATE_DESCRIPTION, content=HIGH_CONTRACT_TEMPLATE_CONTENT, now_text=now_text, ), ] @classmethod def _ensure_system_templates( cls, templates: list[dict[str, Any]], ) -> tuple[list[dict[str, Any]], bool]: merged = [item for item in templates if isinstance(item, dict)] known_ids = {str(item.get("id") or "").strip() for item in merged if str(item.get("id") or "").strip()} changed = False for preset in cls._build_system_templates(): template_id = str(preset.get("id") or "").strip() if not template_id or template_id in known_ids: continue merged.append(preset) known_ids.add(template_id) changed = True return merged, changed