"""Scoring and matrix generation for human service coverage.""" from __future__ import annotations from collections import Counter from typing import Iterable, Sequence from .catalog import HUMAN_PROFILES, CATEGORY_KEYWORDS, categories_for_text from .models import ( EvidenceKind, HumanProfile, MatrixCell, MaturityLevel, NeedCategory, PlatformDefinition, PlatformHumanReport, PlatformScan, Recommendation, OrderType, clamp_score, maturity_from_score, merge_unique, score_label, ) EVIDENCE_WEIGHTS: dict[EvidenceKind, int] = { EvidenceKind.README: 5, EvidenceKind.PACKAGE_SCRIPT: 7, EvidenceKind.ROUTE: 8, EvidenceKind.OPENAPI: 12, EvidenceKind.TEST: 10, EvidenceKind.CONFIG: 5, EvidenceKind.DOC: 5, EvidenceKind.WORKER: 8, EvidenceKind.STORAGE: 8, EvidenceKind.MCP_TOOL: 12, EvidenceKind.UI_SURFACE: 11, EvidenceKind.SECURITY: 12, EvidenceKind.BUSINESS_RULE: 10, EvidenceKind.OBSERVABILITY: 11, EvidenceKind.UNKNOWN: 2, } PROFILE_SIGNAL_BONUS: dict[NeedCategory, tuple[EvidenceKind, ...]] = { NeedCategory.ADMINISTRATION: (EvidenceKind.MCP_TOOL, EvidenceKind.UI_SURFACE, EvidenceKind.ROUTE), NeedCategory.SUPPORT: (EvidenceKind.OBSERVABILITY, EvidenceKind.ROUTE, EvidenceKind.DOC), NeedCategory.FINANCE: (EvidenceKind.BUSINESS_RULE, EvidenceKind.ROUTE, EvidenceKind.OPENAPI), NeedCategory.LEGAL: (EvidenceKind.DOC, EvidenceKind.SECURITY, EvidenceKind.OBSERVABILITY), NeedCategory.SECURITY: (EvidenceKind.SECURITY, EvidenceKind.OPENAPI, EvidenceKind.TEST), NeedCategory.OPERATIONS: (EvidenceKind.OBSERVABILITY, EvidenceKind.PACKAGE_SCRIPT, EvidenceKind.WORKER), NeedCategory.STRATEGY: (EvidenceKind.DOC, EvidenceKind.OBSERVABILITY, EvidenceKind.MCP_TOOL), NeedCategory.DOCUMENTATION: (EvidenceKind.README, EvidenceKind.DOC, EvidenceKind.OPENAPI), NeedCategory.SELF_SERVICE: (EvidenceKind.UI_SURFACE, EvidenceKind.ROUTE, EvidenceKind.MCP_TOOL), NeedCategory.COMMERCIAL: (EvidenceKind.BUSINESS_RULE, EvidenceKind.OPENAPI, EvidenceKind.ROUTE), NeedCategory.EXPERIENCE: (EvidenceKind.UI_SURFACE, EvidenceKind.README, EvidenceKind.MCP_TOOL), NeedCategory.GOVERNANCE: (EvidenceKind.OBSERVABILITY, EvidenceKind.SECURITY, EvidenceKind.OPENAPI), NeedCategory.INTEGRATION: (EvidenceKind.MCP_TOOL, EvidenceKind.WORKER, EvidenceKind.SECURITY), NeedCategory.OBSERVABILITY: (EvidenceKind.OBSERVABILITY, EvidenceKind.TEST, EvidenceKind.ROUTE), } def evidence_counter(scan: PlatformScan) -> Counter[EvidenceKind]: counter: Counter[EvidenceKind] = Counter() for evidence in scan.evidence: counter[evidence.kind] += 1 return counter def category_text_score(scan: PlatformScan, categories: Iterable[NeedCategory]) -> int: text = " ".join( [ scan.platform.title, scan.platform.mission, scan.readme_excerpt, " ".join(evidence.summary for evidence in scan.evidence[:120]), ] ).lower() score = 0 for category in categories: keywords = CATEGORY_KEYWORDS.get(category, ()) hits = sum(1 for keyword in keywords if keyword.lower() in text) score += min(14, hits * 3) return min(32, score) def base_repository_score(scan: PlatformScan) -> int: if not scan.exists: return 0 score = 8 if scan.git_present: score += 7 if scan.readme_excerpt: score += 8 if scan.code_lines > 0: score += 8 if scan.code_lines >= 1000: score += 4 if scan.code_lines >= 5000: score += 4 if scan.has_tests: score += 8 if scan.has_openapi: score += 8 if scan.has_worker: score += 4 if scan.scripts: score += min(7, len(scan.scripts)) return min(64, score) def profile_alignment_score(scan: PlatformScan, profile: HumanProfile) -> int: score = 0 counter = evidence_counter(scan) for category in profile.priority_needs: if category in scan.platform.primary_categories: score += 8 for kind in PROFILE_SIGNAL_BONUS.get(category, ()): score += min(9, counter[kind] * 2) if profile.profile_id in scan.platform.expected_profiles: score += 14 categories_from_text = set(categories_for_text(scan.readme_excerpt)) score += len(categories_from_text.intersection(profile.priority_needs)) * 4 return min(44, score) def penalty_score(scan: PlatformScan, profile: HumanProfile) -> int: penalty = 0 warning_text = " ".join(scan.warnings).lower() if "sem .git" in warning_text: penalty += 8 if "nenhuma linha de codigo" in warning_text: penalty += 20 if "testes nao encontrados" in warning_text and NeedCategory.OPERATIONS in profile.priority_needs: penalty += 6 if "openapi" in warning_text and NeedCategory.INTEGRATION in profile.priority_needs: penalty += 6 if scan.platform.known_blockers: penalty += min(14, 5 * len(scan.platform.known_blockers)) return penalty def build_strengths(scan: PlatformScan, profile: HumanProfile, score: int) -> tuple[str, ...]: strengths: list[str] = [] if scan.exists: strengths.append("repositorio real encontrado") if scan.git_present: strengths.append("historico Git local disponivel") if scan.readme_excerpt: strengths.append("README tecnico fornece contexto inicial") if scan.has_tests: strengths.append("testes foram detectados") if scan.has_openapi: strengths.append("contrato OpenAPI foi detectado") if scan.has_worker: strengths.append("sinais de Worker/Cloudflare foram detectados") if profile.profile_id in scan.platform.expected_profiles: strengths.append(f"plataforma declarada como relevante para {profile.name}") for category in profile.priority_needs: if category in scan.platform.primary_categories: strengths.append(f"categoria {category.value} e parte do papel principal da plataforma") if score >= 75: strengths.append("pontuacao indica atendimento humano forte ou pronto") return merge_unique(strengths)[:8] def build_gaps(scan: PlatformScan, profile: HumanProfile, score: int) -> tuple[str, ...]: gaps: list[str] = [] if not scan.exists: gaps.append("repositorio real nao existe no espelho local") if scan.exists and not scan.git_present: gaps.append("repositorio precisa de Git inicializado e remoto configurado") if scan.exists and not scan.readme_excerpt: gaps.append("falta README tecnico para leitura humana") if scan.exists and not scan.has_tests: gaps.append("faltam testes detectaveis para provar comportamento") if scan.exists and not scan.has_openapi and NeedCategory.INTEGRATION in profile.priority_needs: gaps.append("falta contrato OpenAPI ou equivalente para integracao auditavel") if scan.platform.known_blockers: gaps.extend(scan.platform.known_blockers) missing_categories = [category.value for category in profile.priority_needs if category not in scan.platform.primary_categories] if score < 60 and missing_categories: gaps.append("categorias humanas secundarias precisam de explicacao: " + ", ".join(missing_categories[:4])) if score < 40: gaps.append("atendimento humano ainda aparece mais planejado do que operacional") return merge_unique(gaps)[:8] def evidence_refs(scan: PlatformScan, profile: HumanProfile) -> tuple[str, ...]: desired_kinds: set[EvidenceKind] = set() for category in profile.priority_needs: desired_kinds.update(PROFILE_SIGNAL_BONUS.get(category, ())) refs: list[str] = [] for evidence in scan.evidence: if evidence.kind in desired_kinds or evidence.is_strong(): refs.append(evidence.reference) if len(refs) >= 8: break return merge_unique(refs) def explain_cell(scan: PlatformScan, profile: HumanProfile, score: int, maturity: MaturityLevel) -> str: label = score_label(score) if not scan.exists: return f"{profile.name} ainda nao tem base local analisavel em {scan.platform.title}." if score >= 75: return ( f"{scan.platform.title} atende {profile.name} em nivel {label}, " f"com maturidade {maturity.value} e evidencias tecnicas suficientes para leitura humana." ) if score >= 50: return ( f"{scan.platform.title} ja oferece sinais uteis para {profile.name}, " "mas precisa transformar capacidades tecnicas em telas, relatorios ou comandos mais claros." ) return ( f"{scan.platform.title} ainda parece {label} para {profile.name}; " "a proxima evolucao deve explicitar necessidades humanas, evidencias e criterio de pronto." ) def score_cell(scan: PlatformScan, profile: HumanProfile) -> MatrixCell: score = base_repository_score(scan) score += profile_alignment_score(scan, profile) score += category_text_score(scan, profile.priority_needs) score -= penalty_score(scan, profile) score = clamp_score(score) maturity = maturity_from_score(score) return MatrixCell( platform_id=scan.platform.platform_id, profile_id=profile.profile_id, score=score, maturity=maturity, explanation=explain_cell(scan, profile, score, maturity), strengths=build_strengths(scan, profile, score), gaps=build_gaps(scan, profile, score), evidence_refs=evidence_refs(scan, profile), ) def build_matrix(scans: Sequence[PlatformScan], profiles: Sequence[HumanProfile] = HUMAN_PROFILES) -> tuple[MatrixCell, ...]: cells: list[MatrixCell] = [] for scan in scans: for profile in profiles: cells.append(score_cell(scan, profile)) return tuple(cells) def cells_for_platform(cells: Sequence[MatrixCell], platform_id: str) -> tuple[MatrixCell, ...]: return tuple(cell for cell in cells if cell.platform_id == platform_id) def top_gaps(cells: Sequence[MatrixCell], limit: int = 8) -> tuple[str, ...]: gaps: list[str] = [] for cell in sorted(cells, key=lambda item: item.score): gaps.extend(cell.gaps) if len(gaps) >= limit: break return merge_unique(gaps)[:limit] def top_strengths(cells: Sequence[MatrixCell], limit: int = 8) -> tuple[str, ...]: strengths: list[str] = [] for cell in sorted(cells, key=lambda item: item.score, reverse=True): strengths.extend(cell.strengths) if len(strengths) >= limit: break return merge_unique(strengths)[:limit] def build_recommendations_for_scan(scan: PlatformScan, cells: Sequence[MatrixCell]) -> tuple[Recommendation, ...]: recommendations: list[Recommendation] = [] platform_id = scan.platform.platform_id low_cells = [cell for cell in cells if cell.score < 60] if not scan.exists: recommendations.append( Recommendation( recommendation_id=f"{platform_id}-criar-repositorio-real", platform_id=platform_id, title="Criar e inicializar repositorio real da plataforma", reason="A plataforma nao possui base local analisavel.", expected_impact="Permitir execucao tecnica, versionamento e sincronizacao.", categories=(NeedCategory.GOVERNANCE, NeedCategory.OPERATIONS), priority=100, suggested_order_type=OrderType.EXECUTIVE, affected_paths=(scan.repo_path,), validation_steps=("git status", "git remote -v", "linha de base criada"), ) ) if scan.exists and not scan.git_present: recommendations.append( Recommendation( recommendation_id=f"{platform_id}-inicializar-git", platform_id=platform_id, title="Inicializar Git e configurar origin correto", reason="O repositorio existe sem .git, impedindo rastreabilidade e sincronizacao.", expected_impact="Fechar base operacional minima para commits, push e hash final.", categories=(NeedCategory.GOVERNANCE, NeedCategory.OPERATIONS), priority=95, suggested_order_type=OrderType.EXECUTIVE, affected_paths=(scan.repo_path,), validation_steps=("git status --short --branch", "git remote -v"), ) ) if scan.exists and not scan.readme_excerpt: recommendations.append( Recommendation( recommendation_id=f"{platform_id}-readme-humano", platform_id=platform_id, title="Criar README tecnico e humano da plataforma", reason="Sem README, o estado tecnico nao vira compreensao humana inicial.", expected_impact="Dar missao, escopo, comandos e criterios de validacao.", categories=(NeedCategory.DOCUMENTATION, NeedCategory.EXPERIENCE), priority=90, suggested_order_type=OrderType.EXECUTIVE, affected_paths=(f"{scan.repo_path}/README.md",), validation_steps=("README revisado", "links e comandos existentes"), ) ) if scan.exists and not scan.has_tests: recommendations.append( Recommendation( recommendation_id=f"{platform_id}-testes-canonicos", platform_id=platform_id, title="Criar testes canonicos de leitura humana", reason="A varredura nao encontrou testes suficientes para validar comportamento.", expected_impact="Aumentar confianca de suporte, operacao e auditoria.", categories=(NeedCategory.OPERATIONS, NeedCategory.OBSERVABILITY), priority=75, suggested_order_type=OrderType.EXECUTIVE, affected_paths=(f"{scan.repo_path}/tests",), validation_steps=("suite local executada", "relatorio de testes registrado"), ) ) if low_cells: weakest = sorted(low_cells, key=lambda cell: cell.score)[:4] profile_ids = ", ".join(cell.profile_id for cell in weakest) recommendations.append( Recommendation( recommendation_id=f"{platform_id}-matriz-perfis-fracos", platform_id=platform_id, title="Fechar lacunas da matriz humana por perfil", reason=f"Perfis com baixo atendimento detectados: {profile_ids}.", expected_impact="Transformar capacidade tecnica em telas, relatorios e mensagens de acao.", categories=(NeedCategory.EXPERIENCE, NeedCategory.GOVERNANCE, NeedCategory.SUPPORT), priority=70, suggested_order_type=OrderType.MANAGERIAL, affected_paths=(scan.repo_path,), validation_steps=("matriz regenerada", "lacunas reclassificadas", "OS de continuidade criada"), ) ) if scan.platform.known_blockers: recommendations.append( Recommendation( recommendation_id=f"{platform_id}-bloqueios-conhecidos", platform_id=platform_id, title="Resolver ou formalizar bloqueios conhecidos", reason="A plataforma possui bloqueios de maturidade ja mapeados.", expected_impact="Reduzir contradicao entre readiness tecnico e utilidade humana.", categories=(NeedCategory.GOVERNANCE, NeedCategory.OBSERVABILITY), priority=85, suggested_order_type=OrderType.MANAGERIAL, affected_paths=(scan.repo_path,), validation_steps=("bloqueios documentados", "status reavaliado", "evidencia anexada"), ) ) recommendations.sort(key=lambda item: (-item.priority, item.title)) return tuple(recommendations) def build_platform_report(scan: PlatformScan, all_cells: Sequence[MatrixCell]) -> PlatformHumanReport: cells = cells_for_platform(all_cells, scan.platform.platform_id) recommendations = build_recommendations_for_scan(scan, cells) strengths = top_strengths(cells) gaps = top_gaps(cells) if scan.exists: summary = ( f"{scan.platform.title} foi analisada com {scan.code_lines} linhas de codigo e " f"{len(scan.evidence)} evidencias locais. Score medio humano: " f"{round(sum(cell.score for cell in cells) / len(cells)) if cells else 0}." ) else: summary = f"{scan.platform.title} nao possui repositorio local analisavel." current_state = tuple(strengths) or ("estado inicial sem evidencias fortes",) future_state = tuple( [ "telas e relatorios devem responder quem e atendido, como e atendido e qual proxima acao", "evidencias devem ser exportaveis para GPT, painel e central de ordens", "cada lacuna humana deve gerar OS executavel com validacao clara", ] ) missing = tuple(gaps) or ("nenhuma lacuna principal detectada pela matriz atual",) return PlatformHumanReport( platform=scan.platform, scan=scan, cells=cells, recommendations=recommendations, summary=summary, current_state=current_state, future_state=future_state, missing_for_humans=missing, ) def build_platform_reports(scans: Sequence[PlatformScan], cells: Sequence[MatrixCell]) -> tuple[PlatformHumanReport, ...]: return tuple(build_platform_report(scan, cells) for scan in scans) def build_global_recommendations(reports: Sequence[PlatformHumanReport]) -> tuple[Recommendation, ...]: recommendations: list[Recommendation] = [] for report in reports: recommendations.extend(report.recommendations[:3]) average_by_platform = sorted(reports, key=lambda report: report.average_score) for report in average_by_platform[:5]: recommendations.append( Recommendation( recommendation_id=f"global-elevar-{report.platform.platform_id}", platform_id=report.platform.platform_id, title=f"Elevar maturidade humana de {report.platform.title}", reason=f"Score medio atual {report.average_score}; lacunas principais exigem continuidade.", expected_impact="Aumentar clareza para administradores, suporte, clientes e planejamento.", categories=report.platform.primary_categories, priority=65 + max(0, 60 - report.average_score), suggested_order_type=OrderType.MANAGERIAL, affected_paths=(report.scan.repo_path,), validation_steps=("relatorio regenerado", "score comparado", "pendencias atualizadas"), ) ) recommendations.sort(key=lambda item: (-item.priority, item.platform_id, item.title)) return tuple(recommendations) def matrix_table(cells: Sequence[MatrixCell], profiles: Sequence[HumanProfile] = HUMAN_PROFILES) -> list[list[str]]: platform_ids = sorted({cell.platform_id for cell in cells}) by_pair = {(cell.platform_id, cell.profile_id): cell for cell in cells} header = ["platform"] + [profile.profile_id for profile in profiles] rows = [header] for platform_id in platform_ids: row = [platform_id] for profile in profiles: cell = by_pair.get((platform_id, profile.profile_id)) row.append(str(cell.score if cell else 0)) rows.append(row) return rows