191 lines
6.5 KiB
Python
191 lines
6.5 KiB
Python
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
|
|
|