861 lines
34 KiB
Python
861 lines
34 KiB
Python
"""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
|