feat: fundar plataforma mais humana
This commit is contained in:
526
src/mais_humana/models.py
Normal file
526
src/mais_humana/models.py
Normal file
@@ -0,0 +1,526 @@
|
||||
"""Core models for human-centered ecosystem analysis.
|
||||
|
||||
The module intentionally keeps the data model dependency-free. The platform is
|
||||
expected to run inside operational mirrors where installing new packages is not
|
||||
always desirable, so every object can be serialized with the standard library.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field, fields, is_dataclass
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Mapping, MutableMapping, Sequence
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
"""Return an ISO-8601 timestamp without relying on local locale settings."""
|
||||
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
||||
|
||||
|
||||
def slugify(value: str) -> str:
|
||||
"""Create a stable ASCII-ish slug for filenames and object ids."""
|
||||
|
||||
allowed: list[str] = []
|
||||
last_dash = False
|
||||
for char in value.lower().strip():
|
||||
if "a" <= char <= "z" or "0" <= char <= "9":
|
||||
allowed.append(char)
|
||||
last_dash = False
|
||||
elif char in {" ", "_", "-", ".", "/", "\\"} and not last_dash:
|
||||
allowed.append("-")
|
||||
last_dash = True
|
||||
return "".join(allowed).strip("-") or "item"
|
||||
|
||||
|
||||
def as_plain_data(value: Any) -> Any:
|
||||
"""Convert dataclasses, enums, paths, and nested values to JSON-safe data."""
|
||||
|
||||
if isinstance(value, Enum):
|
||||
return value.value
|
||||
if isinstance(value, Path):
|
||||
return str(value)
|
||||
if is_dataclass(value):
|
||||
return {item.name: as_plain_data(getattr(value, item.name)) for item in fields(value)}
|
||||
if isinstance(value, Mapping):
|
||||
return {str(key): as_plain_data(inner) for key, inner in value.items()}
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
return [as_plain_data(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
class EvidenceKind(str, Enum):
|
||||
"""Kinds of evidence detected in a platform repository."""
|
||||
|
||||
README = "readme"
|
||||
PACKAGE_SCRIPT = "package_script"
|
||||
ROUTE = "route"
|
||||
OPENAPI = "openapi"
|
||||
TEST = "test"
|
||||
CONFIG = "config"
|
||||
DOC = "doc"
|
||||
WORKER = "worker"
|
||||
STORAGE = "storage"
|
||||
MCP_TOOL = "mcp_tool"
|
||||
UI_SURFACE = "ui_surface"
|
||||
SECURITY = "security"
|
||||
BUSINESS_RULE = "business_rule"
|
||||
OBSERVABILITY = "observability"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class NeedCategory(str, Enum):
|
||||
"""Human need categories used by the matrix engine."""
|
||||
|
||||
ADMINISTRATION = "administration"
|
||||
SUPPORT = "support"
|
||||
FINANCE = "finance"
|
||||
LEGAL = "legal"
|
||||
SECURITY = "security"
|
||||
OPERATIONS = "operations"
|
||||
STRATEGY = "strategy"
|
||||
DOCUMENTATION = "documentation"
|
||||
SELF_SERVICE = "self_service"
|
||||
COMMERCIAL = "commercial"
|
||||
EXPERIENCE = "experience"
|
||||
GOVERNANCE = "governance"
|
||||
INTEGRATION = "integration"
|
||||
OBSERVABILITY = "observability"
|
||||
|
||||
|
||||
class MaturityLevel(str, Enum):
|
||||
"""Operational maturity level from a human point of view."""
|
||||
|
||||
NOT_FOUND = "not_found"
|
||||
PLANNED = "planned"
|
||||
CATALOGED = "cataloged"
|
||||
TECHNICAL = "technical"
|
||||
EXPLAINABLE = "explainable"
|
||||
ACTIONABLE = "actionable"
|
||||
READY_FOR_HUMAN = "ready_for_human"
|
||||
AUDITABLE = "auditable"
|
||||
|
||||
|
||||
class OrderType(str, Enum):
|
||||
"""Service-order type used by the order generator."""
|
||||
|
||||
EXECUTIVE = "executiva"
|
||||
MANAGERIAL = "gerencial"
|
||||
|
||||
|
||||
class OrderStatus(str, Enum):
|
||||
"""Compact status for generated and executed orders."""
|
||||
|
||||
PLANNED = "planejada"
|
||||
RUNNING = "em_execucao"
|
||||
COMPLETED = "concluida"
|
||||
PARTIAL = "parcial"
|
||||
BLOCKED = "bloqueada"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Evidence:
|
||||
"""A small, inspectable proof that a capability exists or is missing."""
|
||||
|
||||
kind: EvidenceKind
|
||||
path: str
|
||||
summary: str
|
||||
line: int | None = None
|
||||
confidence: float = 0.5
|
||||
tags: tuple[str, ...] = ()
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
@property
|
||||
def reference(self) -> str:
|
||||
if self.line is None:
|
||||
return self.path
|
||||
return f"{self.path}:{self.line}"
|
||||
|
||||
def is_strong(self) -> bool:
|
||||
return self.confidence >= 0.75
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class HumanNeed:
|
||||
"""A concrete need a person may have while using the ecosystem."""
|
||||
|
||||
need_id: str
|
||||
title: str
|
||||
category: NeedCategory
|
||||
description: str
|
||||
success_markers: tuple[str, ...]
|
||||
risk_if_missing: str
|
||||
expected_surfaces: tuple[str, ...] = ()
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class HumanProfile:
|
||||
"""A human role that should be served by one or more platforms."""
|
||||
|
||||
profile_id: str
|
||||
name: str
|
||||
description: str
|
||||
priority_needs: tuple[NeedCategory, ...]
|
||||
typical_questions: tuple[str, ...]
|
||||
expected_outputs: tuple[str, ...]
|
||||
sensitive_concerns: tuple[str, ...] = ()
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
def wants(self, category: NeedCategory) -> bool:
|
||||
return category in self.priority_needs
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PlatformDefinition:
|
||||
"""Canonical description of a managed platform."""
|
||||
|
||||
platform_id: str
|
||||
repo_name: str
|
||||
central_folder: str
|
||||
title: str
|
||||
mission: str
|
||||
primary_categories: tuple[NeedCategory, ...]
|
||||
expected_profiles: tuple[str, ...]
|
||||
related_platforms: tuple[str, ...] = ()
|
||||
expected_surfaces: tuple[str, ...] = ()
|
||||
known_blockers: tuple[str, ...] = ()
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class FileMetric:
|
||||
"""Line and extension metric for a repository file."""
|
||||
|
||||
path: str
|
||||
extension: str
|
||||
lines: int
|
||||
bytes_size: int
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ScriptCommand:
|
||||
"""A command exposed by package.json or other local metadata."""
|
||||
|
||||
name: str
|
||||
command: str
|
||||
source_file: str
|
||||
intent: str = "unknown"
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PlatformScan:
|
||||
"""Repository scan result used as input for matrix and report generation."""
|
||||
|
||||
platform: PlatformDefinition
|
||||
repo_path: str
|
||||
exists: bool
|
||||
git_present: bool
|
||||
branch: str | None
|
||||
head: str | None
|
||||
remote_origin: str | None
|
||||
readme_excerpt: str
|
||||
file_metrics: tuple[FileMetric, ...]
|
||||
scripts: tuple[ScriptCommand, ...]
|
||||
evidence: tuple[Evidence, ...]
|
||||
warnings: tuple[str, ...]
|
||||
scanned_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
@property
|
||||
def total_lines(self) -> int:
|
||||
return sum(metric.lines for metric in self.file_metrics)
|
||||
|
||||
@property
|
||||
def code_lines(self) -> int:
|
||||
code_ext = {".ts", ".tsx", ".js", ".mjs", ".cjs", ".py", ".java"}
|
||||
return sum(metric.lines for metric in self.file_metrics if metric.extension in code_ext)
|
||||
|
||||
@property
|
||||
def has_tests(self) -> bool:
|
||||
return any(item.kind == EvidenceKind.TEST for item in self.evidence)
|
||||
|
||||
@property
|
||||
def has_openapi(self) -> bool:
|
||||
return any(item.kind == EvidenceKind.OPENAPI for item in self.evidence)
|
||||
|
||||
@property
|
||||
def has_worker(self) -> bool:
|
||||
return any(item.kind == EvidenceKind.WORKER for item in self.evidence)
|
||||
|
||||
@property
|
||||
def strong_evidence_count(self) -> int:
|
||||
return sum(1 for item in self.evidence if item.is_strong())
|
||||
|
||||
def evidence_by_kind(self, kind: EvidenceKind) -> tuple[Evidence, ...]:
|
||||
return tuple(item for item in self.evidence if item.kind == kind)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class MatrixCell:
|
||||
"""Human service score for one platform and one profile."""
|
||||
|
||||
platform_id: str
|
||||
profile_id: str
|
||||
score: int
|
||||
maturity: MaturityLevel
|
||||
explanation: str
|
||||
strengths: tuple[str, ...]
|
||||
gaps: tuple[str, ...]
|
||||
evidence_refs: tuple[str, ...]
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
@property
|
||||
def normalized_score(self) -> float:
|
||||
return max(0.0, min(1.0, self.score / 100.0))
|
||||
|
||||
def is_ready(self) -> bool:
|
||||
return self.score >= 75
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Recommendation:
|
||||
"""Actionable recommendation derived from scan and matrix signals."""
|
||||
|
||||
recommendation_id: str
|
||||
platform_id: str
|
||||
title: str
|
||||
reason: str
|
||||
expected_impact: str
|
||||
categories: tuple[NeedCategory, ...]
|
||||
priority: int
|
||||
suggested_order_type: OrderType
|
||||
affected_paths: tuple[str, ...] = ()
|
||||
validation_steps: tuple[str, ...] = ()
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PlatformHumanReport:
|
||||
"""Report model for one platform."""
|
||||
|
||||
platform: PlatformDefinition
|
||||
scan: PlatformScan
|
||||
cells: tuple[MatrixCell, ...]
|
||||
recommendations: tuple[Recommendation, ...]
|
||||
summary: str
|
||||
current_state: tuple[str, ...]
|
||||
future_state: tuple[str, ...]
|
||||
missing_for_humans: tuple[str, ...]
|
||||
generated_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
@property
|
||||
def average_score(self) -> int:
|
||||
if not self.cells:
|
||||
return 0
|
||||
return round(sum(cell.score for cell in self.cells) / len(self.cells))
|
||||
|
||||
@property
|
||||
def ready_profiles(self) -> tuple[str, ...]:
|
||||
return tuple(cell.profile_id for cell in self.cells if cell.is_ready())
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class EcosystemHumanReport:
|
||||
"""Full ecosystem report model."""
|
||||
|
||||
scans: tuple[PlatformScan, ...]
|
||||
platform_reports: tuple[PlatformHumanReport, ...]
|
||||
recommendations: tuple[Recommendation, ...]
|
||||
generated_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
@property
|
||||
def total_code_lines(self) -> int:
|
||||
return sum(scan.code_lines for scan in self.scans)
|
||||
|
||||
@property
|
||||
def total_files(self) -> int:
|
||||
return sum(len(scan.file_metrics) for scan in self.scans)
|
||||
|
||||
@property
|
||||
def average_score(self) -> int:
|
||||
cells = [cell for report in self.platform_reports for cell in report.cells]
|
||||
if not cells:
|
||||
return 0
|
||||
return round(sum(cell.score for cell in cells) / len(cells))
|
||||
|
||||
def report_for(self, platform_id: str) -> PlatformHumanReport | None:
|
||||
for report in self.platform_reports:
|
||||
if report.platform.platform_id == platform_id:
|
||||
return report
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ServiceOrder:
|
||||
"""Generated service order metadata and body."""
|
||||
|
||||
order_id: str
|
||||
order_type: OrderType
|
||||
project_id: str
|
||||
title: str
|
||||
purpose: str
|
||||
object_scope: str
|
||||
reason: str
|
||||
expected_result: str
|
||||
affected_paths: tuple[str, ...]
|
||||
validations: tuple[str, ...]
|
||||
ready_criteria: tuple[str, ...]
|
||||
status: OrderStatus = OrderStatus.PLANNED
|
||||
priority: str = "media"
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GeneratedFile:
|
||||
"""File generated or updated by the platform runtime."""
|
||||
|
||||
path: str
|
||||
description: str
|
||||
function: str
|
||||
file_type: str
|
||||
changed_by: str
|
||||
change_summary: str
|
||||
relation_to_order: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ReportBundle:
|
||||
"""Paths and counters produced by a generation run."""
|
||||
|
||||
output_root: str
|
||||
generated_files: tuple[GeneratedFile, ...]
|
||||
platform_count: int
|
||||
profile_count: int
|
||||
matrix_cells: int
|
||||
total_code_lines_analyzed: int
|
||||
warnings: tuple[str, ...]
|
||||
generated_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return as_plain_data(self)
|
||||
|
||||
|
||||
def clamp_score(value: int | float) -> int:
|
||||
"""Clamp a numeric score into the 0..100 range."""
|
||||
|
||||
return int(max(0, min(100, round(float(value)))))
|
||||
|
||||
|
||||
def maturity_from_score(score: int) -> MaturityLevel:
|
||||
"""Map numeric score to a human maturity label."""
|
||||
|
||||
score = clamp_score(score)
|
||||
if score == 0:
|
||||
return MaturityLevel.NOT_FOUND
|
||||
if score < 20:
|
||||
return MaturityLevel.PLANNED
|
||||
if score < 35:
|
||||
return MaturityLevel.CATALOGED
|
||||
if score < 50:
|
||||
return MaturityLevel.TECHNICAL
|
||||
if score < 65:
|
||||
return MaturityLevel.EXPLAINABLE
|
||||
if score < 80:
|
||||
return MaturityLevel.ACTIONABLE
|
||||
if score < 92:
|
||||
return MaturityLevel.READY_FOR_HUMAN
|
||||
return MaturityLevel.AUDITABLE
|
||||
|
||||
|
||||
def merge_unique(values: Iterable[str]) -> tuple[str, ...]:
|
||||
"""Return values in input order while dropping empty strings and duplicates."""
|
||||
|
||||
seen: set[str] = set()
|
||||
output: list[str] = []
|
||||
for value in values:
|
||||
cleaned = str(value).strip()
|
||||
if not cleaned or cleaned in seen:
|
||||
continue
|
||||
seen.add(cleaned)
|
||||
output.append(cleaned)
|
||||
return tuple(output)
|
||||
|
||||
|
||||
def group_by_platform(recommendations: Sequence[Recommendation]) -> dict[str, list[Recommendation]]:
|
||||
grouped: dict[str, list[Recommendation]] = {}
|
||||
for item in recommendations:
|
||||
grouped.setdefault(item.platform_id, []).append(item)
|
||||
for items in grouped.values():
|
||||
items.sort(key=lambda rec: (-rec.priority, rec.title))
|
||||
return grouped
|
||||
|
||||
|
||||
def incrementing_id(prefix: str, index: int, title: str) -> str:
|
||||
"""Create a readable id with a numeric prefix and a slug."""
|
||||
|
||||
return f"{index:04d}_{prefix}__{slugify(title)}"
|
||||
|
||||
|
||||
def summarize_warnings(scans: Sequence[PlatformScan]) -> tuple[str, ...]:
|
||||
warnings: list[str] = []
|
||||
for scan in scans:
|
||||
for warning in scan.warnings:
|
||||
warnings.append(f"{scan.platform.platform_id}: {warning}")
|
||||
return merge_unique(warnings)
|
||||
|
||||
|
||||
def score_label(score: int) -> str:
|
||||
"""Return a compact label for user-facing matrix tables."""
|
||||
|
||||
score = clamp_score(score)
|
||||
if score >= 90:
|
||||
return "excelente"
|
||||
if score >= 75:
|
||||
return "forte"
|
||||
if score >= 60:
|
||||
return "util"
|
||||
if score >= 40:
|
||||
return "tecnico"
|
||||
if score >= 20:
|
||||
return "inicial"
|
||||
if score > 0:
|
||||
return "fragil"
|
||||
return "ausente"
|
||||
|
||||
|
||||
def ensure_mapping(data: MutableMapping[str, Any], key: str, default: Any) -> Any:
|
||||
"""Small helper used by storage migration code."""
|
||||
|
||||
if key not in data:
|
||||
data[key] = default
|
||||
return data[key]
|
||||
|
||||
Reference in New Issue
Block a user