"""Candidate rewrite for Builder entries (light STAR optimization).""" from __future__ import annotations from copy import deepcopy import re from typing import Any from .state import _dedupe_strings from ..experience_optimizer import _fact_text_is_preserved, split_description_parts def _candidate_rewrite( agent: Any, profile: dict[str, Any], entry: dict[str, Any], section: str, *, instruction: str | None = None, ensure_facts: bool = False, ) -> dict[str, Any]: try: proposal = agent.expander.expand( deepcopy(entry), context={ "job_type": profile.get("job_type"), "target_position": profile.get("target_position"), "entry_type": section, "instruction": instruction, }, ) except Exception: proposal = {} original = str(entry.get("description") or "").strip() optimized = str(proposal.get("optimized_description") or "").strip() or original if ensure_facts: # Explicit user-requested revision: still-missing material facts are folded # back in (the user asked for them; this is not a silent auto-append). missing = _uncovered_material_facts(optimized, original) if missing: if "• " in optimized: optimized = optimized + "".join(f"\n• {fact}" for fact in missing) else: optimized = f"{optimized.rstrip('。')};{';'.join(missing)}。" return { "optimized_description": optimized, "changes": proposal.get("changes") or [], "source": proposal.get("source") or "ai_expanded", "uncovered_facts": _uncovered_material_facts(optimized, original), **({"generation_source": proposal["generation_source"]} if proposal.get("generation_source") else {}), } def _uncovered_material_facts(candidate: str, original: str) -> list[str]: """Material user facts the candidate dropped. Reported, never auto-appended.""" uncovered = [fact for fact in _material_fact_fragments(original) if not _fact_is_preserved(fact, candidate)] fragments = split_description_parts(original) if len(fragments) >= 2: # Structured descriptions (feature lists, tech stack, outcomes) are checked # fragment by fragment, so a dropped feature module is reported even when the # tech stack survived. Single-sentence descriptions keep the regex-only path. ledger = [ {"id": f"fragment_{index}", "source": "user_form", "field": "description_part", "text": fragment} for index, fragment in enumerate(fragments, start=1) ] uncovered.extend( fragment for index, fragment in enumerate(fragments, start=1) if not _fact_text_is_preserved(f"fragment_{index}", ledger, candidate) ) return _dedupe_strings(uncovered) def _material_fact_fragments(text: str) -> list[str]: facts: list[str] = [] patterns = ( r"gpa\s*[::]?\s*\d+(?:\.\d+)?\s*/\s*\d+(?:\.\d+)?", r"(?:排名\s*)?(?:前\s*百分之\s*\d+(?:\.\d+)?|前\s*\d+(?:\.\d+)?\s*%|top\s*\d+(?:\.\d+)?\s*%)", r"(?:专业|年级)?(?:排名)?前(?:十|二十|三十|五十)", r"(?:获得|荣获|获评|获奖|取得)[^。;;\n]{0,30}(?:奖学金|奖项|荣誉|一等奖|二等奖|三等奖|优秀[^。;;\n]{0,12})", r"(?:完成|参与|负责|主导|开发|设计|实现|搭建|推进|开展)[^。;;\n]{0,40}(?:课程项目|课程设计|项目|竞赛|实验室|实践|实训|研究|论文)", r"(?:服务|覆盖|面向|参与|支持|管理|处理|完成|交付|提升|降低|增长)[^。;;\n]{0,20}?\d+(?:\.\d+)?\s*(?:%|人|名(?:学生|用户|客户|参与者)?|次|天|周|月|小时|万元|万|千|个|项|篇|场)", ) for pattern in patterns: facts.extend(match.group(0).strip(" \t,,") for match in re.finditer(pattern, text, flags=re.IGNORECASE)) tool_pattern = r"\b(?:python|sql|java|javascript|typescript|vue|react|excel|power\s*bi|tableau|pandas|tensorflow|pytorch|docker|git|linux)\b" facts.extend(match.group(0).strip() for match in re.finditer(tool_pattern, text, flags=re.IGNORECASE)) return _dedupe_strings([fact for fact in facts if fact]) def _fact_is_preserved(fact: str, candidate: str) -> bool: normalized_fact = _normalize_material_fact(fact) normalized_candidate = _normalize_material_fact(candidate) return bool(normalized_fact) and normalized_fact in normalized_candidate def _normalize_material_fact(value: str) -> str: normalized = value.casefold().replace("百分之", "%") normalized = re.sub(r"(?:排名|专业排名|年级排名)?前\s*(\d+(?:\.\d+)?)\s*%?", r"top\1", normalized) normalized = re.sub(r"top\s*(\d+(?:\.\d+)?)\s*%?", r"top\1", normalized) return re.sub(r"[\s,,。;;::]", "", normalized)