"""Project execution observation adapter for PMFlow AI. This module converts PMFlow OS truth/execution projections into customer-safe evidence facts. It is intentionally read-only and does not decide the final answer shape. """ from __future__ import annotations from dataclasses import dataclass from typing import Any from ..schemas.project_operating_evidence import ( ProjectOperatingEvidenceFacet, ProjectOperatingEvidencePacket, ProjectOperatingEvidenceRef, ProjectOperatingSuggestedAction, ) from ..schemas.pmflow_ai_turn_runtime import PMFlowAIEvidenceFinding @dataclass(frozen=True) class ProjectOperatingEvidenceReaderInput: project_id: int project_title: str project_status: str project_stage: str loop_stage: str truth: dict[str, Any] execution_summary: dict[str, Any] project: dict[str, Any] execution: dict[str, Any] acceptance: dict[str, Any] release_gate: dict[str, Any] quality_contract: dict[str, Any] quality_summary: dict[str, Any] task_counts: dict[str, Any] active_tasks: list[dict[str, Any]] current_task: dict[str, Any] evidence_ref: ProjectOperatingEvidenceRef def build_project_operating_evidence_packet( *, project_id: int, title: str, status: str, truth_payload: dict[str, Any] | None, execution_payload: dict[str, Any] | None, fallback_task_counts: dict[str, Any] | None, object_binding: dict[str, Any] | None = None, ) -> ProjectOperatingEvidencePacket: """Build a structured read-only operating packet for one project.""" truth = _dict(truth_payload) execution_summary = _dict(execution_payload) project = _dict(truth.get("project")) execution = _dict(execution_summary.get("execution")) acceptance = _dict(execution_summary.get("acceptance")) release_gate = _dict(execution_summary.get("releaseGate")) quality_contract = _dict(execution_summary.get("qualityContract")) quality_summary = _dict(quality_contract.get("summary")) task_counts = _dict(fallback_task_counts) active_tasks = _active_tasks(execution_summary) current_task = _current_task(execution=execution, execution_summary=execution_summary) project_title = _text(project.get("name") or project.get("title") or title or f"项目 {project_id}", 160) project_status = _text(project.get("status") or status, 80) project_stage = _text(project.get("stage") or truth.get("stage"), 120) loop_stage = _text(truth.get("loopStage"), 120) ref = ProjectOperatingEvidenceRef( ref_type="project", ref_id=f"project:{project_id}", display_label=project_title, source_surface="PMFlow OS", ) reader_input = ProjectOperatingEvidenceReaderInput( project_id=project_id, project_title=project_title, project_status=project_status, project_stage=project_stage, loop_stage=loop_stage, truth=truth, execution_summary=execution_summary, project=project, execution=execution, acceptance=acceptance, release_gate=release_gate, quality_contract=quality_contract, quality_summary=quality_summary, task_counts=task_counts, active_tasks=active_tasks, current_task=current_task, evidence_ref=ref, ) command_state = _read_command_state(reader_input) workbench_state = _read_workbench_state(reader_input) decision_review_state = _read_decision_review_state(reader_input) execution_state = _read_execution_state(reader_input) quality_test_state = _read_quality_test_state(reader_input) release_growth_state = _read_release_growth_state(reader_input) team_state = _read_team_state(reader_input) project_memory_state = _read_project_memory_state(reader_input) risk_blocker_state = _read_risk_blocker_state(reader_input) recent_timeline_state = _read_recent_timeline_state(reader_input) next_actions = _unique_texts(list(release_gate.get("nextActions") or []), limit=6) blockers = _project_blockers(acceptance=acceptance, release_gate=release_gate, quality_contract=quality_contract) safe_actions = [ ProjectOperatingSuggestedAction( action_key=f"prepare_next_action_{index}", display_label=action, safety="prepare_only", reason="来自 PMFlow OS 门禁/执行态的只读建议,执行前仍需要用户确认边界。", target_ref=f"project:{project_id}", ) for index, action in enumerate(next_actions[:5], start=1) ] blocked_actions = [ ProjectOperatingSuggestedAction( action_key=f"blocked_by_evidence_{index}", display_label=blocker, safety="blocked", reason="当前证据显示该阻塞未解除。", target_ref=f"project:{project_id}", ) for index, blocker in enumerate(blockers[:5], start=1) ] return ProjectOperatingEvidencePacket( project_id=project_id, project_ref=f"project:{project_id}", project_title=project_title, project_status=project_status, object_binding=_dict(object_binding), command_state=command_state, workbench_state=workbench_state, decision_review_state=decision_review_state, execution_state=execution_state, quality_test_state=quality_test_state, release_growth_state=release_growth_state, team_state=team_state, project_memory_state=project_memory_state, risk_blocker_state=risk_blocker_state, recent_timeline_state=recent_timeline_state, evidence_refs=[ref], safe_next_actions=safe_actions, blocked_actions=blocked_actions, ) def build_project_execution_findings( *, project_id: int, title: str, status: str, truth_payload: dict[str, Any] | None, execution_payload: dict[str, Any] | None, fallback_task_counts: dict[str, Any] | None, relevance: str, ) -> tuple[list[PMFlowAIEvidenceFinding], list[str]]: """Build bounded findings and coverage gaps for one project.""" truth = _dict(truth_payload) execution_summary = _dict(execution_payload) project = _dict(truth.get("project")) execution = _dict(execution_summary.get("execution")) acceptance = _dict(execution_summary.get("acceptance")) release_gate = _dict(execution_summary.get("releaseGate")) quality_contract = _dict(execution_summary.get("qualityContract")) quality_summary = _dict(quality_contract.get("summary")) project_title = _text(project.get("name") or project.get("title") or title or f"项目 {project_id}", 160) project_status = _text(project.get("status") or status, 80) project_stage = _text(project.get("stage") or truth.get("stage"), 120) loop_stage = _text(truth.get("loopStage"), 120) findings: list[PMFlowAIEvidenceFinding] = [] gaps: list[str] = [] overview_parts = [project_title] if project_stage: overview_parts.append(f"阶段:{project_stage}") if project_status: overview_parts.append(f"状态:{project_status}") if loop_stage: overview_parts.append(f"运行环节:{loop_stage}") findings.append( _finding( fact="项目概况:" + ",".join(overview_parts) + "。", implication="用于判断当前推进建议应绑定项目阶段和运行环节,而不是只看项目是否存在。", relevance=relevance, ) ) if not project_stage: gaps.append("project_stage_missing") current_task = _current_task(execution=execution, execution_summary=execution_summary) if current_task: task_title = _text(current_task.get("title"), 220) task_status = _text(current_task.get("status"), 80) execution_status = _text(execution.get("status"), 80) task_id = _text(current_task.get("id") or execution.get("taskId"), 80) parts = [task_title or f"任务 {task_id}" if task_id else "当前任务"] if task_id: parts.append(f"任务ID:{task_id}") if task_status: parts.append(f"任务状态:{task_status}") if execution_status: parts.append(f"执行状态:{execution_status}") findings.append( _finding( fact="当前执行任务:" + ",".join(parts) + "。", implication="这是当前推进顺序的第一锚点,优先处理它的完成证据和阻塞。", relevance=relevance, ) ) else: gaps.append("current_execution_state_missing") acceptance_status = _text(acceptance.get("status"), 100) required_action = _text(acceptance.get("requiredAction"), 160) if acceptance_status or required_action: parts = [] if acceptance_status: parts.append(f"验收状态:{acceptance_status}") if required_action: parts.append(f"下一步要求:{required_action}") findings.append( _finding( fact="执行验收:" + ",".join(parts) + "。", implication="用于判断当前推进是否应先补验收材料、测试证据或人工确认。", relevance=relevance, ) ) gate_status = _text(release_gate.get("status"), 100) gate_title = _text(release_gate.get("title"), 120) gate_summary = _text(release_gate.get("summary"), 260) if gate_status or gate_title or gate_summary: parts = [] if gate_status: parts.append(f"门禁状态:{gate_status}") if gate_title: parts.append(gate_title) if gate_summary: parts.append(gate_summary) findings.append( _finding( fact="发布/验收门禁:" + ",".join(parts) + "。", implication="门禁状态决定当前能否继续发布、交付或进入下一轮开发。", relevance=relevance, ) ) blockers = _project_blockers(acceptance=acceptance, release_gate=release_gate, quality_contract=quality_contract) if blockers: findings.append( _finding( fact="阻塞项:" + ";".join(blockers[:6]) + "。", implication="推进建议应优先消除这些阻塞,而不是继续泛化规划。", relevance=relevance, ) ) else: gaps.append("project_blockers_or_release_gate_missing") active_tasks = _active_tasks(execution_summary) if active_tasks: task_summaries = [] for item in active_tasks[:8]: task_title = _text(item.get("title"), 180) if not task_title: continue task_status = _text(item.get("status"), 60) task_summaries.append(f"{task_title}{f'({task_status})' if task_status else ''}") if task_summaries: findings.append( _finding( fact=f"候选任务:当前有 {len(active_tasks)} 个执行/待执行任务," + ";".join(task_summaries[:5]) + "。", implication="这些任务构成可排序的推进队列,可用于给出下一步行动顺序。", relevance=relevance, ) ) else: task_counts = _dict(fallback_task_counts) count_summary = _task_count_summary(task_counts) if count_summary: findings.append( _finding( fact="候选任务:" + count_summary + "。", implication="当前只拿到任务数量,缺少任务标题和阻塞细节。", relevance=relevance, confidence="medium", ) ) else: gaps.append("active_or_next_tasks_missing") next_actions = _unique_texts(list(release_gate.get("nextActions") or []), limit=5) if next_actions: findings.append( _finding( fact="系统建议下一步:" + ";".join(next_actions[:4]) + "。", implication="这是 PMFlow OS 根据门禁与证据链给出的直接推进动作。", relevance=relevance, ) ) memory_summary = _memory_summary(truth) if memory_summary: findings.append( _finding( fact="项目记忆:" + memory_summary, implication="项目记忆用于区分已确认事实和仍待审的想法,避免重复规划。", relevance=relevance, confidence="medium", ) ) quality_missing = _safe_int(quality_summary.get("releaseBlockingMissing")) if quality_missing: findings.append( _finding( fact=f"质量合同:还有 {quality_missing} 个发布阻塞级证据未完成。", implication="当前推进应先补齐发布阻塞级证据,再讨论发布或扩大范围。", relevance=relevance, ) ) if not execution_summary: gaps.append("project_execution_projection_missing") return findings[:10], _unique_texts(gaps, limit=8) def _facet( *, key: str, label: str, facts: list[str], summary: str, evidence_refs: list[ProjectOperatingEvidenceRef], missing: list[str], partial: bool = False, ) -> ProjectOperatingEvidenceFacet: clean_facts = _unique_texts(facts, limit=12) coverage = "covered" if clean_facts and not partial and not missing else "partial" if clean_facts else "missing" return ProjectOperatingEvidenceFacet( facet_key=key, display_label=label, coverage_status=coverage, summary=_text(summary or ";".join(clean_facts[:4]), 500), facts=clean_facts, evidence_refs=evidence_refs, remaining_gaps=[] if coverage == "covered" else missing, ) def _read_command_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet: facts = _unique_texts( [ f"项目:{data.project_title}" if data.project_title else "", f"阶段:{data.project_stage}" if data.project_stage else "", f"状态:{data.project_status}" if data.project_status else "", f"运行环节:{data.loop_stage}" if data.loop_stage else "", f"最后写入:{_text(data.truth.get('lastWritebackAt'), 120)}" if data.truth.get("lastWritebackAt") else "", ], limit=8, ) return _facet( key="command_state", label="指挥台", facts=facts, summary=",".join(facts), evidence_refs=[data.evidence_ref], missing=["project_stage_missing"] if not data.project_stage else [], ) def _read_workbench_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet: task_facts = _task_facts(data) requirements = _model_rows(data.truth.get("requirements")) risks = _model_rows(data.truth.get("risks")) facts = [ *task_facts, f"需求 {len(requirements)} 个" if requirements else "", f"风险 {len(risks)} 个" if risks else "", ] return _facet( key="workbench_state", label="项目工作台", facts=_unique_texts(facts, limit=12), summary=";".join(_unique_texts(facts, limit=5)), evidence_refs=[data.evidence_ref], missing=[] if _unique_texts(facts, limit=12) else ["active_or_next_tasks_missing"], partial=bool(task_facts and not data.active_tasks), ) def _read_decision_review_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet: reviewer_routing = _dict(data.truth.get("reviewerRouting")) routing_summary = _dict(reviewer_routing.get("summary")) change_sets = _list(data.truth.get("changeSets")) change_items = _list(data.truth.get("changeItems")) approvals = _list(data.truth.get("approvals")) routing_items = _list(reviewer_routing.get("items")) facts = _unique_texts( [ f"待审变更集 {len(change_sets)} 个" if change_sets else "", f"待审变更项 {len(change_items)} 个" if change_items else "", f"审批记录 {len(approvals)} 条" if approvals else "", f"待审路由 {len(routing_items)} 条" if routing_items else "", f"待审数:{routing_summary.get('totalPendingReview')}" if routing_summary.get("totalPendingReview") is not None else "", f"高风险待审:{routing_summary.get('highRiskCount')}" if routing_summary.get("highRiskCount") else "", f"验收状态:{_text(data.acceptance.get('status'), 100)}" if data.acceptance.get("status") else "", f"下一步要求:{_text(data.acceptance.get('requiredAction'), 160)}" if data.acceptance.get("requiredAction") else "", ], limit=12, ) return _facet( key="decision_review_state", label="决策与待确认", facts=facts, summary=";".join(facts[:5]), evidence_refs=[data.evidence_ref], missing=[] if facts else ["decision_review_state_missing"], ) def _read_execution_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet: prompt_package = _dict(data.execution_summary.get("promptPackage")) repo_bindings = _list(data.execution_summary.get("repoBindings")) recent_events = _list(data.execution_summary.get("recentProgressEvents")) recent_deliveries = _list(data.execution_summary.get("recentDeliveries")) facts = _unique_texts( [ f"执行状态:{_text(data.execution.get('status'), 100)}" if data.execution.get("status") else "", *_task_facts(data)[:4], f"执行包:{_text(prompt_package.get('title') or prompt_package.get('id'), 160)}" if prompt_package else "", f"代码仓库绑定 {len(repo_bindings)} 个" if repo_bindings else "", f"进展事件 {len(recent_events)} 条" if recent_events else "", f"交付记录 {len(recent_deliveries)} 条" if recent_deliveries else "", ], limit=12, ) return _facet( key="execution_state", label="开发执行", facts=facts, summary=";".join(facts[:5]), evidence_refs=[data.evidence_ref], missing=[] if data.current_task or data.execution.get("status") or recent_events else ["current_execution_state_missing"], ) def _read_quality_test_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet: test_runs = _model_rows(data.truth.get("testRuns")) delivery_artifacts = _dict(data.execution_summary.get("deliveryArtifacts")) artifact_review = _dict(data.execution_summary.get("artifactReview")) artifact_completeness = _dict(data.execution_summary.get("artifactCompleteness")) test_outputs = _list(delivery_artifacts.get("test_outputs") or delivery_artifacts.get("testOutputs")) review_items = _list(artifact_review.get("items") or artifact_review.get("reviewItems")) quality_blockers = _unique_texts(list(data.quality_contract.get("blockingReasons") or []), limit=6) quality_missing = _safe_int(data.quality_summary.get("releaseBlockingMissing")) facts = _unique_texts( [ f"发布阻塞级证据未完成 {quality_missing} 个" if quality_missing else "", f"测试记录 {len(test_runs)} 条" if test_runs else "", f"测试输出 {len(test_outputs)} 条" if test_outputs else "", f"交付审查项 {len(review_items)} 个" if review_items else "", f"证据完整度:{_text(artifact_completeness.get('status'), 100)}" if artifact_completeness.get("status") else "", *_quality_review_facts(review_items), *quality_blockers, ], limit=12, ) return _facet( key="quality_test_state", label="测试与质量", facts=facts, summary=";".join(facts[:5]), evidence_refs=[data.evidence_ref], missing=[] if data.quality_contract or test_runs or test_outputs or artifact_review else ["quality_test_state_missing"], partial=bool((data.quality_contract or artifact_review) and not facts), ) def _read_release_growth_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet: releases = _model_rows(data.truth.get("releases")) metric_signals = _model_rows(data.truth.get("metricSignals")) release_signals = _dict(data.execution_summary.get("releaseSignals")) next_actions = _unique_texts(list(data.release_gate.get("nextActions") or []), limit=6) feedback = _list(release_signals.get("feedback") or release_signals.get("feedbackIngests")) recommendations = _list(release_signals.get("recommendations") or release_signals.get("iterationRecommendations")) facts = _unique_texts( [ f"门禁状态:{_text(data.release_gate.get('status'), 100)}" if data.release_gate.get("status") else "", _text(data.release_gate.get("title"), 120), _text(data.release_gate.get("summary"), 260), f"发布记录 {len(releases)} 条" if releases else "", f"增长信号 {len(metric_signals)} 条" if metric_signals else "", f"反馈输入 {len(feedback)} 条" if feedback else "", f"迭代建议 {len(recommendations)} 条" if recommendations else "", *[f"建议动作:{item}" for item in next_actions], ], limit=12, ) return _facet( key="release_growth_state", label="发布与增长", facts=facts, summary=";".join(facts[:5]), evidence_refs=[data.evidence_ref], missing=[] if data.release_gate or releases or metric_signals or release_signals else ["release_growth_state_missing"], partial=bool((data.release_gate or release_signals) and not facts), ) def _read_team_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet: reviewer_routing = _dict(data.truth.get("reviewerRouting")) routing_summary = _dict(reviewer_routing.get("summary")) routing_items = _list(reviewer_routing.get("items")) team_inbox = _dict(data.truth.get("teamInbox")) inbox_counts = [ f"{label} {len(rows)}" for label, rows in ( ("产品收件箱", _list(team_inbox.get("product"))), ("工程收件箱", _list(team_inbox.get("engineering"))), ("测试收件箱", _list(team_inbox.get("qa"))), ("增长收件箱", _list(team_inbox.get("growth"))), ) if rows ] facts = _unique_texts( [ *_team_facts(project=data.project, execution=data.execution, active_tasks=data.active_tasks), *inbox_counts, f"待审路由 {len(routing_items)} 条" if routing_items else "", f"缺审查人 {routing_summary.get('missingReviewerCount')} 项" if routing_summary.get("missingReviewerCount") else "", *_reviewer_suggestion_facts(routing_items), ], limit=12, ) return _facet( key="team_state", label="团队层", facts=facts, summary=";".join(facts[:5]), evidence_refs=[data.evidence_ref], missing=[] if facts else ["team_state_missing"], ) def _read_project_memory_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet: memory = _dict(data.truth.get("projectMemory")) source_summary = _dict(memory.get("sourceSummary")) memory_summary = _memory_summary(data.truth) facts = _unique_texts( [ memory_summary.rstrip("。") if memory_summary else "", *[f"{key} {value}" for key, value in source_summary.items() if value], ], limit=12, ) return _facet( key="project_memory_state", label="项目记忆", facts=facts, summary=";".join(facts[:5]), evidence_refs=[data.evidence_ref], missing=[] if facts else ["project_memory_missing"], ) def _read_risk_blocker_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet: risks = _model_rows(data.truth.get("risks")) warnings = _unique_texts(list(data.truth.get("projectionWarnings") or []) + list(data.execution_summary.get("warnings") or []), limit=8) blockers = _project_blockers(acceptance=data.acceptance, release_gate=data.release_gate, quality_contract=data.quality_contract) artifact_review = _dict(data.execution_summary.get("artifactReview")) review_blockers = _unique_texts(list(artifact_review.get("blockingReasons") or []), limit=6) risk_facts = _unique_texts( [ *blockers, *review_blockers, *[f"风险:{_text(item.get('title') or item.get('summary'), 180)}" for item in risks[:5] if isinstance(item, dict)], *[f"投影警告:{item}" for item in warnings[:4]], "已读取验收/门禁/质量信息,未发现明确阻塞项。" if not blockers and not review_blockers and (data.acceptance or data.release_gate or data.quality_contract) else "", ], limit=12, ) return _facet( key="risk_blocker_state", label="风险与阻塞", facts=risk_facts, summary=";".join(risk_facts[:5]), evidence_refs=[data.evidence_ref], missing=[] if risk_facts else ["project_blockers_or_release_gate_missing"], partial=bool(risk_facts and not blockers and not review_blockers), ) def _read_recent_timeline_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet: facts = _timeline_facts(execution_summary=data.execution_summary, current_task=data.current_task, truth=data.truth) return _facet( key="recent_timeline_state", label="最近动态", facts=facts, summary=";".join(facts[:5]), evidence_refs=[data.evidence_ref], missing=[] if facts else ["recent_timeline_missing"], partial=bool(not facts and (data.current_task or data.active_tasks)), ) def _task_facts(data: ProjectOperatingEvidenceReaderInput) -> list[str]: facts: list[str] = [] if data.current_task: task_title = _text(data.current_task.get("title"), 220) task_status = _text(data.current_task.get("status"), 80) task_id = _text(data.current_task.get("id") or data.execution.get("taskId"), 80) facts.append( "当前任务:" + ",".join( part for part in [ task_title or (f"任务 {task_id}" if task_id else "当前任务"), f"任务ID:{task_id}" if task_id else "", f"状态:{task_status}" if task_status else "", ] if part ) ) if data.active_tasks: facts.append(f"执行/待执行任务 {len(data.active_tasks)} 个") for item in data.active_tasks[:5]: task_title = _text(item.get("title"), 160) task_status = _text(item.get("status"), 60) if task_title: facts.append(f"{task_title}{f'({task_status})' if task_status else ''}") else: count_summary = _task_count_summary(data.task_counts) if count_summary: facts.append(count_summary) return _unique_texts(facts, limit=10) def _project_blockers( *, acceptance: dict[str, Any], release_gate: dict[str, Any], quality_contract: dict[str, Any], ) -> list[str]: return _unique_texts( [ *list(acceptance.get("blockingReasons") or []), *list(release_gate.get("blockingReasons") or []), *list(quality_contract.get("blockingReasons") or []), ], limit=8, ) def _team_facts( *, project: dict[str, Any], execution: dict[str, Any], active_tasks: list[dict[str, Any]], ) -> list[str]: candidates: list[Any] = [ project.get("ownerName"), project.get("owner"), project.get("responsibleUserName"), execution.get("assigneeName"), execution.get("assignee"), ] for item in active_tasks[:5]: candidates.extend([item.get("assigneeName"), item.get("ownerName"), item.get("reviewerName")]) facts = [f"负责人/协作者:{value}" for value in _unique_texts(candidates, limit=6)] return facts def _quality_review_facts(review_items: list[Any]) -> list[str]: out: list[str] = [] for item in review_items[:6]: if not isinstance(item, dict): continue title = _text(item.get("title") or item.get("item") or item.get("summary"), 160) status = _text(item.get("status"), 80) if title: out.append(f"交付审查:{title}{f'({status})' if status else ''}") return _unique_texts(out, limit=6) def _reviewer_suggestion_facts(rows: list[Any]) -> list[str]: out: list[str] = [] for item in rows[:6]: if not isinstance(item, dict): continue reviewer = _dict(item.get("suggestedReviewer")) display_name = _text(reviewer.get("displayName") or reviewer.get("role"), 120) reason = _text(item.get("reviewReason") or reviewer.get("reason"), 180) if display_name or reason: out.append("审查建议:" + ",".join(part for part in [display_name, reason] if part)) return _unique_texts(out, limit=6) def _timeline_facts(*, execution_summary: dict[str, Any], current_task: dict[str, Any], truth: dict[str, Any] | None = None) -> list[str]: rows: list[Any] = [] truth_payload = _dict(truth) rows.extend(_list(truth_payload.get("feed"))[:6]) for key in ("recentProgressEvents", "recentDeliveries", "recentEvents", "timeline", "activities", "progressEvents", "events"): value = execution_summary.get(key) if isinstance(value, list): rows.extend(value[:6]) facts: list[str] = [] for item in rows[:8]: if isinstance(item, dict): text = _text(item.get("summary") or item.get("title") or item.get("description") or item.get("message"), 220) status = _text(item.get("status"), 80) if text: facts.append(f"{text}{f'({status})' if status else ''}") else: text = _text(item, 220) if text: facts.append(text) if not facts and current_task: title = _text(current_task.get("title"), 160) status = _text(current_task.get("status"), 80) if title: facts.append(f"当前最近可见动态来自正在处理的任务:{title}{f'({status})' if status else ''}") return _unique_texts(facts, limit=8) def _model_rows(value: Any) -> list[dict[str, Any]]: rows = value if isinstance(value, list) else [] out: list[dict[str, Any]] = [] for item in rows: if isinstance(item, dict): out.append(dict(item)) elif hasattr(item, "model_dump"): out.append(item.model_dump(mode="json")) return out def _list(value: Any) -> list[Any]: return list(value) if isinstance(value, list) else [] def _finding( *, fact: str, implication: str, relevance: str, confidence: str = "high", ) -> PMFlowAIEvidenceFinding: return PMFlowAIEvidenceFinding( fact=_text(fact, 700), source="PMFlow 项目执行态证据", implication=_text(implication, 700), relevance_to_goal=_text(relevance, 160), confidence=confidence if confidence in {"high", "medium", "low", "unknown"} else "unknown", ) def _current_task(*, execution: dict[str, Any], execution_summary: dict[str, Any]) -> dict[str, Any]: task_id = _text(execution.get("taskId"), 80) active_tasks = _active_tasks(execution_summary) if task_id: for item in active_tasks: if _text(item.get("id"), 80) == task_id: return item if active_tasks: return active_tasks[0] prompt = _text(execution.get("prompt"), 700) title = _extract_prompt_line(prompt, "- Title:") status = _extract_prompt_line(prompt, "- Status:") if title or task_id: return {"id": task_id, "title": title, "status": status} return {} def _active_tasks(execution_summary: dict[str, Any]) -> list[dict[str, Any]]: rows = execution_summary.get("activeDevTasks") if not isinstance(rows, list): return [] return [dict(item) for item in rows if isinstance(item, dict)] def _memory_summary(truth: dict[str, Any]) -> str: memory = _dict(truth.get("projectMemory")) truth_nodes = list(memory.get("truthNodeFacts") or []) semantic_facts = list(memory.get("semanticFacts") or []) rows: list[str] = [] for item in truth_nodes[:4]: if isinstance(item, dict): text = _text(item.get("summary") or item.get("title") or item.get("fact"), 160) else: text = _text(item, 160) if text: rows.append(text) if not rows: for item in semantic_facts[:4]: if isinstance(item, dict): text = _text(item.get("summary") or item.get("value") or item.get("fact"), 160) else: text = _text(item, 160) if text: rows.append(text) return ";".join(_unique_texts(rows, limit=4)) + ("。" if rows else "") def _task_count_summary(task_counts: dict[str, Any]) -> str: parts: list[str] = [] for key, label in ( ("todo", "待处理"), ("pending", "待处理"), ("in_progress", "进行中"), ("blocked", "阻塞"), ("done", "已完成"), ): value = _safe_int(task_counts.get(key)) if value: parts.append(f"{label} {value}") return "任务:" + "、".join(parts[:5]) if parts else "" def _extract_prompt_line(prompt: str, prefix: str) -> str: for line in prompt.splitlines(): if line.strip().startswith(prefix): return _text(line.split(prefix, 1)[-1].strip(), 220) return "" def _dict(value: Any) -> dict[str, Any]: return dict(value) if isinstance(value, dict) else {} def _text(value: Any, limit: int) -> str: return str(value or "").strip().replace("\x00", "")[:limit] def _safe_int(value: Any) -> int: try: if value is None or value == "": return 0 return int(value) except Exception: return 0 def _unique_texts(values: list[Any], *, limit: int) -> list[str]: out: list[str] = [] for value in values: text = _text(value, 240) if text and text not in out: out.append(text) if len(out) >= limit: break return out