"""聚合阶段 2~6 的真实案例、外部服务和恢复证据,生成统一蓝图退出报告。""" from __future__ import annotations from datetime import UTC, datetime from typing import Any from django.db import DatabaseError from apps.tickets.evaluation import build_stage_2_acceptance_report from apps.tickets.support_evaluation import build_stage_3_acceptance_report SENSITIVE_KEY_PARTS = ("password", "secret", "token", "credential") EXTERNAL_GATE_SPECS: dict[str, dict[str, Any]] = { "stage3_retrieval": { "title": "阶段 3:PostgreSQL 混合检索与重排", "required_true": ["thresholds_met"], "required_nonempty": [ "embedding_deployment", "rerank_deployment", "metrics", "hnsw", ], "required_positive": ["sample_count"], "required_values": {"environment": "postgresql"}, }, "stage4_distillation": { "title": "阶段 4:真实模型知识蒸馏闭环", "required_true": [ "independent_review", "anonymization_review_passed", "published", "retrieved_after_publish", ], "required_positive": ["sample_count"], "required_sets": { "services": {"postgresql", "embedding", "rerank", "text_model"}, }, }, "stage4_documents": { "title": "阶段 4:真实文档安全导入闭环", "required_true": ["failure_recovery_verified"], "required_sets": { "formats": {"PDF", "TXT", "LOG", "DOCX", "XLSX", "PPTX"}, "services": {"minio", "clamav", "postgresql", "embedding"}, }, }, "stage6_channel": { "title": "阶段 6:真实企业渠道", "required_true": [ "signature_verified", "inbound_verified", "outbound_verified", "receipt_verified", "idempotency_verified", ], "required_nonempty": ["provider"], }, "stage6_identity": { "title": "阶段 6:真实企业身份与目录", "required_true": [ "oidc_login_verified", "group_claim_verified", "failure_fallback_verified", "ldap_directory_sync_verified", ], "required_nonempty": ["oidc_provider", "ldap_directory"], }, "stage6_production_ops": { "title": "阶段 6:生产告警、调度与异地备份", "required_true": [ "alerts_verified", "backup_schedule_installed", "offsite_encrypted_copy_verified", ], "required_nonempty": ["alert_receiver", "backup_schedule", "offsite_storage"], }, } def build_blueprint_readiness_report( *, include_synthetic: bool = False, external_evidence: dict[str, Any] | None = None, recovery_evidence: dict[str, Any] | None = None, stage2_report: dict[str, Any] | None = None, stage3_report: dict[str, Any] | None = None, ) -> dict[str, Any]: """生成九门槛退出报告;只有全部真实证据通过时才返回 ``exit_ready``。""" stage2_report = ( stage2_report if stage2_report is not None else _safe_evaluation_report( build_stage_2_acceptance_report, "阶段 2", include_synthetic, ) ) stage3_report = ( stage3_report if stage3_report is not None else _safe_evaluation_report( build_stage_3_acceptance_report, "阶段 3", include_synthetic, ) ) gates = [ _evaluation_gate( "stage2_intake", "阶段 2:脱敏真实案例智能建单", stage2_report, ), _evaluation_gate( "stage3_support", "阶段 3:脱敏真实案例支持闭环", stage3_report, ), ] manifest_reasons = _manifest_reasons(external_evidence) evidence_gates = ( external_evidence.get("gates", {}) if isinstance(external_evidence, dict) and isinstance(external_evidence.get("gates"), dict) else {} ) for code, spec in EXTERNAL_GATE_SPECS.items(): record = evidence_gates.get(code) reasons = ( [*manifest_reasons] if manifest_reasons else _external_gate_reasons(code, record, spec) ) gates.append( { "code": code, "title": spec["title"], "status": "passed" if not reasons else "not_ready", "source": "external_evidence_manifest", "reasons": reasons, "evidence": record if isinstance(record, dict) else {}, } ) recovery_reasons = _recovery_reasons(recovery_evidence) gates.append( { "code": "stage6_recovery", "title": "阶段 6:隔离一致性恢复", "status": "passed" if not recovery_reasons else "not_ready", "source": "recovery_evidence", "reasons": recovery_reasons, "evidence": recovery_evidence if isinstance(recovery_evidence, dict) else {}, } ) passed_count = sum(gate["status"] == "passed" for gate in gates) exit_ready = passed_count == len(gates) return { "schema_version": 1, "generated_at": datetime.now(tz=UTC).isoformat(), "status": "passed" if exit_ready else "not_ready", "exit_ready": exit_ready, "summary": { "passed_gate_count": passed_count, "total_gate_count": len(gates), "remaining_gate_count": len(gates) - passed_count, }, "gates": gates, } def _safe_evaluation_report(builder, stage: str, include_synthetic: bool) -> dict[str, Any]: """将目标数据库未迁移或不可用转换为明确的未就绪结果。""" try: return builder(include_synthetic=include_synthetic) except DatabaseError: return { "status": "not_ready", "exit_ready": False, "metrics": {}, "reasons": [ { "code": "database_schema_unavailable", "message": f"{stage} 验收数据不可读取,请先在目标环境执行最新数据库迁移", } ], } def _evaluation_gate(code: str, title: str, report: dict[str, Any]) -> dict[str, Any]: reasons = [ str(reason.get("message") or reason.get("code") or "验收条件未满足") for reason in report.get("reasons", []) if isinstance(reason, dict) ] if not report.get("exit_ready") and not reasons: reasons.append(f"{title}尚无足够真实案例证据") return { "code": code, "title": title, "status": "passed" if report.get("exit_ready") else "not_ready", "source": "database_evaluation", "reasons": reasons, "evidence": report, } def _manifest_reasons(payload: dict[str, Any] | None) -> list[str]: if payload is None: return ["未提供外部验收证据清单"] if not isinstance(payload, dict): return ["外部验收证据必须是 JSON 对象"] if payload.get("schema_version") != 1: return ["外部验收证据 schema_version 必须为 1"] if _find_sensitive_keys(payload): return ["外部验收证据不得包含密码、密钥、令牌或凭据字段"] if not isinstance(payload.get("gates"), dict): return ["外部验收证据缺少 gates 对象"] return [] def _external_gate_reasons(code: str, record: Any, spec: dict[str, Any]) -> list[str]: if not isinstance(record, dict): return ["缺少该退出条件的外部验收记录"] reasons = [] if record.get("status") != "passed": reasons.append("验收状态尚未标记为 passed") if not _valid_timestamp(record.get("verified_at")): reasons.append("verified_at 必须是带时区的 ISO 8601 时间") if not str(record.get("reviewer") or "").strip(): reasons.append("缺少验收人 reviewer") artifacts = record.get("artifact_refs") if not isinstance(artifacts, list) or not artifacts or not all( isinstance(item, str) and item.strip() for item in artifacts ): reasons.append("artifact_refs 必须包含至少一个非空证据引用") for field in spec.get("required_true", []): if record.get(field) is not True: reasons.append(f"{field} 必须为 true") for field in spec.get("required_nonempty", []): if record.get(field) in (None, "", [], {}): reasons.append(f"{field} 不能为空") for field in spec.get("required_positive", []): value = record.get(field) if not isinstance(value, (int, float)) or isinstance(value, bool) or value <= 0: reasons.append(f"{field} 必须大于 0") for field, expected in spec.get("required_values", {}).items(): if record.get(field) != expected: reasons.append(f"{field} 必须为 {expected}") for field, expected_items in spec.get("required_sets", {}).items(): value = record.get(field) actual = set(value) if isinstance(value, list) else set() missing = sorted(expected_items - actual) if missing: reasons.append(f"{field} 缺少: {', '.join(missing)}") if record.get("status") == "passed" and code == "stage3_retrieval": reasons.extend(_retrieval_metric_reasons(record)) return reasons def _retrieval_metric_reasons(record: dict[str, Any]) -> list[str]: metrics = record.get("metrics") hnsw = record.get("hnsw") reasons = [] if not isinstance(metrics, dict): return ["metrics 必须记录 recall_at_5 和 p95_latency_ms"] recall = metrics.get("recall_at_5") if not isinstance(recall, (int, float)) or isinstance(recall, bool) or not 0 <= recall <= 1: reasons.append("metrics.recall_at_5 必须位于 0 到 1") latency = metrics.get("p95_latency_ms") if not isinstance(latency, (int, float)) or isinstance(latency, bool) or latency <= 0: reasons.append("metrics.p95_latency_ms 必须大于 0") if not isinstance(hnsw, dict) or any( not isinstance(hnsw.get(key), int) or hnsw[key] <= 0 for key in ("m", "ef_construction", "ef_search") ): reasons.append("hnsw 必须记录正整数 m、ef_construction 和 ef_search") return reasons def _recovery_reasons(payload: dict[str, Any] | None) -> list[str]: if not isinstance(payload, dict): return ["缺少机器可读的隔离恢复演练证据"] reasons = [] expected = { "status": "passed", "database_smoke": "ticket_and_attachment_restored", "object_storage_smoke": "marker_restored", "django_check": "passed", } for field, value in expected.items(): if payload.get(field) != value: reasons.append(f"{field} 必须为 {value}") if not str(payload.get("backup_id") or "").strip(): reasons.append("缺少 backup_id") duration = payload.get("duration_seconds") if not isinstance(duration, (int, float)) or isinstance(duration, bool) or duration <= 0: reasons.append("duration_seconds 必须大于 0") return reasons def _valid_timestamp(value: Any) -> bool: if not isinstance(value, str) or not value.strip(): return False try: parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) except ValueError: return False return parsed.tzinfo is not None def _find_sensitive_keys(value: Any, path: str = "") -> list[str]: """递归定位可能承载密码、密钥、令牌或凭据的字段路径。""" found = [] if isinstance(value, dict): for key, item in value.items(): next_path = f"{path}.{key}" if path else str(key) if any(part in str(key).lower() for part in SENSITIVE_KEY_PARTS): found.append(next_path) found.extend(_find_sensitive_keys(item, next_path)) elif isinstance(value, list): for index, item in enumerate(value): found.extend(_find_sensitive_keys(item, f"{path}[{index}]")) return found