"""Signal extraction rules for the Mais Humana operational dossier. This module turns local repository evidence into normalized operational signals. It does not decide whether a project is good or bad by a single keyword. The goal is to preserve useful nuance: * a Docs catalog-only decision can be a formal exception or a blocker; * a BYOK credential reference is a capability, while a missing tenant smoke is a blocker; * Cloudflare plugin denial is expected and must not become a platform blocker; * wrangler, HTTP evidence, readiness, sameSource, and panelReady are real operational signals; * repository, Git, tests, OpenAPI, and security redaction remain separate gates. """ from __future__ import annotations import re from dataclasses import dataclass from pathlib import Path from typing import Iterable, Sequence from .models import EvidenceKind, NeedCategory, PlatformHumanReport, PlatformScan, Recommendation, merge_unique, slugify from .operational_models import ( EvidenceRole, GateDomain, HumanReadinessStage, OperationalSignal, SignalKind, SignalSeverity, SourceConfidence, SourceReference, source_refs_from_evidence, source_refs_from_strings, stable_digest, ) @dataclass(slots=True) class SignalRule: """A lightweight matching rule for evidence summaries, warning text, and paths.""" rule_id: str title: str kind: SignalKind domain: GateDomain severity: SignalSeverity stage: HumanReadinessStage categories: tuple[NeedCategory, ...] patterns: tuple[str, ...] positive_summary: str next_action: str tags: tuple[str, ...] = () def matches(self, text: str) -> bool: lowered = text.lower() return any(re.search(pattern, lowered, re.I) for pattern in self.patterns) def rule( rule_id: str, title: str, kind: SignalKind, domain: GateDomain, severity: SignalSeverity, stage: HumanReadinessStage, categories: Iterable[NeedCategory], patterns: Iterable[str], summary: str, next_action: str, tags: Iterable[str] = (), ) -> SignalRule: return SignalRule( rule_id=rule_id, title=title, kind=kind, domain=domain, severity=severity, stage=stage, categories=tuple(categories), patterns=tuple(patterns), positive_summary=summary, next_action=next_action, tags=tuple(tags), ) CAPABILITY_RULES: tuple[SignalRule, ...] = ( rule( "readiness-surface", "Readiness operacional detectada", SignalKind.CAPABILITY, GateDomain.OBSERVABILITY, SignalSeverity.INFO, HumanReadinessStage.TECHNICAL_READY, (NeedCategory.OBSERVABILITY, NeedCategory.OPERATIONS), (r"\breadiness\b", r"\bready\b", r"prontid"), "O repositorio possui indicios de readiness ou prontidao operacional.", "manter readiness como evidencia regressiva", ("readiness",), ), rule( "health-surface", "Health check detectado", SignalKind.CAPABILITY, GateDomain.RUNTIME, SignalSeverity.INFO, HumanReadinessStage.TECHNICAL_READY, (NeedCategory.OPERATIONS, NeedCategory.OBSERVABILITY), (r"\bhealth\b", r"/health\b"), "O repositorio expoe ou documenta health check.", "validar health em smoke local ou publicado", ("health",), ), rule( "openapi-contract", "Contrato OpenAPI detectado", SignalKind.CAPABILITY, GateDomain.CONTRACT, SignalSeverity.INFO, HumanReadinessStage.HUMAN_EXPLAINABLE, (NeedCategory.DOCUMENTATION, NeedCategory.INTEGRATION, NeedCategory.GOVERNANCE), (r"openapi", r"swagger"), "O repositorio possui contrato OpenAPI, documento ou rota relacionada.", "manter contrato sincronizado com rotas reais", ("openapi",), ), rule( "panel-ready-signal", "panelReady detectado", SignalKind.CAPABILITY, GateDomain.PANEL, SignalSeverity.INFO, HumanReadinessStage.PANEL_READY, (NeedCategory.EXPERIENCE, NeedCategory.GOVERNANCE), (r"panelready", r"panel ready", r"painel.*pront"), "Ha indicio de contrato de tela pronto para painel humano.", "validar se panelReady usa a mesma fonte do GPT", ("panelReady",), ), rule( "same-source-signal", "sameSource detectado", SignalKind.CAPABILITY, GateDomain.PANEL, SignalSeverity.INFO, HumanReadinessStage.PANEL_READY, (NeedCategory.EXPERIENCE, NeedCategory.GOVERNANCE, NeedCategory.OBSERVABILITY), (r"samesource", r"same source", r"mesma fonte", r"sourcehash", r"recordsHash"), "Ha indicio de mesma fonte entre GPT, painel e evidencia.", "manter hash de fonte e registros em regressao", ("sameSource",), ), rule( "credential-ref-signal", "credentialRef detectado", SignalKind.CAPABILITY, GateDomain.SECURITY, SignalSeverity.INFO, HumanReadinessStage.CONTROLLED_READY, (NeedCategory.SECURITY, NeedCategory.INTEGRATION), (r"credentialref", r"credential ref", r"credential:"), "A plataforma usa referencia de credencial em vez de expor segredo bruto.", "validar nao vazamento em relatorios, logs e respostas", ("credentialRef", "redaction"), ), rule( "byok-signal", "BYOK detectado", SignalKind.CAPABILITY, GateDomain.INTEGRATION, SignalSeverity.INFO, HumanReadinessStage.CONTROLLED_READY, (NeedCategory.INTEGRATION, NeedCategory.SECURITY, NeedCategory.COMMERCIAL), (r"\bbyok\b", r"bring your own key", r"credencial.*cliente"), "A jornada BYOK aparece como superficie tecnica ou comercial.", "validar cadeia organizacao, usuario, entitlement, credentialRef, smoke e consumo", ("BYOK",), ), rule( "audit-trace-signal", "Trace e auditoria detectados", SignalKind.CAPABILITY, GateDomain.OBSERVABILITY, SignalSeverity.INFO, HumanReadinessStage.HUMAN_EXPLAINABLE, (NeedCategory.OBSERVABILITY, NeedCategory.GOVERNANCE), (r"\baudit\b", r"\btrace\b", r"auditid", r"traceid", r"auditoria"), "A plataforma registra ou expõe trace/audit para leitura operacional.", "garantir que trace/audit nao contenham segredo bruto", ("audit", "trace"), ), rule( "business-entitlement-signal", "Entitlement ou regra comercial detectada", SignalKind.CAPABILITY, GateDomain.BUSINESS, SignalSeverity.INFO, HumanReadinessStage.HUMAN_EXPLAINABLE, (NeedCategory.COMMERCIAL, NeedCategory.FINANCE), (r"entitlement", r"checkout", r"invoice", r"billing", r"franquia", r"cobranca"), "Ha evidencia de regra comercial, cobranca, consumo ou entitlement.", "sincronizar Business como fonte unica de plano, franquia e bloqueio", ("business", "entitlement"), ), rule( "identity-rbac-signal", "Identity/RBAC detectado", SignalKind.CAPABILITY, GateDomain.IDENTITY, SignalSeverity.INFO, HumanReadinessStage.HUMAN_EXPLAINABLE, (NeedCategory.SECURITY, NeedCategory.ADMINISTRATION, NeedCategory.GOVERNANCE), (r"\brbac\b", r"identity", r"organizacao", r"organization", r"user", r"usuario", r"tenant"), "Ha evidencia de identidade, papel, organizacao, tenant ou permissao.", "amarrar actor, organizationId, role e escopo nos contratos humanos", ("identity", "rbac"), ), rule( "wrangler-operational-signal", "Wrangler operacional detectado", SignalKind.CAPABILITY, GateDomain.CLOUD, SignalSeverity.INFO, HumanReadinessStage.TECHNICAL_READY, (NeedCategory.OPERATIONS, NeedCategory.INTEGRATION), (r"\bwrangler\b", r"workers\.dev", r"cloudflare worker"), "Ha evidencia de operacao Cloudflare por wrangler/Worker.", "usar wrangler para deploy, logs, rotas, secrets e health checks", ("wrangler", "cloudflare"), ), ) BLOCKER_RULES: tuple[SignalRule, ...] = ( rule( "repo-missing", "Repositorio real ausente", SignalKind.BLOCKER, GateDomain.REPOSITORY, SignalSeverity.CRITICAL, HumanReadinessStage.NOT_FOUND, (NeedCategory.GOVERNANCE, NeedCategory.OPERATIONS), (r"repositorio real nao encontrado", r"repo.*ausente", r"not found"), "Sem repositorio real local nao ha base material para validar a plataforma.", "criar ou clonar repositorio real sem numero da pasta gerencial", ("repository",), ), rule( "git-missing", "Git local ausente ou inacessivel", SignalKind.BLOCKER, GateDomain.REPOSITORY, SignalSeverity.HIGH, HumanReadinessStage.LOCAL_ONLY, (NeedCategory.GOVERNANCE, NeedCategory.OPERATIONS), (r"sem \.git", r"git.*ausente", r"git.*inacess", r"permission denied.*\.git", r"index\.lock"), "Sem Git operacional a rodada nao consegue registrar commit, hash e sincronizacao.", "resolver ACL de .git, configurar origin e repetir commit/push", ("git", "sync"), ), rule( "tests-missing", "Testes nao encontrados", SignalKind.GAP, GateDomain.TESTS, SignalSeverity.MEDIUM, HumanReadinessStage.TECHNICAL_READY, (NeedCategory.OPERATIONS, NeedCategory.OBSERVABILITY), (r"testes nao encontrados", r"no tests", r"sem teste"), "A varredura nao encontrou suite ou smoke detectavel.", "criar teste canonico de health/readiness/contrato humano", ("tests",), ), rule( "openapi-missing", "Contrato OpenAPI nao encontrado", SignalKind.GAP, GateDomain.CONTRACT, SignalSeverity.MEDIUM, HumanReadinessStage.TECHNICAL_READY, (NeedCategory.DOCUMENTATION, NeedCategory.INTEGRATION), (r"openapi nao encontrado", r"openapi.*missing", r"sem openapi"), "Sem contrato OpenAPI ou equivalente, a integracao fica menos auditavel.", "publicar OpenAPI minima ou declarar contrato alternativo versionado", ("openapi", "contract"), ), rule( "docs-catalog-only", "Docs catalogOnly exige decisao formal", SignalKind.BLOCKER, GateDomain.DOCS, SignalSeverity.HIGH, HumanReadinessStage.CATALOG_ONLY, (NeedCategory.DOCUMENTATION, NeedCategory.GOVERNANCE), (r"catalogonly", r"catalog_only", r"catalog-only", r"docs.*catalog"), "Docs aparece como catalogOnly; isso precisa ser excecao formal ou leitura minima responseReady.", "promover leitura canonica minima de Docs ou registrar excecao deliberada", ("docs", "catalogOnly"), ), rule( "intelligence-unsupported", "Intelligence sem promocao operacional completa", SignalKind.BLOCKER, GateDomain.GOVERNANCE, SignalSeverity.HIGH, HumanReadinessStage.CATALOG_ONLY, (NeedCategory.STRATEGY, NeedCategory.OBSERVABILITY), (r"unsupported", r"catalogonly-local-ready", r"intelligence.*planned", r"public endpoint.*missing"), "Intelligence aparece local/catalogada, mas ainda depende de endpoint, storage ou registro operacional.", "manter como catalogOnly planejada ate publicar smoke HTTP e registrar no MCP central", ("intelligence", "unsupported"), ), rule( "credential-live-pending", "Credencial live ou BYOK pendente", SignalKind.BLOCKER, GateDomain.INTEGRATION, SignalSeverity.HIGH, HumanReadinessStage.CONTROLLED_READY, (NeedCategory.INTEGRATION, NeedCategory.SECURITY, NeedCategory.COMMERCIAL), (r"token.*missing", r"credential.*not.*ready", r"needs_token", r"live.*credential", r"credencial live", r"byok.*pend"), "A integracao depende de credencial live, token ou credentialRef por tenant.", "criar sessao BYOK, gerar credentialRef, executar smoke readonly e provar nao vazamento", ("credential", "BYOK"), ), rule( "panel-source-divergence", "Painel e GPT podem divergir", SignalKind.RISK, GateDomain.PANEL, SignalSeverity.HIGH, HumanReadinessStage.PANEL_READY, (NeedCategory.EXPERIENCE, NeedCategory.GOVERNANCE), (r"samesource.*false", r"same source.*false", r"diverg", r"source.*mismatch"), "Ha indicio de divergencia entre fonte do painel e fonte explicada pelo GPT.", "reconciliar sourceEndpoint, sourceToolId, sourcePayloadHash e sourceRecordsHash", ("sameSource", "panelReady"), ), rule( "plugin-cloudflare-expected-denial", "Negativa do plugin Cloudflare nao e blocker operacional", SignalKind.EXCEPTION, GateDomain.CLOUD, SignalSeverity.INFO, HumanReadinessStage.TECHNICAL_READY, (NeedCategory.OPERATIONS, NeedCategory.INTEGRATION), (r"plugin.*cloudflare.*denied", r"cloudflare-plugin-auth-denied", r"user rejected mcp tool call"), "Falha ou negativa do plugin Cloudflare e esperada e nao deve bloquear a OS.", "registrar tentativa do plugin e seguir trabalho operacional por wrangler quando aplicavel", ("cloudflare-plugin", "expected"), ), rule( "cloudflare-binding-local-blocker", "Bindings Cloudflare ausentes no ambiente local", SignalKind.RISK, GateDomain.CLOUD, SignalSeverity.MEDIUM, HumanReadinessStage.TECHNICAL_READY, (NeedCategory.OPERATIONS, NeedCategory.INTEGRATION), (r"cloudflare-bindings", r"binding.*missing", r"bindings.*ausent", r"d1.*missing", r"kv.*missing", r"r2.*missing"), "O runtime local indica bindings ausentes; isso limita prova live, mas nao invalida evidencia local.", "validar bindings com wrangler e registrar ambiente alvo do smoke", ("cloudflare", "bindings"), ), ) PLATFORM_SPECIFIC_SIGNALS: dict[str, tuple[SignalRule, ...]] = { "docs": ( rule( "docs-canonical-read", "Leitura canonica de Docs precisa ficar explicita", SignalKind.DECISION, GateDomain.DOCS, SignalSeverity.HIGH, HumanReadinessStage.CATALOG_ONLY, (NeedCategory.DOCUMENTATION, NeedCategory.GOVERNANCE), (r"docs", r"document", r"contrato", r"canon"), "Docs precisa decidir entre leitura responseReady minima e excecao catalogOnly formal.", "criar gate Docs: responseReady minimo ou excecao documentada sem bloquear ready global", ("docs", "decision"), ), ), "integracoes": ( rule( "integracoes-byok-chain", "Jornada BYOK ponta a ponta deve ser provada", SignalKind.DECISION, GateDomain.INTEGRATION, SignalSeverity.HIGH, HumanReadinessStage.CONTROLLED_READY, (NeedCategory.INTEGRATION, NeedCategory.SECURITY, NeedCategory.COMMERCIAL), (r"byok", r"credentialref", r"cloudflare", r"gitlab", r"stripe", r"whatsapp"), "Integracoes tem base BYOK, mas precisa provar usuario, organizacao, entitlement, credentialRef e smoke.", "executar fluxo encadeado BYOK com nao vazamento e consumo auditavel", ("BYOK", "integracoes"), ), ), "business": ( rule( "business-blocker-isolation", "Business deve isolar blockers por produto", SignalKind.DECISION, GateDomain.BUSINESS, SignalSeverity.MEDIUM, HumanReadinessStage.HUMAN_EXPLAINABLE, (NeedCategory.COMMERCIAL, NeedCategory.FINANCE, NeedCategory.GOVERNANCE), (r"blocker", r"panelready", r"entitlement", r"readycontrolled", r"commercial"), "Business aparece como fonte de readiness comercial e precisa impedir contaminacao global indevida.", "validar blocker por productId, stage e impacto comercial isolado", ("business", "blocker-policy"), ), ), "compliance": ( rule( "compliance-admin-view", "Compliance deve manter admin view same-source", SignalKind.DECISION, GateDomain.COMPLIANCE, SignalSeverity.MEDIUM, HumanReadinessStage.PANEL_READY, (NeedCategory.LEGAL, NeedCategory.SECURITY, NeedCategory.GOVERNANCE), (r"compliance\.admin_view\.readiness", r"sameSource", r"panelReady", r"retention", r"policy"), "Compliance possui admin view e deve manter mesma fonte, redaction, retention e evidencia.", "validar regressao de panelReady, source hash, retention e dados redigidos", ("compliance", "admin-view"), ), ), "intelligence": ( rule( "intelligence-promotion-gates", "Intelligence precisa de gates de promocao", SignalKind.DECISION, GateDomain.GOVERNANCE, SignalSeverity.HIGH, HumanReadinessStage.CATALOG_ONLY, (NeedCategory.STRATEGY, NeedCategory.OBSERVABILITY, NeedCategory.GOVERNANCE), (r"runtimeMinimum", r"responseReadyControlled", r"catalogOnly-local-ready", r"public smoke"), "Intelligence ja descreve gates, mas precisa evidencia publica para sair de catalogOnly local.", "executar smoke publico health/profile/readiness/openapi/admin e publicar evidencia", ("intelligence", "promotion"), ), ), } def evidence_text(report: PlatformHumanReport) -> str: parts = [ report.platform.platform_id, report.platform.title, report.platform.mission, report.scan.readme_excerpt, " ".join(report.scan.warnings), ] parts.extend(evidence.summary for evidence in report.scan.evidence[:240]) parts.extend(evidence.path for evidence in report.scan.evidence[:240]) parts.extend(recommendation.title + " " + recommendation.reason for recommendation in report.recommendations[:12]) return "\n".join(parts) def refs_for_rule(report: PlatformHumanReport, rule_item: SignalRule, limit: int = 6) -> tuple[SourceReference, ...]: matched = [] for evidence in report.scan.evidence: text = f"{evidence.path} {evidence.summary} {' '.join(evidence.tags)}" if rule_item.matches(text): matched.append(evidence) refs = list(source_refs_from_evidence(matched, limit=limit)) if not refs and report.scan.warnings: for warning in report.scan.warnings: if rule_item.matches(warning): refs.append(SourceReference(path=report.scan.repo_path, summary=warning, confidence=SourceConfidence.DERIVED, role=EvidenceRole.PRIMARY)) if not refs and report.platform.known_blockers: for blocker in report.platform.known_blockers: if rule_item.matches(blocker) or rule_item.kind == SignalKind.BLOCKER: refs.append(SourceReference(path=report.scan.repo_path, summary=blocker, confidence=SourceConfidence.DECLARED, role=EvidenceRole.PRIMARY)) if not refs and rule_item.matches(report.scan.readme_excerpt): refs.append(SourceReference(path=f"{report.scan.repo_path}/README.md", summary="README contem sinal relacionado.", confidence=SourceConfidence.DERIVED)) return tuple(refs[:limit]) def signal_from_rule(report: PlatformHumanReport, rule_item: SignalRule, refs: Sequence[SourceReference] | None = None) -> OperationalSignal: refs = tuple(refs or ()) base = { "platform": report.platform.platform_id, "rule": rule_item.rule_id, "refs": [ref.reference for ref in refs], } return OperationalSignal( signal_id=f"{report.platform.platform_id}.{rule_item.rule_id}.{stable_digest(base, length=8)}", platform_id=report.platform.platform_id, kind=rule_item.kind, domain=rule_item.domain, title=rule_item.title, summary=rule_item.positive_summary, severity=rule_item.severity, stage=rule_item.stage, categories=rule_item.categories, sources=tuple(refs), tags=rule_item.tags, next_action=rule_item.next_action, ) def scan_rules(report: PlatformHumanReport, rules: Sequence[SignalRule]) -> tuple[OperationalSignal, ...]: text = evidence_text(report) signals: list[OperationalSignal] = [] for rule_item in rules: if not rule_item.matches(text): continue refs = refs_for_rule(report, rule_item) signals.append(signal_from_rule(report, rule_item, refs)) return tuple(signals) def repository_signals(report: PlatformHumanReport) -> tuple[OperationalSignal, ...]: scan = report.scan signals: list[OperationalSignal] = [] if scan.exists: signals.append( OperationalSignal( signal_id=f"{scan.platform.platform_id}.repo.exists", platform_id=scan.platform.platform_id, kind=SignalKind.CAPABILITY, domain=GateDomain.REPOSITORY, title="Repositorio local encontrado", summary="O espelho local existe e pode ser analisado.", severity=SignalSeverity.INFO, stage=HumanReadinessStage.LOCAL_ONLY, sources=(SourceReference(path=scan.repo_path, summary="Repositorio existe no filesystem.", confidence=SourceConfidence.DIRECT),), tags=("repository",), next_action="manter repositorio sincronizado e com hash rastreavel", ) ) else: signals.append( OperationalSignal( signal_id=f"{scan.platform.platform_id}.repo.missing", platform_id=scan.platform.platform_id, kind=SignalKind.BLOCKER, domain=GateDomain.REPOSITORY, title="Repositorio local ausente", summary="Nao existe espelho local para leitura ou validacao.", severity=SignalSeverity.CRITICAL, stage=HumanReadinessStage.NOT_FOUND, sources=(SourceReference(path=scan.repo_path, summary="Caminho nao encontrado.", confidence=SourceConfidence.MISSING, role=EvidenceRole.ABSENT),), tags=("repository", "missing"), next_action="criar ou clonar o repositorio real", ) ) if scan.git_present: signals.append( OperationalSignal( signal_id=f"{scan.platform.platform_id}.git.present", platform_id=scan.platform.platform_id, kind=SignalKind.CAPABILITY, domain=GateDomain.REPOSITORY, title="Git local detectado", summary="Branch, HEAD ou metadados Git foram detectados no repositorio.", severity=SignalSeverity.INFO, stage=HumanReadinessStage.TECHNICAL_READY, sources=source_refs_from_strings((scan.branch or "branch desconhecida", scan.head or "head desconhecido", scan.remote_origin or "origin nao configurado"), "Metadado Git detectado."), tags=("git",), next_action="validar git status e sincronizacao no fechamento", ) ) elif scan.exists: signals.append( OperationalSignal( signal_id=f"{scan.platform.platform_id}.git.missing", platform_id=scan.platform.platform_id, kind=SignalKind.BLOCKER, domain=GateDomain.REPOSITORY, title="Git local nao detectado", summary="Repositorio existe, mas .git nao foi detectado pela varredura.", severity=SignalSeverity.HIGH, stage=HumanReadinessStage.LOCAL_ONLY, sources=(SourceReference(path=scan.repo_path, summary="Repositorio sem .git detectavel.", confidence=SourceConfidence.MISSING),), tags=("git", "sync"), next_action="inicializar Git ou corrigir permissao local de .git", ) ) return tuple(signals) def warning_signals(report: PlatformHumanReport) -> tuple[OperationalSignal, ...]: signals: list[OperationalSignal] = [] for warning in report.scan.warnings: matched = False for rule_item in BLOCKER_RULES: if rule_item.matches(warning): refs = (SourceReference(path=report.scan.repo_path, summary=warning, confidence=SourceConfidence.DERIVED, role=EvidenceRole.PRIMARY),) signals.append(signal_from_rule(report, rule_item, refs)) matched = True if not matched: signal_id = f"{report.platform.platform_id}.warning.{slugify(warning)}.{stable_digest(warning, 6)}" signals.append( OperationalSignal( signal_id=signal_id, platform_id=report.platform.platform_id, kind=SignalKind.GAP, domain=GateDomain.GOVERNANCE, title="Warning de varredura", summary=warning, severity=SignalSeverity.MEDIUM, stage=HumanReadinessStage.TECHNICAL_READY, sources=(SourceReference(path=report.scan.repo_path, summary=warning, confidence=SourceConfidence.DERIVED),), tags=("warning",), next_action="classificar warning e registrar evidencia de resolucao ou excecao", ) ) return tuple(signals) def known_blocker_signals(report: PlatformHumanReport) -> tuple[OperationalSignal, ...]: signals: list[OperationalSignal] = [] for blocker in report.platform.known_blockers: matched_rules = [rule_item for rule_item in BLOCKER_RULES if rule_item.matches(blocker)] if not matched_rules: matched_rules = [ rule( "known-blocker", "Bloqueio conhecido catalogado", SignalKind.BLOCKER, GateDomain.GOVERNANCE, SignalSeverity.HIGH, HumanReadinessStage.BLOCKED, report.platform.primary_categories, (re.escape(blocker.lower()),), "Bloqueio conhecido precisa ser resolvido, isolado ou formalizado.", "resolver, isolar ou formalizar o bloqueio conhecido", ("known-blocker",), ) ] for rule_item in matched_rules: refs = (SourceReference(path=report.scan.repo_path, summary=blocker, confidence=SourceConfidence.DECLARED, role=EvidenceRole.PRIMARY),) signals.append(signal_from_rule(report, rule_item, refs)) return tuple(signals) def score_signals(report: PlatformHumanReport) -> tuple[OperationalSignal, ...]: score = report.average_score if score >= 90: return ( OperationalSignal( signal_id=f"{report.platform.platform_id}.score.high", platform_id=report.platform.platform_id, kind=SignalKind.CAPABILITY, domain=GateDomain.GOVERNANCE, title="Score humano alto", summary=f"Score medio humano {score}; a plataforma tem boa cobertura por perfil.", severity=SignalSeverity.INFO, stage=HumanReadinessStage.HUMAN_EXPLAINABLE, tags=("score",), next_action="trocar score isolado por gates com blockers formais e evidencia viva", ), ) if score >= 70: severity = SignalSeverity.LOW stage = HumanReadinessStage.HUMAN_EXPLAINABLE elif score >= 50: severity = SignalSeverity.MEDIUM stage = HumanReadinessStage.TECHNICAL_READY else: severity = SignalSeverity.HIGH stage = HumanReadinessStage.PLANNED weakest = sorted(report.cells, key=lambda item: item.score)[:4] evidence = tuple( SourceReference(path=report.scan.repo_path, summary=f"{cell.profile_id}:{cell.score}", confidence=SourceConfidence.DERIVED) for cell in weakest ) return ( OperationalSignal( signal_id=f"{report.platform.platform_id}.score.attention", platform_id=report.platform.platform_id, kind=SignalKind.GAP, domain=GateDomain.GOVERNANCE, title="Score humano exige melhoria", summary=f"Score medio humano {score}; perfis mais frageis precisam de OS direcionada.", severity=severity, stage=stage, sources=evidence, tags=("score", "matrix"), next_action="priorizar perfis de menor score em tela, relatorio ou comando humano", ), ) def recommendation_signals(report: PlatformHumanReport, recommendations: Sequence[Recommendation]) -> tuple[OperationalSignal, ...]: signals: list[OperationalSignal] = [] for rec in recommendations: if rec.platform_id != report.platform.platform_id: continue severity = SignalSeverity.HIGH if rec.priority >= 85 else SignalSeverity.MEDIUM if rec.priority >= 65 else SignalSeverity.LOW kind = SignalKind.BLOCKER if rec.priority >= 85 else SignalKind.DECISION domain = domain_from_categories(rec.categories) refs = source_refs_from_strings(rec.affected_paths or (report.scan.repo_path,), rec.reason, confidence=SourceConfidence.DECLARED) signals.append( OperationalSignal( signal_id=f"{rec.recommendation_id}.{stable_digest(rec.reason, 6)}", platform_id=report.platform.platform_id, kind=kind, domain=domain, title=rec.title, summary=rec.reason, severity=severity, stage=HumanReadinessStage.HUMAN_EXPLAINABLE, categories=rec.categories, sources=refs, tags=("recommendation", rec.suggested_order_type.value), next_action=rec.expected_impact, ) ) return tuple(signals) def domain_from_categories(categories: Sequence[NeedCategory]) -> GateDomain: priority = { NeedCategory.SECURITY: GateDomain.SECURITY, NeedCategory.INTEGRATION: GateDomain.INTEGRATION, NeedCategory.COMMERCIAL: GateDomain.BUSINESS, NeedCategory.FINANCE: GateDomain.BUSINESS, NeedCategory.LEGAL: GateDomain.COMPLIANCE, NeedCategory.DOCUMENTATION: GateDomain.DOCS, NeedCategory.EXPERIENCE: GateDomain.PANEL, NeedCategory.OBSERVABILITY: GateDomain.OBSERVABILITY, NeedCategory.GOVERNANCE: GateDomain.GOVERNANCE, NeedCategory.OPERATIONS: GateDomain.RUNTIME, } for category in categories: if category in priority: return priority[category] return GateDomain.GOVERNANCE def dedupe_signals(signals: Iterable[OperationalSignal]) -> tuple[OperationalSignal, ...]: seen: set[tuple[str, str, str]] = set() output: list[OperationalSignal] = [] for signal in signals: key = (signal.platform_id, signal.title.lower(), signal.domain.value) if key in seen: continue seen.add(key) output.append(signal) output.sort(key=lambda item: (item.platform_id, -severity_to_sort(item.severity), item.domain.value, item.title)) return tuple(output) def severity_to_sort(severity: SignalSeverity) -> int: return { SignalSeverity.CRITICAL: 5, SignalSeverity.HIGH: 4, SignalSeverity.MEDIUM: 3, SignalSeverity.LOW: 2, SignalSeverity.INFO: 1, }.get(severity, 0) def build_operational_signals(report: PlatformHumanReport, recommendations: Sequence[Recommendation] = ()) -> tuple[OperationalSignal, ...]: signals: list[OperationalSignal] = [] signals.extend(repository_signals(report)) signals.extend(scan_rules(report, CAPABILITY_RULES)) signals.extend(scan_rules(report, BLOCKER_RULES)) signals.extend(scan_rules(report, PLATFORM_SPECIFIC_SIGNALS.get(report.platform.platform_id, ()))) signals.extend(warning_signals(report)) signals.extend(known_blocker_signals(report)) signals.extend(score_signals(report)) signals.extend(recommendation_signals(report, recommendations)) return dedupe_signals(signals) def summarize_blockers(signals: Sequence[OperationalSignal], limit: int = 12) -> tuple[str, ...]: blockers = [signal for signal in signals if signal.is_blocking] blockers.sort(key=lambda item: (-severity_to_sort(item.severity), item.platform_id, item.title)) return merge_unique(f"{signal.platform_id}: {signal.title} - {signal.next_action}" for signal in blockers[:limit]) def summarize_capabilities(signals: Sequence[OperationalSignal], limit: int = 12) -> tuple[str, ...]: caps = [signal for signal in signals if signal.kind == SignalKind.CAPABILITY] caps.sort(key=lambda item: (item.platform_id, item.domain.value, item.title)) return merge_unique(f"{signal.platform_id}: {signal.title}" for signal in caps[:limit]) def scan_repository_for_order_text(repo_path: Path, patterns: Sequence[str], max_files: int = 80) -> tuple[SourceReference, ...]: """Search text files for specific order-related patterns. The function is intentionally small and safe: it skips known build/vendor directories, reads only bounded text files, and returns references rather than raw content. """ skip = {".git", "node_modules", "dist", "build", "coverage", "__pycache__", ".wrangler", "vendor"} extensions = {".md", ".ts", ".tsx", ".js", ".mjs", ".cjs", ".py", ".json", ".toml", ".yml", ".yaml"} refs: list[SourceReference] = [] if not repo_path.exists(): return ( SourceReference(path=str(repo_path), summary="Repositorio nao encontrado para busca de texto.", confidence=SourceConfidence.MISSING, role=EvidenceRole.ABSENT), ) stack = [repo_path] while stack and len(refs) < max_files: current = stack.pop() try: entries = sorted(current.iterdir(), key=lambda item: item.name.lower()) except OSError: continue for entry in entries: if entry.is_dir(): if entry.name not in skip: stack.append(entry) continue if entry.suffix.lower() not in extensions: continue try: if entry.stat().st_size > 240_000: continue lines = entry.read_text(encoding="utf-8", errors="ignore").splitlines() except OSError: continue rel = str(entry.relative_to(repo_path)).replace("\\", "/") for index, line in enumerate(lines, start=1): lowered = line.lower() if any(re.search(pattern, lowered, re.I) for pattern in patterns): refs.append( SourceReference( path=rel, line=index, summary="Trecho local referencia tema da ordem sem expor conteudo bruto.", confidence=SourceConfidence.DIRECT, role=EvidenceRole.SUPPORTING, ) ) break if len(refs) >= max_files: break return tuple(refs)