from __future__ import annotations from .shared import * from .charter_seed_utils import _build_signal_context_text, _safe_threshold, _to_bullet_text, _to_string_list from .charter_prefill_utils import _default_ai_validations, _default_escalation_trigger, _generate_charter_seed _MODULE_FIELD_KEYS: dict[str, tuple[str, ...]] = { "module_1": ("problem_statement", "goal_statement", "kpis"), "module_2": ("in_scope", "out_of_scope", "alignment_qa"), "module_3": ("primary_model", "fallback_model", "capability_table", "dependency_map", "business_view", "engineering_view"), "module_4": ("milestones", "role_effort_estimates", "capability_owner_assignment", "buffer_policy"), "module_5": ("risk_register", "trigger_rules", "execution_rule"), "module_6": ("attachment_metadata", "attachment_summary", "search_tags", "governance_rule"), } _STATUS_REASON_MAP = { "evidence_insufficient": "缺少可验证证据", "generation_failed": "AI 生成失败", "generic_text": "文本存在但证据不足", "conflict_blocked": "存在未决冲突", } def _field_required(module_key: str, field_key: str) -> bool: return field_key in _CHARTER_REQUIRED_FIELDS.get(module_key, ()) def _field_title(module_key: str, field_key: str) -> str: return f"{module_key}.{field_key}" def _stringify_field_value(value: Any) -> str: if value is None: return "" if isinstance(value, str): return value.strip() if isinstance(value, (list, tuple)): items = _to_string_list(value) if not items: return "" return _to_bullet_text(items) return str(value).strip() def _normalize_evidence_items(value: Any) -> list[dict[str, Any]]: if not isinstance(value, list): return [] normalized: list[dict[str, Any]] = [] for item in value: if isinstance(item, dict): try: signal_id = int(item.get("signal_id")) if item.get("signal_id") is not None else None except (TypeError, ValueError): signal_id = None normalized.append( { "signal_id": signal_id, "summary": str(item.get("summary") or item.get("excerpt") or "").strip(), "quote": str(item.get("quote") or item.get("snippet") or "").strip(), "reason": str(item.get("reason") or "").strip(), "confidence": _safe_threshold(item.get("confidence"), 0.0), } ) continue text = str(item or "").strip() if text: normalized.append( { "signal_id": None, "summary": text, "quote": "", "reason": "", "confidence": 0.0, } ) return normalized def _legacy_seed_modules(seed: dict[str, Any]) -> dict[str, dict[str, Any]]: return { "module_1": { "problem_statement": seed.get("summary"), "goal_statement": _to_bullet_text(_to_string_list(seed.get("objectives"))), "kpis": _to_bullet_text( _to_string_list(seed.get("success_metrics") or seed.get("success_criteria")) ), }, "module_2": { "in_scope": _to_bullet_text(_to_string_list(seed.get("in_scope"))), "out_of_scope": _to_bullet_text(_to_string_list(seed.get("out_of_scope"))), "alignment_qa": str(seed.get("alignment_qa") or "").strip(), }, "module_3": { "primary_model": seed.get("primary_model"), "fallback_model": seed.get("fallback_model"), "capability_table": seed.get("capability_table"), "dependency_map": _to_bullet_text(_to_string_list(seed.get("dependencies"))), "business_view": seed.get("business_view"), "engineering_view": seed.get("engineering_view"), }, "module_4": { "milestones": _to_bullet_text(_to_string_list(seed.get("milestones") or seed.get("mvp_items"))), "role_effort_estimates": _to_bullet_text(_to_string_list(seed.get("team_setup"))), "capability_owner_assignment": seed.get("owner_suggestion"), "buffer_policy": seed.get("buffer_policy"), }, "module_5": { "risk_register": _to_bullet_text(_to_string_list(seed.get("risks"))), "trigger_rules": _to_bullet_text(_to_string_list(seed.get("trigger_rules"))), "execution_rule": _to_bullet_text(_to_string_list(seed.get("execution_rule"))), }, "module_6": { "attachment_metadata": _to_bullet_text(_to_string_list(seed.get("attachment_metadata"))), "attachment_summary": seed.get("attachment_summary"), "search_tags": _to_bullet_text(_to_string_list(seed.get("search_tags"))), "governance_rule": seed.get("governance_rule"), }, } def _derive_status( *, value: str, evidence: list[dict[str, Any]], required: bool, explicit_status: str, generation_failed: bool, ) -> str: if explicit_status: return explicit_status if value and evidence: return "valid" if value: return "generic_text" if generation_failed: return "generation_failed" if required: return "evidence_insufficient" return "generation_failed" def _readiness_detail_for_status(module_key: str, field_key: str, status: str) -> str: reason = _STATUS_REASON_MAP.get(status, "未通过校验") return f"{_field_title(module_key, field_key)} {reason}" def _build_verification_metrics(modules: dict[str, dict[str, str]], field_status: dict[str, dict[str, str]]) -> list[dict[str, Any]]: if field_status.get("module_1", {}).get("kpis") != "valid": return [] items = _to_string_list(modules.get("module_1", {}).get("kpis")) return [ { "name": item, "expected": item, "source_field": "module_1.kpis", } for item in items ] def _build_risk_gates(modules: dict[str, dict[str, str]], field_status: dict[str, dict[str, str]]) -> list[dict[str, Any]]: trigger_status = field_status.get("module_5", {}).get("trigger_rules") execution_status = field_status.get("module_5", {}).get("execution_rule") if trigger_status != "valid" or execution_status != "valid": return [] triggers = _to_string_list(modules.get("module_5", {}).get("trigger_rules")) action = modules.get("module_5", {}).get("execution_rule", "").strip() return [ { "name": trigger, "action": action, "source_fields": ["module_5.trigger_rules", "module_5.execution_rule"], } for trigger in triggers ] async def _build_ai_first_charter_content( *, db: AsyncSession, tenant_id: int, project_id: int, signal_rows: list[Signal], signal_ids: list[int], raw_text: str = "", ) -> dict[str, Any]: signal_context = _build_signal_context_text(signal_rows, raw_text=raw_text) seed = await _generate_charter_seed( db=db, tenant_id=tenant_id, project_id=project_id, signal_context=signal_context, ) generation_error = str(seed.get("generation_error") or "").strip() seed_modules = seed.get("modules") if not isinstance(seed_modules, dict): seed_modules = _legacy_seed_modules(seed) modules: dict[str, dict[str, str]] = {} field_status: dict[str, dict[str, str]] = {} field_evidence: dict[str, dict[str, list[dict[str, Any]]]] = {} blocking_fields: list[str] = [] blocking_reasons: list[str] = [] for module_key, field_keys in _MODULE_FIELD_KEYS.items(): modules[module_key] = {} field_status[module_key] = {} field_evidence[module_key] = {} module_seed = seed_modules.get(module_key) if not isinstance(module_seed, dict): module_seed = {} for field_key in field_keys: raw_field = module_seed.get(field_key) explicit_status = "" if isinstance(raw_field, dict): explicit_status = str(raw_field.get("status") or "").strip() raw_value = raw_field.get("value") if raw_value is None: raw_value = raw_field.get("text") else: raw_value = raw_field value = _stringify_field_value(raw_value) evidence = _normalize_evidence_items(raw_field.get("evidence")) if isinstance(raw_field, dict) else [] status = _derive_status( value=value, evidence=evidence, required=_field_required(module_key, field_key), explicit_status=explicit_status, generation_failed=bool(generation_error), ) modules[module_key][field_key] = value field_status[module_key][field_key] = status field_evidence[module_key][field_key] = evidence if _field_required(module_key, field_key) and status != "valid": blocking_fields.append(_field_title(module_key, field_key)) if isinstance(raw_field, dict): reason = str(raw_field.get("blocking_reason") or "").strip() else: reason = "" blocking_reasons.append(reason or _readiness_detail_for_status(module_key, field_key, status)) source_refs = [f"signal:{sid}" for sid in signal_ids] now_iso = now_cn_aware().isoformat() validations_m1 = _default_ai_validations("模块1:触发与目标") validations_m2 = _default_ai_validations("模块2:范围与对齐") validations_m3 = _default_ai_validations("模块3:模型与依赖") validations_m4 = _default_ai_validations("模块4:计划与资源") validations_m5 = _default_ai_validations("模块5:风险与兜底") validations_m6 = _default_ai_validations("模块6:附件与治理") escalation = _default_escalation_trigger() for module_key, validations in ( ("module_1", validations_m1), ("module_2", validations_m2), ("module_3", validations_m3), ("module_4", validations_m4), ("module_5", validations_m5), ("module_6", validations_m6), ): modules[module_key]["ai_validations"] = validations modules[module_key]["escalation_trigger"] = escalation explicit_readiness = seed.get("readiness") if isinstance(seed.get("readiness"), dict) else {} for item in explicit_readiness.get("blocking_fields") or []: field = str(item or "").strip() if field and field not in blocking_fields: blocking_fields.append(field) for item in explicit_readiness.get("blocking_reasons") or []: reason = str(item or "").strip() if reason and reason not in blocking_reasons: blocking_reasons.append(reason) required_total = sum(len(fields) for fields in _CHARTER_REQUIRED_FIELDS.values()) valid_required = 0 for module_key, fields in _CHARTER_REQUIRED_FIELDS.items(): for field_key in fields: if field_status.get(module_key, {}).get(field_key) == "valid": valid_required += 1 readiness = { "ready_for_confirm": not blocking_fields and explicit_readiness.get("ready_for_confirm") is not False, "blocking_fields": blocking_fields, "blocking_reasons": blocking_reasons, } derived_artifacts = seed.get("derived_artifacts") if isinstance(seed.get("derived_artifacts"), dict) else {} if not isinstance(derived_artifacts.get("task_seeds"), list): derived_artifacts["task_seeds"] = [] if not isinstance(derived_artifacts.get("verification_metrics"), list) or not derived_artifacts.get("verification_metrics"): derived_artifacts["verification_metrics"] = _build_verification_metrics(modules, field_status) if not isinstance(derived_artifacts.get("risk_gates"), list) or not derived_artifacts.get("risk_gates"): derived_artifacts["risk_gates"] = _build_risk_gates(modules, field_status) summary = modules["module_1"]["problem_statement"] background = str(seed.get("background") or "").strip() objectives = _to_string_list(modules["module_1"]["goal_statement"]) scope = str(seed.get("scope") or "").strip() in_scope_items = _to_string_list(modules["module_2"]["in_scope"]) out_of_scope_items = _to_string_list(modules["module_2"]["out_of_scope"]) success_metrics = _to_string_list(modules["module_1"]["kpis"]) risks = _to_string_list(modules["module_5"]["risk_register"]) return { "schema_version": "charter_ai_first_v1", "auto_generated_by_ai": True, "metadata": { "status": "draft", "confidence_score": _safe_threshold( seed.get("confidence_score") or (valid_required / required_total if required_total else 0.0), 0.0, ), "last_validated_at": now_iso, "evidence_refs": source_refs, "escalation_reason": blocking_reasons[0] if blocking_reasons else generation_error or None, }, "modules": modules, "field_status": field_status, "field_evidence": field_evidence, "readiness": readiness, "derived_artifacts": derived_artifacts, "signal_ids": signal_ids, "summary": summary, "background": background, "objectives": objectives, "scope": scope, "success_metrics": success_metrics, "core_metrics": success_metrics, "rationale": summary, "decision": _to_bullet_text(objectives), "in_scope": in_scope_items, "out_of_scope": out_of_scope_items, "risks": risks, "raw_source_text": raw_text.strip() or None, }