feat(admin): concluir pipeline governada de tools na fase 6

feat/self-evolving-tools-foundation
parent 640e422498
commit de455b8566

@ -11,10 +11,13 @@ from admin_app.api.schemas import (
AdminToolDraftIntakeResponse,
AdminToolDraftListResponse,
AdminToolGenerationPipelineResponse,
AdminToolGovernanceDecisionRequest,
AdminToolGovernanceTransitionResponse,
AdminToolManagementActionResponse,
AdminToolOverviewResponse,
AdminToolPublicationListResponse,
AdminToolReviewDecisionRequest,
AdminToolReviewDetailResponse,
AdminToolReviewQueueResponse,
)
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
@ -171,12 +174,32 @@ def panel_tool_review_queue(
)
@router.get(
"/review-queue/{version_id}",
response_model=AdminToolReviewDetailResponse,
)
def panel_tool_review_queue_detail(
version_id: str,
service: ToolManagementService = Depends(get_tool_management_service),
_current_staff: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.build_review_detail_payload(version_id)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
return _build_review_detail_response(payload)
@router.post(
"/review-queue/{version_id}/review",
response_model=AdminToolGovernanceTransitionResponse,
)
def panel_tool_review_queue_review(
version_id: str,
decision: AdminToolReviewDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
@ -188,6 +211,8 @@ def panel_tool_review_queue_review(
reviewer_staff_account_id=current_staff.id,
reviewer_name=current_staff.display_name,
reviewer_role=current_staff.role,
decision_notes=decision.decision_notes,
reviewed_generated_code=decision.reviewed_generated_code,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
@ -205,6 +230,7 @@ def panel_tool_review_queue_review(
)
def panel_tool_review_queue_approve(
version_id: str,
decision: AdminToolReviewDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
@ -216,6 +242,7 @@ def panel_tool_review_queue_approve(
approver_staff_account_id=current_staff.id,
approver_name=current_staff.display_name,
approver_role=current_staff.role,
decision_notes=decision.decision_notes,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
@ -274,6 +301,66 @@ def panel_tool_publications_publish(
return _build_governance_transition_response(payload)
@router.post(
"/publications/{version_id}/deactivate",
response_model=AdminToolGovernanceTransitionResponse,
)
def panel_tool_publications_deactivate(
version_id: str,
decision: AdminToolGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.PUBLISH_TOOLS)
),
):
try:
payload = service.deactivate_version(
version_id,
actor_staff_account_id=current_staff.id,
actor_name=current_staff.display_name,
actor_role=current_staff.role,
decision_notes=decision.decision_notes,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
return _build_governance_transition_response(payload)
@router.post(
"/publications/{version_id}/rollback",
response_model=AdminToolGovernanceTransitionResponse,
)
def panel_tool_publications_rollback(
version_id: str,
decision: AdminToolGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.PUBLISH_TOOLS)
),
):
try:
payload = service.rollback_version(
version_id,
actor_staff_account_id=current_staff.id,
actor_name=current_staff.display_name,
actor_role=current_staff.role,
decision_notes=decision.decision_notes,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
return _build_governance_transition_response(payload)
def _build_pipeline_response(payload: dict) -> AdminToolGenerationPipelineResponse:
return AdminToolGenerationPipelineResponse(
@ -305,6 +392,32 @@ def _build_governance_transition_response(payload: dict) -> AdminToolGovernanceT
)
def _build_review_detail_response(payload: dict) -> AdminToolReviewDetailResponse:
return AdminToolReviewDetailResponse(
service="orquestrador-admin",
version_id=payload["version_id"],
tool_name=payload["tool_name"],
display_name=payload["display_name"],
domain=payload["domain"],
version_number=payload["version_number"],
status=payload["status"],
summary=payload["summary"],
description=payload["description"],
business_goal=payload["business_goal"],
owner_name=payload["owner_name"],
parameters=payload["parameters"],
queue_entry=payload["queue_entry"],
automated_validations=payload["automated_validations"],
automated_validation_summary=payload["automated_validation_summary"],
generated_module=payload["generated_module"],
generated_callable=payload["generated_callable"],
generated_source_code=payload["generated_source_code"],
human_gate=payload["human_gate"],
decision_history=payload["decision_history"],
next_steps=payload["next_steps"],
)
def _build_panel_actions(
settings: AdminSettings,
current_role: StaffRole | str | None = None,

@ -11,10 +11,13 @@ from admin_app.api.schemas import (
AdminToolDraftIntakeResponse,
AdminToolDraftListResponse,
AdminToolGenerationPipelineResponse,
AdminToolGovernanceDecisionRequest,
AdminToolGovernanceTransitionResponse,
AdminToolManagementActionResponse,
AdminToolOverviewResponse,
AdminToolPublicationListResponse,
AdminToolReviewDecisionRequest,
AdminToolReviewDetailResponse,
AdminToolReviewQueueResponse,
)
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
@ -171,12 +174,32 @@ def tool_review_queue(
)
@router.get(
"/review-queue/{version_id}",
response_model=AdminToolReviewDetailResponse,
)
def tool_review_queue_detail(
version_id: str,
service: ToolManagementService = Depends(get_tool_management_service),
_current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.build_review_detail_payload(version_id)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
return _build_review_detail_response(payload)
@router.post(
"/review-queue/{version_id}/review",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_review_queue_review(
version_id: str,
decision: AdminToolReviewDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
@ -188,6 +211,8 @@ def tool_review_queue_review(
reviewer_staff_account_id=current_staff.id,
reviewer_name=current_staff.display_name,
reviewer_role=current_staff.role,
decision_notes=decision.decision_notes,
reviewed_generated_code=decision.reviewed_generated_code,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
@ -205,6 +230,7 @@ def tool_review_queue_review(
)
def tool_review_queue_approve(
version_id: str,
decision: AdminToolReviewDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
@ -216,6 +242,7 @@ def tool_review_queue_approve(
approver_staff_account_id=current_staff.id,
approver_name=current_staff.display_name,
approver_role=current_staff.role,
decision_notes=decision.decision_notes,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
@ -274,6 +301,66 @@ def tool_publications_publish(
return _build_governance_transition_response(payload)
@router.post(
"/publications/{version_id}/deactivate",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_publications_deactivate(
version_id: str,
decision: AdminToolGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.PUBLISH_TOOLS)
),
):
try:
payload = service.deactivate_version(
version_id,
actor_staff_account_id=current_staff.id,
actor_name=current_staff.display_name,
actor_role=current_staff.role,
decision_notes=decision.decision_notes,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
return _build_governance_transition_response(payload)
@router.post(
"/publications/{version_id}/rollback",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_publications_rollback(
version_id: str,
decision: AdminToolGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.PUBLISH_TOOLS)
),
):
try:
payload = service.rollback_version(
version_id,
actor_staff_account_id=current_staff.id,
actor_name=current_staff.display_name,
actor_role=current_staff.role,
decision_notes=decision.decision_notes,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
return _build_governance_transition_response(payload)
def _build_pipeline_response(payload: dict) -> AdminToolGenerationPipelineResponse:
return AdminToolGenerationPipelineResponse(
@ -305,6 +392,32 @@ def _build_governance_transition_response(payload: dict) -> AdminToolGovernanceT
)
def _build_review_detail_response(payload: dict) -> AdminToolReviewDetailResponse:
return AdminToolReviewDetailResponse(
service="orquestrador-admin",
version_id=payload["version_id"],
tool_name=payload["tool_name"],
display_name=payload["display_name"],
domain=payload["domain"],
version_number=payload["version_number"],
status=payload["status"],
summary=payload["summary"],
description=payload["description"],
business_goal=payload["business_goal"],
owner_name=payload["owner_name"],
parameters=payload["parameters"],
queue_entry=payload["queue_entry"],
automated_validations=payload["automated_validations"],
automated_validation_summary=payload["automated_validation_summary"],
generated_module=payload["generated_module"],
generated_callable=payload["generated_callable"],
generated_source_code=payload["generated_source_code"],
human_gate=payload["human_gate"],
decision_history=payload["decision_history"],
next_steps=payload["next_steps"],
)
def _build_actions(
settings: AdminSettings,
current_role: StaffRole | str | None = None,

@ -776,6 +776,75 @@ class AdminToolReviewQueueResponse(BaseModel):
supported_statuses: list[ToolLifecycleStatus]
class AdminToolReviewDecisionRequest(BaseModel):
decision_notes: str = Field(min_length=12, max_length=2000)
reviewed_generated_code: bool = False
@field_validator("decision_notes")
@classmethod
def normalize_decision_notes(cls, value: str) -> str:
return value.strip()
class AdminToolGovernanceDecisionRequest(BaseModel):
decision_notes: str = Field(min_length=12, max_length=2000)
@field_validator("decision_notes")
@classmethod
def normalize_decision_notes(cls, value: str) -> str:
return value.strip()
class AdminToolReviewHumanGateResponse(BaseModel):
current_gate: str
review_action_available: bool
approval_action_available: bool
publication_action_available: bool
deactivation_action_available: bool
rollback_action_available: bool
rollback_target_version_id: str | None = None
rollback_target_version_number: int | None = None
requires_decision_notes: bool
requires_code_review_confirmation: bool
class AdminToolReviewHistoryEntryResponse(BaseModel):
action_key: str
label: str
summary: str
previous_status: str | None = None
current_status: str | None = None
actor_name: str | None = None
actor_role: str | None = None
decision_notes: str | None = None
reviewed_generated_code: bool | None = None
recorded_at: datetime | None = None
class AdminToolReviewDetailResponse(BaseModel):
service: str
version_id: str
tool_name: str
display_name: str
domain: str
version_number: int = Field(ge=1)
status: ToolLifecycleStatus
summary: str
description: str
business_goal: str
owner_name: str | None = None
parameters: list["AdminToolPublicationParameterResponse"] = Field(default_factory=list)
queue_entry: AdminToolReviewQueueEntryResponse
automated_validations: list["AdminToolAutomatedValidationResponse"] = Field(default_factory=list)
automated_validation_summary: str | None = None
generated_module: str
generated_callable: str
generated_source_code: str
human_gate: AdminToolReviewHumanGateResponse
decision_history: list[AdminToolReviewHistoryEntryResponse] = Field(default_factory=list)
next_steps: list[str] = Field(default_factory=list)
class AdminToolPublicationParameterResponse(BaseModel):
name: str
parameter_type: ToolParameterType
@ -791,6 +860,7 @@ class AdminToolPublicationSummaryResponse(BaseModel):
domain: str
version: int
status: ToolLifecycleStatus
version_id: str | None = None
parameter_count: int
parameters: list[AdminToolPublicationParameterResponse] = Field(default_factory=list)
author_name: str | None = None
@ -798,6 +868,10 @@ class AdminToolPublicationSummaryResponse(BaseModel):
implementation_callable: str
published_by: str | None = None
published_at: datetime | None = None
deactivation_action_available: bool = False
rollback_action_available: bool = False
rollback_target_version_id: str | None = None
rollback_target_version_number: int | None = None
class AdminToolPublicationListResponse(BaseModel):

@ -21,6 +21,8 @@ class ToolArtifactKind(str, Enum):
DIRECTOR_REVIEW = "director_review"
DIRECTOR_APPROVAL = "director_approval"
PUBLICATION_RELEASE = "publication_release"
PUBLICATION_DEACTIVATION = "publication_deactivation"
PUBLICATION_ROLLBACK = "publication_rollback"
class ToolArtifactStorageKind(str, Enum):

@ -97,6 +97,7 @@ _REVIEW_QUEUE_STATUSES = (
ToolLifecycleStatus.APPROVED,
ToolLifecycleStatus.FAILED,
)
_HUMAN_DECISION_NOTES_MIN_LENGTH = 12
class ToolManagementService:
@ -356,6 +357,63 @@ class ToolManagementService:
"supported_statuses": list(_REVIEW_QUEUE_STATUSES),
}
def build_review_detail_payload(self, version_id: str) -> dict:
if (
self.draft_repository is None
or self.version_repository is None
or self.metadata_repository is None
):
raise RuntimeError(
"Fluxo de governanca de tools ainda nao esta completamente conectado ao armazenamento administrativo."
)
normalized_version_id = str(version_id or "").strip().lower()
version = self.version_repository.get_by_version_id(normalized_version_id)
if version is None:
raise LookupError("Versao administrativa nao encontrada.")
draft = self.draft_repository.get_by_tool_name(version.tool_name)
if draft is None:
raise RuntimeError("Draft raiz da tool nao encontrado para a versao governada.")
metadata = self.metadata_repository.get_by_tool_version_id(version.id)
if metadata is None:
raise RuntimeError("Metadados persistidos da versao nao encontrados para a governanca administrativa.")
validation_payload = {}
if self.artifact_repository is not None:
validation_artifact = self.artifact_repository.get_by_tool_version_and_kind(
version.id,
ToolArtifactKind.VALIDATION_REPORT,
)
if validation_artifact is not None:
validation_payload = dict(validation_artifact.payload_json or {})
automated_validation = self._extract_latest_automated_validation(version.id)
generated_source_code = str(validation_payload.get("generated_source_code") or "").strip()
return {
"version_id": version.version_id,
"tool_name": version.tool_name,
"display_name": metadata.display_name,
"domain": metadata.domain,
"version_number": version.version_number,
"status": version.status,
"summary": version.summary,
"description": metadata.description,
"business_goal": version.business_goal,
"owner_name": version.owner_display_name,
"parameters": self._serialize_parameters_for_response(metadata.parameters_json),
"queue_entry": self._serialize_review_queue_entry(version),
"automated_validations": list(validation_payload.get("automated_checks") or []),
"automated_validation_summary": automated_validation.get("summary"),
"generated_module": build_generated_tool_module_name(version.tool_name),
"generated_callable": GENERATED_TOOL_ENTRYPOINT,
"generated_source_code": generated_source_code,
"human_gate": self._build_human_review_gate(version),
"decision_history": self._list_governance_history_entries(version.id),
"next_steps": self._build_review_detail_next_steps(version, bool(generated_source_code)),
}
def build_publications_payload(self) -> dict:
publications_by_tool_name = {
publication["tool_name"]: publication
@ -481,7 +539,7 @@ class ToolManagementService:
pipeline_snapshot = self._build_pipeline_snapshot(version.status)
if automated_validation_result and automated_validation_result["passed"]:
message = (
"Pipeline de geracao executado com sucesso e as validacoes automaticas de contrato, assinatura e schema passaram. "
"Pipeline de geracao executado com sucesso e as validacoes automaticas de contrato, assinatura, importacao e smoke tests passaram. "
"A versao agora segue para a proxima etapa de validacao governada."
)
next_steps = [
@ -490,11 +548,11 @@ class ToolManagementService:
]
else:
message = (
"Pipeline de geracao executado, mas alguma validacao automatica de contrato, assinatura ou schema falhou. "
"Pipeline de geracao executado, mas alguma validacao automatica de contrato, assinatura, importacao ou smoke test falhou. "
"A versao foi marcada como failed para ajuste e nova tentativa."
)
next_steps = [
"Ajustar metadados, assinatura esperada e schema dos parametros antes de rodar o pipeline novamente.",
"Ajustar metadados, assinatura esperada, importacao do modulo e smoke tests antes de rodar o pipeline novamente.",
"Enquanto alguma validacao automatica falhar, a versao nao pode seguir para aprovacao e ativacao.",
]
return {
@ -517,9 +575,27 @@ class ToolManagementService:
reviewer_staff_account_id: int,
reviewer_name: str,
reviewer_role: StaffRole | str,
decision_notes: str,
reviewed_generated_code: bool,
) -> dict:
normalized_notes = self._normalize_human_decision_notes(decision_notes)
normalized_version_id = str(version_id or "").strip().lower()
version = (
self.version_repository.get_by_version_id(normalized_version_id)
if self.version_repository is not None
else None
)
if version is not None and version.status == ToolLifecycleStatus.GENERATED:
if not reviewed_generated_code:
raise ValueError(
"A revisao humana exige confirmar que o codigo gerado foi analisado antes da validacao."
)
if not self._version_has_generated_source(normalized_version_id):
raise ValueError(
"A revisao humana exige que a pipeline tenha registrado o codigo completo gerado para esta versao."
)
return self._transition_version_status(
version_id,
normalized_version_id,
target_status=ToolLifecycleStatus.VALIDATED,
allowed_current_statuses=(ToolLifecycleStatus.GENERATED,),
actor_staff_account_id=reviewer_staff_account_id,
@ -529,6 +605,8 @@ class ToolManagementService:
artifact_kind=ToolArtifactKind.DIRECTOR_REVIEW,
artifact_summary="Revisao inicial de diretor registrada para a versao governada.",
success_message="Versao revisada por diretor com sucesso e pronta para aprovacao.",
decision_notes=normalized_notes,
reviewed_generated_code=reviewed_generated_code,
next_steps=[
"A diretoria ainda precisa aprovar formalmente a versao antes da publicacao.",
"Depois da aprovacao, a publicacao ativa a tool no catalogo governado do produto.",
@ -542,7 +620,9 @@ class ToolManagementService:
approver_staff_account_id: int,
approver_name: str,
approver_role: StaffRole | str,
decision_notes: str,
) -> dict:
normalized_notes = self._normalize_human_decision_notes(decision_notes)
return self._transition_version_status(
version_id,
target_status=ToolLifecycleStatus.APPROVED,
@ -554,6 +634,7 @@ class ToolManagementService:
artifact_kind=ToolArtifactKind.DIRECTOR_APPROVAL,
artifact_summary="Aprovacao de diretor registrada para a versao governada.",
success_message="Versao aprovada por diretor com sucesso e pronta para publicacao.",
decision_notes=normalized_notes,
next_steps=[
"A publicacao administrativa ainda precisa ser executada antes da ativacao.",
"Enquanto a versao estiver apenas aprovada, ela permanece fora do catalogo ativo do produto.",
@ -585,6 +666,176 @@ class ToolManagementService:
],
)
def deactivate_version(
self,
version_id: str,
*,
actor_staff_account_id: int,
actor_name: str,
actor_role: StaffRole | str,
decision_notes: str,
) -> dict:
payload = self._transition_version_status(
version_id,
target_status=ToolLifecycleStatus.ARCHIVED,
allowed_current_statuses=(ToolLifecycleStatus.ACTIVE,),
actor_staff_account_id=actor_staff_account_id,
actor_name=actor_name,
actor_role=actor_role,
required_permission=AdminPermission.PUBLISH_TOOLS,
artifact_kind=ToolArtifactKind.PUBLICATION_DEACTIVATION,
artifact_summary="Publicacao ativa desativada pela diretoria.",
success_message="Versao ativa desativada com sucesso e retirada do catalogo governado.",
decision_notes=self._normalize_human_decision_notes(decision_notes),
next_steps=[
"A versao saiu do catalogo ativo e agora permanece apenas para historico e auditoria.",
"Se houver uma versao arquivada anterior da mesma tool, a diretoria pode executar rollback controlado quando necessario.",
],
)
payload["queue_entry"] = None
return payload
def rollback_version(
self,
version_id: str,
*,
actor_staff_account_id: int,
actor_name: str,
actor_role: StaffRole | str,
decision_notes: str,
) -> dict:
normalized_role = normalize_staff_role(actor_role)
if not role_has_permission(normalized_role, AdminPermission.PUBLISH_TOOLS):
raise PermissionError(
f"Papel '{normalized_role.value}' sem permissao administrativa '{AdminPermission.PUBLISH_TOOLS.value}'."
)
if (
self.draft_repository is None
or self.version_repository is None
or self.metadata_repository is None
):
raise RuntimeError(
"Fluxo de governanca de tools ainda nao esta completamente conectado ao armazenamento administrativo."
)
normalized_version_id = str(version_id or "").strip().lower()
current_version = self.version_repository.get_by_version_id(normalized_version_id)
if current_version is None:
raise LookupError("Versao administrativa nao encontrada.")
if current_version.status != ToolLifecycleStatus.ACTIVE:
raise ValueError(
f"O rollback exige uma versao atualmente active, mas a versao esta em '{current_version.status.value}'."
)
rollback_version = self._find_latest_archived_version(
tool_name=current_version.tool_name,
excluding_version_id=current_version.id,
)
if rollback_version is None:
raise ValueError("Nenhuma versao arquivada disponivel para rollback controlado desta tool.")
draft = self.draft_repository.get_by_tool_name(current_version.tool_name)
if draft is None:
raise RuntimeError("Draft raiz da tool nao encontrado para o rollback governado.")
current_metadata = self.metadata_repository.get_by_tool_version_id(current_version.id)
rollback_metadata = self.metadata_repository.get_by_tool_version_id(rollback_version.id)
if current_metadata is None or rollback_metadata is None:
raise RuntimeError("Metadados persistidos nao encontrados para executar o rollback governado.")
normalized_notes = self._normalize_human_decision_notes(decision_notes)
repository_session = self._resolve_repository_session()
atomic_write_options = {"commit": False} if repository_session is not None else {}
artifact_commit = False if repository_session is not None else None
try:
self._ensure_human_governance_ready_for_activation(rollback_version.id)
self.version_repository.update_status(
current_version,
status=ToolLifecycleStatus.ARCHIVED,
**atomic_write_options,
)
self.metadata_repository.update_status(
current_metadata,
status=ToolLifecycleStatus.ARCHIVED,
**atomic_write_options,
)
self.version_repository.update_status(
rollback_version,
status=ToolLifecycleStatus.ACTIVE,
**atomic_write_options,
)
self.metadata_repository.update_status(
rollback_metadata,
status=ToolLifecycleStatus.ACTIVE,
**atomic_write_options,
)
self.draft_repository.update_status(
draft,
status=ToolLifecycleStatus.ACTIVE,
**atomic_write_options,
)
self._persist_governance_artifact(
draft=draft,
version=current_version,
artifact_kind=ToolArtifactKind.PUBLICATION_DEACTIVATION,
summary="Versao ativa desativada para permitir rollback controlado.",
previous_status=ToolLifecycleStatus.ACTIVE,
current_status=ToolLifecycleStatus.ARCHIVED,
actor_staff_account_id=actor_staff_account_id,
actor_name=actor_name,
actor_role=normalized_role,
decision_notes=normalized_notes,
extra_payload={
"deactivated_for_rollback": True,
"rollback_target_version_id": rollback_version.version_id,
"rollback_target_version_number": rollback_version.version_number,
},
commit=artifact_commit,
)
self._persist_governance_artifact(
draft=draft,
version=rollback_version,
artifact_kind=ToolArtifactKind.PUBLICATION_ROLLBACK,
summary="Rollback controlado executado para restaurar a versao arquivada no catalogo ativo.",
previous_status=ToolLifecycleStatus.ARCHIVED,
current_status=ToolLifecycleStatus.ACTIVE,
actor_staff_account_id=actor_staff_account_id,
actor_name=actor_name,
actor_role=normalized_role,
decision_notes=normalized_notes,
extra_payload={
"rollback_from_version_id": current_version.version_id,
"rollback_from_version_number": current_version.version_number,
},
commit=artifact_commit,
)
if repository_session is not None:
self._commit_repository_session(
repository_session,
draft=draft,
version=rollback_version,
)
except Exception:
if repository_session is not None:
repository_session.rollback()
raise
return {
"message": (
"Rollback executado com sucesso e a versao arquivada voltou ao catalogo governado como ativa."
),
"version_id": rollback_version.version_id,
"tool_name": rollback_version.tool_name,
"version_number": rollback_version.version_number,
"status": rollback_version.status,
"queue_entry": None,
"publication": self._serialize_metadata_publication(rollback_metadata),
"next_steps": [
"A versao restaurada voltou ao catalogo ativo do produto sob governanca da diretoria.",
"A versao que estava ativa foi arquivada para manter trilha auditavel do rollback controlado.",
],
}
def _transition_version_status(
self,
version_id: str,
@ -598,6 +849,8 @@ class ToolManagementService:
artifact_kind: ToolArtifactKind,
artifact_summary: str,
success_message: str,
decision_notes: str | None = None,
reviewed_generated_code: bool | None = None,
next_steps: list[str],
) -> dict:
normalized_role = normalize_staff_role(actor_role)
@ -644,6 +897,7 @@ class ToolManagementService:
try:
if target_status == ToolLifecycleStatus.ACTIVE:
self._ensure_human_governance_ready_for_activation(version.id)
self._archive_active_publications(
tool_name=version.tool_name,
excluding_version_id=version.id,
@ -675,6 +929,8 @@ class ToolManagementService:
actor_staff_account_id=actor_staff_account_id,
actor_name=actor_name,
actor_role=normalized_role,
decision_notes=decision_notes,
reviewed_generated_code=reviewed_generated_code,
commit=artifact_commit,
)
if repository_session is not None:
@ -995,6 +1251,9 @@ class ToolManagementService:
actor_staff_account_id: int,
actor_name: str,
actor_role: StaffRole,
decision_notes: str | None = None,
reviewed_generated_code: bool | None = None,
extra_payload: dict | None = None,
commit: bool | None = None,
) -> None:
if self.artifact_repository is None:
@ -1018,6 +1277,9 @@ class ToolManagementService:
actor_staff_account_id=actor_staff_account_id,
actor_name=actor_name,
actor_role=actor_role,
decision_notes=decision_notes,
reviewed_generated_code=reviewed_generated_code,
extra_payload=extra_payload,
),
author_staff_account_id=actor_staff_account_id,
author_display_name=actor_name,
@ -1034,8 +1296,11 @@ class ToolManagementService:
actor_staff_account_id: int,
actor_name: str,
actor_role: StaffRole,
decision_notes: str | None = None,
reviewed_generated_code: bool | None = None,
extra_payload: dict | None = None,
) -> dict:
return {
payload = {
"source": "director_governance",
"action": artifact_kind.value,
"tool_name": version.tool_name,
@ -1046,7 +1311,12 @@ class ToolManagementService:
"actor_staff_account_id": actor_staff_account_id,
"actor_display_name": actor_name,
"actor_role": actor_role.value,
"decision_notes": str(decision_notes or "").strip() or None,
"reviewed_generated_code": reviewed_generated_code,
}
if extra_payload:
payload.update(extra_payload)
return payload
def _persist_initial_version_artifacts(
self,
@ -1341,6 +1611,14 @@ class ToolManagementService:
"signature_schema": dict(signature_schema_blueprint),
"import_loading": dict(import_loading_result),
"smoke_tests": dict(smoke_test_result),
"generated_source_code": (
str(import_loading_result.get("rendered_source") or "").strip()
or self._render_generated_tool_module_source(
version=version,
metadata=metadata,
signature_schema_blueprint=signature_schema_blueprint,
)
),
"publication_envelope": publication_envelope,
}
@ -1499,6 +1777,11 @@ class ToolManagementService:
"loaded_callable": GENERATED_TOOL_ENTRYPOINT,
"loaded_signature": None,
"sandbox_package_root": None,
"rendered_source": self._render_generated_tool_module_source(
version=version,
metadata=metadata,
signature_schema_blueprint=signature_schema_blueprint,
),
"issues": [
"generated import/loading validation skipped because the signature/schema blueprint is invalid."
],
@ -1538,6 +1821,7 @@ class ToolManagementService:
"loaded_callable": load_result["loaded_callable"],
"loaded_signature": loaded_signature,
"sandbox_package_root": load_result["sandbox_package_root"],
"rendered_source": load_result["rendered_source"],
"issues": issues,
}
@ -1973,6 +2257,189 @@ class ToolManagementService:
"queued_at": version.updated_at or version.created_at,
}
def _version_has_generated_source(self, version_id: str) -> bool:
if self.version_repository is None or self.artifact_repository is None:
return False
normalized_version_id = str(version_id or "").strip().lower()
version = self.version_repository.get_by_version_id(normalized_version_id)
if version is None:
return False
validation_artifact = self.artifact_repository.get_by_tool_version_and_kind(
version.id,
ToolArtifactKind.VALIDATION_REPORT,
)
if validation_artifact is None:
return False
generated_source_code = str((validation_artifact.payload_json or {}).get("generated_source_code") or "").strip()
return bool(generated_source_code)
def _find_latest_archived_version(
self,
*,
tool_name: str,
excluding_version_id: int | None = None,
) -> ToolVersion | None:
if self.version_repository is None:
return None
for archived_version in self.version_repository.list_versions(
tool_name=tool_name,
statuses=(ToolLifecycleStatus.ARCHIVED,),
):
if excluding_version_id is not None and archived_version.id == excluding_version_id:
continue
return archived_version
return None
@staticmethod
def _normalize_human_decision_notes(decision_notes: str) -> str:
normalized_notes = str(decision_notes or "").strip()
if len(normalized_notes) < _HUMAN_DECISION_NOTES_MIN_LENGTH:
raise ValueError(
"A decisao humana precisa registrar um parecer com pelo menos "
f"{_HUMAN_DECISION_NOTES_MIN_LENGTH} caracteres."
)
return normalized_notes
def _ensure_human_governance_ready_for_activation(self, tool_version_id: int) -> None:
if self.artifact_repository is None:
raise RuntimeError(
"A ativacao governada exige trilha de auditoria habilitada para validar a aprovacao humana."
)
review_artifact = self.artifact_repository.get_by_tool_version_and_kind(
tool_version_id,
ToolArtifactKind.DIRECTOR_REVIEW,
)
approval_artifact = self.artifact_repository.get_by_tool_version_and_kind(
tool_version_id,
ToolArtifactKind.DIRECTOR_APPROVAL,
)
review_payload = dict(review_artifact.payload_json or {}) if review_artifact is not None else {}
approval_payload = dict(approval_artifact.payload_json or {}) if approval_artifact is not None else {}
if not review_payload.get("decision_notes") or not bool(review_payload.get("reviewed_generated_code")):
raise ValueError(
"A ativacao exige uma revisao humana registrada com parecer e confirmacao de leitura do codigo gerado."
)
if not approval_payload.get("decision_notes"):
raise ValueError(
"A ativacao exige uma aprovacao humana registrada com parecer explicito da diretoria."
)
def _build_human_review_gate(self, version: ToolVersion) -> dict:
rollback_candidate = None
if version.status == ToolLifecycleStatus.ACTIVE:
rollback_candidate = self._find_latest_archived_version(
tool_name=version.tool_name,
excluding_version_id=version.id,
)
return {
"current_gate": ToolManagementService._build_review_gate(version.status),
"review_action_available": version.status == ToolLifecycleStatus.GENERATED,
"approval_action_available": version.status == ToolLifecycleStatus.VALIDATED,
"publication_action_available": version.status == ToolLifecycleStatus.APPROVED,
"deactivation_action_available": version.status == ToolLifecycleStatus.ACTIVE,
"rollback_action_available": version.status == ToolLifecycleStatus.ACTIVE and rollback_candidate is not None,
"rollback_target_version_id": rollback_candidate.version_id if rollback_candidate is not None else None,
"rollback_target_version_number": rollback_candidate.version_number if rollback_candidate is not None else None,
"requires_decision_notes": version.status in {
ToolLifecycleStatus.GENERATED,
ToolLifecycleStatus.VALIDATED,
ToolLifecycleStatus.ACTIVE,
},
"requires_code_review_confirmation": version.status == ToolLifecycleStatus.GENERATED,
}
def _build_review_detail_next_steps(
self,
version: ToolVersion,
generated_source_available: bool,
) -> list[str]:
status = version.status
next_steps_by_status = {
ToolLifecycleStatus.DRAFT: [
"Execute a pipeline de geracao para produzir o modulo governado antes da revisao humana.",
"Enquanto a versao estiver em draft, ela permanece fora da aprovacao e da ativacao.",
],
ToolLifecycleStatus.GENERATED: [
"Analise o codigo completo gerado, confirme a leitura manual e registre a revisao da diretoria.",
"Somente depois da revisao humana a versao pode seguir para aprovacao formal.",
],
ToolLifecycleStatus.VALIDATED: [
"Registre o parecer final de aprovacao da diretoria antes da publicacao.",
"A ativacao continua bloqueada ate existir aprovacao humana explicita.",
],
ToolLifecycleStatus.APPROVED: [
"A versao ja foi aprovada pela diretoria e agora pode seguir para publicacao controlada.",
"A ativacao vai validar novamente a trilha de revisao e aprovacao humana antes de entrar no catalogo.",
],
ToolLifecycleStatus.ACTIVE: [
"A versao esta ativa no catalogo governado e pode ser desativada com parecer explicito da diretoria.",
"Quando houver uma versao arquivada anterior, o rollback controlado pode restaurar rapidamente a publicacao anterior.",
],
ToolLifecycleStatus.ARCHIVED: [
"Esta versao foi retirada do catalogo ativo e permanece arquivada para historico e auditoria.",
"A diretoria pode restaurar uma versao arquivada por rollback controlado a partir da publicacao ativa correspondente.",
],
ToolLifecycleStatus.FAILED: [
"Corrija os bloqueios da pipeline e execute uma nova geracao antes de voltar para a revisao humana.",
"Enquanto a versao estiver em failed, a aprovacao e a ativacao permanecem indisponiveis.",
],
}
next_steps = list(next_steps_by_status.get(status, ["Acompanhe a governanca da versao pela trilha administrativa."]))
if status == ToolLifecycleStatus.ACTIVE:
rollback_candidate = self._find_latest_archived_version(
tool_name=version.tool_name,
excluding_version_id=version.id,
)
if rollback_candidate is not None:
next_steps.append(
f"Ha uma versao arquivada disponivel para rollback: v{rollback_candidate.version_number}."
)
if not generated_source_available:
next_steps.append("O codigo completo aparece aqui assim que a pipeline gerar e registrar a funcao governada.")
return next_steps
def _list_governance_history_entries(self, tool_version_id: int) -> list[dict]:
if self.artifact_repository is None:
return []
history_entries = self.artifact_repository.list_artifacts(
tool_version_id=tool_version_id,
artifact_stage=ToolArtifactStage.GOVERNANCE,
)
return [
self._serialize_governance_history_entry(artifact)
for artifact in reversed(history_entries)
]
@staticmethod
def _serialize_governance_history_entry(artifact) -> dict:
payload = dict(artifact.payload_json or {})
label_by_kind = {
ToolArtifactKind.DIRECTOR_REVIEW: "Revisao humana registrada",
ToolArtifactKind.DIRECTOR_APPROVAL: "Aprovacao humana registrada",
ToolArtifactKind.PUBLICATION_RELEASE: "Publicacao administrativa registrada",
ToolArtifactKind.PUBLICATION_DEACTIVATION: "Desativacao registrada",
ToolArtifactKind.PUBLICATION_ROLLBACK: "Rollback registrado",
}
return {
"action_key": artifact.artifact_kind.value,
"label": label_by_kind.get(artifact.artifact_kind, "Governanca registrada"),
"summary": artifact.summary,
"previous_status": payload.get("previous_status"),
"current_status": payload.get("current_status"),
"actor_name": payload.get("actor_display_name"),
"actor_role": payload.get("actor_role"),
"decision_notes": payload.get("decision_notes"),
"reviewed_generated_code": payload.get("reviewed_generated_code"),
"recorded_at": artifact.updated_at or artifact.created_at,
}
@staticmethod
def _build_review_gate(status: ToolLifecycleStatus) -> str:
gate_by_status = {
@ -1980,6 +2447,8 @@ class ToolManagementService:
ToolLifecycleStatus.GENERATED: "validation_required",
ToolLifecycleStatus.VALIDATED: "director_approval_required",
ToolLifecycleStatus.APPROVED: "director_publication_required",
ToolLifecycleStatus.ACTIVE: "publication_active",
ToolLifecycleStatus.ARCHIVED: "archived_history",
ToolLifecycleStatus.FAILED: "pipeline_retry_required",
}
return gate_by_status.get(status, "governance_required")
@ -2035,6 +2504,18 @@ class ToolManagementService:
def _serialize_metadata_publication(self, metadata: ToolMetadata) -> dict:
parameters = self._serialize_parameters_for_response(metadata.parameters_json)
version_record = None
if self.version_repository is not None:
for candidate in self.version_repository.list_versions(tool_name=metadata.tool_name):
if candidate.id == metadata.tool_version_id:
version_record = candidate
break
rollback_candidate = None
if metadata.status == ToolLifecycleStatus.ACTIVE:
rollback_candidate = self._find_latest_archived_version(
tool_name=metadata.tool_name,
excluding_version_id=metadata.tool_version_id,
)
return {
"publication_id": metadata.metadata_id,
"tool_name": metadata.tool_name,
@ -2043,6 +2524,7 @@ class ToolManagementService:
"domain": metadata.domain,
"version": metadata.version_number,
"status": metadata.status,
"version_id": version_record.version_id if version_record is not None else None,
"parameter_count": len(parameters),
"parameters": parameters,
"author_name": metadata.author_display_name,
@ -2050,6 +2532,10 @@ class ToolManagementService:
"implementation_callable": GENERATED_TOOL_ENTRYPOINT,
"published_by": metadata.author_display_name,
"published_at": metadata.updated_at or metadata.created_at,
"deactivation_action_available": metadata.status == ToolLifecycleStatus.ACTIVE and version_record is not None,
"rollback_action_available": metadata.status == ToolLifecycleStatus.ACTIVE and rollback_candidate is not None,
"rollback_target_version_id": rollback_candidate.version_id if rollback_candidate is not None else None,
"rollback_target_version_number": rollback_candidate.version_number if rollback_candidate is not None else None,
}
def _serialize_draft_summary(self, draft: ToolDraft) -> dict:

@ -672,6 +672,88 @@ def render_tool_review_page(
</div>
</div>
<div class="card border-0 shadow-sm admin-surface-card mt-4 mb-4">
<div class="card-body p-4">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Detalhe da versao</p>
<h3 class="h3 fw-semibold mb-2">Revisao humana antes da ativacao</h3>
<p class="text-secondary mb-0">Selecione uma versao da fila para validar o contrato, inspecionar o codigo completo gerado e registrar a decisao da diretoria.</p>
</div>
<span class="badge rounded-pill bg-body-tertiary text-secondary border" data-tool-review-detail-status>Nenhum item</span>
</div>
<div class="row g-4">
<div class="col-12 col-xxl-5">
<div class="d-flex flex-column gap-3">
<div class="admin-tool-review-note p-4" data-tool-review-detail-summary>
<div class="fw-semibold mb-2" data-tool-review-detail-title>Selecione um item da fila</div>
<p class="text-secondary mb-0">O detalhe da versao aparece aqui junto com o resumo funcional e o gate humano atual.</p>
</div>
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Contexto e parametros</p>
<div class="vstack gap-2" data-tool-review-detail-meta>
<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Nenhuma versao selecionada.</div>
</div>
</div>
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Validacoes automaticas</p>
<div class="vstack gap-2" data-tool-review-validation-list>
<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">As validacoes da pipeline aparecem aqui.</div>
</div>
</div>
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Historico da diretoria</p>
<div class="vstack gap-2" data-tool-review-history-list>
<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Nenhuma decisao humana registrada ainda.</div>
</div>
</div>
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Proximos passos</p>
<div class="vstack gap-2" data-tool-review-next-steps>
<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Os proximos passos da versao aparecem aqui.</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-xxl-7">
<div class="d-flex flex-column gap-3 h-100">
<div>
<label class="form-label fw-semibold" for="admin-tool-review-generated-code">Codigo completo da funcao gerada</label>
<textarea class="form-control rounded-4 font-monospace" id="admin-tool-review-generated-code" rows="22" readonly data-tool-review-code>O codigo gerado pela pipeline aparecera aqui assim que uma versao for selecionada.</textarea>
<div class="form-text">Use este campo para revisar a implementacao completa antes de validar, aprovar e ativar a nova tool.</div>
</div>
<div>
<label class="form-label fw-semibold" for="admin-tool-review-decision-notes">Parecer da diretoria</label>
<textarea class="form-control rounded-4" id="admin-tool-review-decision-notes" rows="5" placeholder="Registre o racional da revisao ou da aprovacao humana." data-tool-review-decision-notes></textarea>
<div class="form-text" data-tool-review-decision-hint>As notas da decisao ficam persistidas na trilha administrativa da versao.</div>
</div>
<div class="form-check admin-tool-inline-note rounded-4 p-3">
<input class="form-check-input" type="checkbox" value="1" id="admin-tool-review-code-check" data-tool-review-reviewed-code>
<label class="form-check-label small text-secondary" for="admin-tool-review-code-check">
Confirmo que revisei o codigo completo gerado antes de validar esta versao.
</label>
</div>
<div class="d-flex flex-wrap gap-2" data-tool-review-actions>
<button class="btn btn-outline-dark rounded-pill" type="button" data-tool-review-action="review" disabled>Registrar revisao</button>
<button class="btn btn-dark rounded-pill" type="button" data-tool-review-action="approve" disabled>Aprovar versao</button>
<button class="btn btn-success rounded-pill" type="button" data-tool-review-action="publish" disabled>Publicar no catalogo</button>
<button class="btn btn-outline-danger rounded-pill" type="button" data-tool-review-action="deactivate" disabled>Desativar versao</button>
<button class="btn btn-warning rounded-pill" type="button" data-tool-review-action="rollback" disabled>Executar rollback</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm admin-surface-card mt-4">
<div class="card-body p-4">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">

@ -762,7 +762,7 @@ def _build_tool_review_view(request: Request, settings: AdminSettings) -> AdminT
review_notes=(
"Conferir se o gate do item combina com o estado esperado do lifecycle.",
"Observar se a descricao e o objetivo operacional da tool estao claros para o time.",
"Usar o catalogo ativo como comparativo antes de promover uma nova versao.",
"Ler o codigo completo gerado antes de validar manualmente a versao.",
),
approval_notes=(
"Verificar nome, descricao e semantica dos parametros antes da aprovacao.",

@ -115,23 +115,96 @@ function mountToolReviewBoard(board) {
const publicationList = board.querySelector("[data-tool-publication-list]");
const lifecycleList = board.querySelector("[data-tool-contract-lifecycle]");
const parameterTypes = board.querySelector("[data-tool-parameter-types]");
const detailStatus = board.querySelector("[data-tool-review-detail-status]");
const detailSummary = board.querySelector("[data-tool-review-detail-summary]");
const detailTitle = board.querySelector("[data-tool-review-detail-title]");
const detailMeta = board.querySelector("[data-tool-review-detail-meta]");
const validationList = board.querySelector("[data-tool-review-validation-list]");
const historyList = board.querySelector("[data-tool-review-history-list]");
const nextStepsList = board.querySelector("[data-tool-review-next-steps]");
const codeField = board.querySelector("[data-tool-review-code]");
const decisionNotes = board.querySelector("[data-tool-review-decision-notes]");
const decisionHint = board.querySelector("[data-tool-review-decision-hint]");
const reviewedGeneratedCode = board.querySelector("[data-tool-review-reviewed-code]");
const reviewButton = board.querySelector('[data-tool-review-action="review"]');
const approveButton = board.querySelector('[data-tool-review-action="approve"]');
const publishButton = board.querySelector('[data-tool-review-action="publish"]');
const deactivateButton = board.querySelector('[data-tool-review-action="deactivate"]');
const rollbackButton = board.querySelector('[data-tool-review-action="rollback"]');
let selectedVersionId = "";
let lastRenderedHumanGate = null;
let lastRenderedHasSourceCode = false;
if (refreshButton) {
refreshButton.addEventListener("click", () => {
void loadBoard();
void loadBoard(selectedVersionId);
});
}
if (queueList) {
queueList.addEventListener("click", (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
const trigger = target.closest("[data-tool-review-select]");
if (!(trigger instanceof HTMLElement)) {
return;
}
const nextVersionId = String(trigger.dataset.versionId || "").trim();
if (!nextVersionId) {
return;
}
void loadReviewDetail(nextVersionId);
});
}
if (publicationList) {
publicationList.addEventListener("click", (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
const trigger = target.closest("[data-tool-publication-select]");
if (!(trigger instanceof HTMLElement)) {
return;
}
const nextVersionId = String(trigger.dataset.versionId || "").trim();
if (!nextVersionId) {
return;
}
void loadReviewDetail(nextVersionId);
});
}
[reviewButton, approveButton, publishButton, deactivateButton, rollbackButton].forEach((button) => {
if (!(button instanceof HTMLButtonElement)) {
return;
}
button.dataset.defaultLabel = button.textContent || "";
button.addEventListener("click", () => {
const actionKey = String(button.dataset.toolReviewAction || "").trim();
if (!actionKey) {
return;
}
void submitGovernanceAction(actionKey);
});
});
renderEmptyDetail("Selecione um item da fila para carregar o contexto completo da revisao humana.");
void loadBoard();
async function loadBoard() {
async function loadBoard(preferredVersionId = "") {
toggleRefreshing(true);
clearFeedback();
const overviewResult = await fetchPanelJson(board.dataset.overviewEndpoint);
const contractsResult = await fetchPanelJson(board.dataset.contractsEndpoint);
const reviewQueueResult = await fetchPanelJson(board.dataset.reviewQueueEndpoint);
const publicationsResult = await fetchPanelJson(board.dataset.publicationsEndpoint);
const [overviewResult, contractsResult, reviewQueueResult, publicationsResult] = await Promise.all([
fetchPanelJson(board.dataset.overviewEndpoint),
fetchPanelJson(board.dataset.contractsEndpoint),
fetchPanelJson(board.dataset.reviewQueueEndpoint),
fetchPanelJson(board.dataset.publicationsEndpoint),
]);
if (!overviewResult.ok && !contractsResult.ok && !reviewQueueResult.ok && !publicationsResult.ok) {
showFeedback("warning", overviewResult.message || "Entre com uma sessao administrativa web para carregar esta tela.");
@ -146,9 +219,29 @@ function mountToolReviewBoard(board) {
renderLockedLifecycle(contractsResult.message);
}
if (reviewQueueResult.ok) {
renderReviewQueue(reviewQueueResult.body);
renderReviewQueue(reviewQueueResult.body, preferredVersionId || selectedVersionId);
const items = Array.isArray(reviewQueueResult.body?.items) ? reviewQueueResult.body.items : [];
if (items.length > 0) {
const nextVersionId = items.some((item) => item?.version_id === (preferredVersionId || selectedVersionId))
? (preferredVersionId || selectedVersionId)
: String(items[0]?.version_id || "").trim();
if (nextVersionId) {
await loadReviewDetail(nextVersionId);
} else {
renderEmptyDetail(reviewQueueResult.body?.message || "Nenhuma versao pronta para detalhe.");
}
} else {
const fallbackVersionId = String(preferredVersionId || selectedVersionId || "").trim();
if (fallbackVersionId) {
await loadReviewDetail(fallbackVersionId);
} else {
selectedVersionId = "";
renderEmptyDetail(reviewQueueResult.body?.message || "Nenhuma versao aguardando revisao neste momento.");
}
}
} else {
renderLockedQueue(reviewQueueResult.message);
renderLockedDetail(reviewQueueResult.message || "A sessao atual nao pode acessar o detalhe de revisao.");
}
if (publicationsResult.ok) {
renderPublications(publicationsResult.body);
@ -160,6 +253,99 @@ function mountToolReviewBoard(board) {
toggleRefreshing(false);
}
async function loadReviewDetail(versionId) {
const normalizedVersionId = String(versionId || "").trim();
if (!normalizedVersionId) {
renderEmptyDetail("Selecione uma versao valida para abrir o detalhe da revisao.");
return;
}
selectedVersionId = normalizedVersionId;
renderDetailLoading();
const detailUrl = `${board.dataset.reviewQueueEndpoint}/${encodeURIComponent(normalizedVersionId)}`;
const detailResult = await fetchPanelJson(detailUrl);
if (!detailResult.ok) {
renderLockedDetail(detailResult.message || "Nao foi possivel carregar o detalhe da versao selecionada.");
showFeedback("warning", detailResult.message || "Nao foi possivel carregar o detalhe da revisao humana.");
return;
}
renderReviewDetail(detailResult.body);
renderReviewQueueSelection(normalizedVersionId);
}
async function submitGovernanceAction(actionKey) {
if (!selectedVersionId) {
showFeedback("warning", "Selecione uma versao da fila antes de registrar uma decisao humana.");
return;
}
const actionUrl = resolveGovernanceActionUrl(actionKey, selectedVersionId);
if (!actionUrl) {
showFeedback("warning", "A acao solicitada nao esta disponivel para esta versao.");
return;
}
let payload;
if (actionKey === "review") {
payload = {
decision_notes: String(decisionNotes?.value || "").trim(),
reviewed_generated_code: Boolean(reviewedGeneratedCode?.checked),
};
} else if (actionKey === "approve" || actionKey === "deactivate" || actionKey === "rollback") {
payload = {
decision_notes: String(decisionNotes?.value || "").trim(),
};
}
toggleActionLoading(actionKey, true);
clearFeedback();
try {
const response = await fetch(actionUrl, {
method: "POST",
credentials: "same-origin",
headers: {
Accept: "application/json",
...(payload ? { "Content-Type": "application/json" } : {}),
},
...(payload ? { body: JSON.stringify(payload) } : {}),
});
const body = await readJson(response);
if (!response.ok) {
throw new Error(body?.detail || "Nao foi possivel registrar a decisao humana desta versao.");
}
if (decisionNotes instanceof HTMLTextAreaElement) {
decisionNotes.value = "";
}
if (reviewedGeneratedCode instanceof HTMLInputElement) {
reviewedGeneratedCode.checked = false;
}
showFeedback("success", body?.message || "Decisao humana registrada com sucesso.");
await loadBoard(body?.version_id || selectedVersionId);
} catch (error) {
showFeedback("danger", error instanceof Error ? error.message : "Erro inesperado ao registrar a decisao humana.");
} finally {
toggleActionLoading(actionKey, false);
}
}
function resolveGovernanceActionUrl(actionKey, versionId) {
const encodedVersionId = encodeURIComponent(String(versionId || "").trim());
if (!encodedVersionId) {
return "";
}
if (actionKey === "publish" || actionKey === "deactivate" || actionKey === "rollback") {
return `${board.dataset.publicationsEndpoint}/${encodedVersionId}/${actionKey}`;
}
if (actionKey === "review" || actionKey === "approve") {
return `${board.dataset.reviewQueueEndpoint}/${encodedVersionId}/${actionKey}`;
}
return "";
}
function toggleRefreshing(isLoading) {
if (!refreshButton || !refreshLabel || !refreshSpinner) {
return;
@ -169,6 +355,26 @@ function mountToolReviewBoard(board) {
refreshLabel.textContent = isLoading ? "Atualizando..." : "Atualizar leitura";
}
function toggleActionLoading(actionKey, isLoading) {
const buttonsByAction = {
review: reviewButton,
approve: approveButton,
publish: publishButton,
deactivate: deactivateButton,
rollback: rollbackButton,
};
const button = buttonsByAction[actionKey];
if (!(button instanceof HTMLButtonElement)) {
return;
}
const defaultLabel = button.dataset.defaultLabel || button.textContent || "";
button.disabled = isLoading || button.disabled;
button.textContent = isLoading ? "Processando..." : defaultLabel;
if (!isLoading) {
configureActionPanel(lastRenderedHumanGate, lastRenderedHasSourceCode);
}
}
function clearFeedback() {
feedback.className = "alert d-none rounded-4 mb-4";
feedback.textContent = "";
@ -204,15 +410,46 @@ function mountToolReviewBoard(board) {
parameterTypes.innerHTML = `<span class="badge rounded-pill bg-body-tertiary text-secondary border">Bloqueado</span>`;
}
function renderReviewQueue(payload) {
function renderReviewQueue(payload, preferredVersionId = "") {
const items = Array.isArray(payload?.items) ? payload.items : [];
setText("[data-tool-review-queue-count]", String(items.length));
setText("[data-tool-review-queue-mode]", payload?.queue_mode || "Fila web");
queueList.innerHTML = items.length > 0
? items.map((item) => `<article class="admin-tool-review-card rounded-4 p-4"><div class="d-flex justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item.gate || "revisao")}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item.display_name || item.tool_name || "Tool")}</h4><div class="small text-secondary">${escapeHtml(item.tool_name || "")}</div></div><span class="badge rounded-pill bg-warning-subtle text-warning-emphasis border border-warning-subtle">${escapeHtml(item.status || "pendente")}</span></div><p class="text-secondary mb-0">${escapeHtml(item.summary || payload?.message || "Item aguardando analise do time.")}</p></article>`).join("")
? items.map((item) => {
const isSelected = String(item?.version_id || "") === String(preferredVersionId || selectedVersionId || "");
const validationSummary = item?.automated_validation_summary
? `<div class="small text-secondary mt-2"><strong>Pipeline:</strong> ${escapeHtml(item.automated_validation_summary)}</div>`
: "";
const queuedAt = item?.queued_at
? `<div class="small text-secondary mt-2"><strong>Atualizado:</strong> ${escapeHtml(formatDateTime(item.queued_at))}</div>`
: "";
return `<article class="admin-tool-review-card rounded-4 p-4 ${isSelected ? "border border-dark" : ""}"><div class="d-flex justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item.gate || "revisao")}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item.display_name || item.tool_name || "Tool")}</h4><div class="small text-secondary">${escapeHtml(item.tool_name || "")}</div></div><span class="badge rounded-pill bg-warning-subtle text-warning-emphasis border border-warning-subtle">${escapeHtml(item.status || "pendente")}</span></div><p class="text-secondary mb-3">${escapeHtml(item.summary || payload?.message || "Item aguardando analise do time.")}</p>${validationSummary}${queuedAt}<div class="pt-3"><button class="btn btn-sm ${isSelected ? "btn-dark" : "btn-outline-dark"} rounded-pill" type="button" data-tool-review-select="true" data-version-id="${escapeHtml(item.version_id || "")}">${isSelected ? "Versao selecionada" : "Abrir detalhe"}</button></div></article>`;
}).join("")
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Fila sem itens no momento</h4><p class="text-secondary mb-0">${escapeHtml(payload?.message || "Nenhuma tool aguardando revisao agora.")}</p></div>`;
}
function renderReviewQueueSelection(versionId) {
const normalizedVersionId = String(versionId || "").trim();
queueList.querySelectorAll("[data-tool-review-select]").forEach((button) => {
if (!(button instanceof HTMLButtonElement)) {
return;
}
const isSelected = String(button.dataset.versionId || "") === normalizedVersionId;
button.classList.toggle("btn-dark", isSelected);
button.classList.toggle("btn-outline-dark", !isSelected);
button.textContent = isSelected ? "Versao selecionada" : "Abrir detalhe";
});
queueList.querySelectorAll(".admin-tool-review-card").forEach((card) => {
if (!(card instanceof HTMLElement)) {
return;
}
const cardButton = card.querySelector("[data-tool-review-select]");
const isSelected = cardButton instanceof HTMLElement && String(cardButton.dataset.versionId || "") === normalizedVersionId;
card.classList.toggle("border", isSelected);
card.classList.toggle("border-dark", isSelected);
});
}
function renderLockedQueue(message) {
setText("[data-tool-review-queue-count]", "0");
setText("[data-tool-review-queue-mode]", "Bloqueado");
@ -224,7 +461,18 @@ function mountToolReviewBoard(board) {
setText("[data-tool-review-publication-count]", String(items.length));
setText("[data-tool-publication-source]", payload?.source || "Catalogo web");
publicationList.innerHTML = items.length > 0
? items.slice(0, 9).map((item) => `<div class="col-12 col-md-6 col-xxl-4"><article class="admin-tool-publication-card rounded-4 p-4 h-100"><div class="d-flex justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item.domain || "tool")}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item.display_name || item.tool_name || "Tool")}</h4><div class="small text-secondary">${escapeHtml(item.tool_name || "")}</div></div><span class="badge rounded-pill bg-success-subtle text-success-emphasis border border-success-subtle">v${escapeHtml(String(item.version || 1))}</span></div><p class="text-secondary mb-3">${escapeHtml(item.description || "Publicacao ativa no catalogo do produto.")}</p><div class="small text-secondary mb-1"><strong>Status:</strong> ${escapeHtml(item.status || "draft")}</div><div class="small text-secondary mb-1"><strong>Parametros:</strong> ${escapeHtml(String(item.parameter_count || 0))}</div><div class="small text-secondary mb-3"><strong>Autor:</strong> ${escapeHtml(item.author_name || item.published_by || "Nao informado")}</div><div class="small text-secondary">${escapeHtml(item.implementation_module || "")}</div></article></div>`).join("")
? items.slice(0, 9).map((item) => {
const manageButton = item?.version_id
? `<div class="pt-3"><button class="btn btn-sm btn-outline-dark rounded-pill" type="button" data-tool-publication-select="true" data-version-id="${escapeHtml(item.version_id || "")}">Abrir detalhe</button></div>`
: "";
const rollbackBadge = item?.rollback_action_available
? `<span class="badge rounded-pill bg-warning-subtle text-warning-emphasis border border-warning-subtle">Rollback disponivel</span>`
: "";
const deactivateBadge = item?.deactivation_action_available
? `<span class="badge rounded-pill bg-body-tertiary text-secondary border">Desativacao disponivel</span>`
: "";
return `<div class="col-12 col-md-6 col-xxl-4"><article class="admin-tool-publication-card rounded-4 p-4 h-100"><div class="d-flex justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item.domain || "tool")}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item.display_name || item.tool_name || "Tool")}</h4><div class="small text-secondary">${escapeHtml(item.tool_name || "")}</div></div><span class="badge rounded-pill bg-success-subtle text-success-emphasis border border-success-subtle">v${escapeHtml(String(item.version || 1))}</span></div><p class="text-secondary mb-3">${escapeHtml(item.description || "Publicacao ativa no catalogo do produto.")}</p><div class="d-flex flex-wrap gap-2 mb-3">${deactivateBadge}${rollbackBadge}</div><div class="small text-secondary mb-1"><strong>Status:</strong> ${escapeHtml(item.status || "draft")}</div><div class="small text-secondary mb-1"><strong>Parametros:</strong> ${escapeHtml(String(item.parameter_count || 0))}</div><div class="small text-secondary mb-3"><strong>Autor:</strong> ${escapeHtml(item.author_name || item.published_by || "Nao informado")}</div><div class="small text-secondary">${escapeHtml(item.implementation_module || "")}</div>${manageButton}</article></div>`;
}).join("")
: `<div class="col-12"><div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Catalogo ativo vazio</h4><p class="text-secondary mb-0">Nenhuma publicacao ativa retornada pela sessao web.</p></div></div>`;
}
@ -233,6 +481,195 @@ function mountToolReviewBoard(board) {
setText("[data-tool-publication-source]", "Bloqueado");
publicationList.innerHTML = `<div class="col-12"><div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Catalogo protegido</h4><p class="text-secondary mb-0">${escapeHtml(message || "A sessao atual nao possui permissao para ler as publicacoes ativas.")}</p></div></div>`;
}
function renderDetailLoading() {
detailStatus.textContent = "Carregando";
detailTitle.textContent = "Sincronizando detalhe da versao";
detailSummary.innerHTML = `<div class="fw-semibold mb-2">Carregando contexto governado</div><p class="text-secondary mb-0">A leitura do detalhe da versao esta em andamento.</p>`;
detailMeta.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Carregando metadados persistidos...</div>`;
validationList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Carregando validacoes automaticas...</div>`;
historyList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Carregando historico humano...</div>`;
nextStepsList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Carregando proximos passos...</div>`;
if (codeField instanceof HTMLTextAreaElement) {
codeField.value = "Carregando codigo gerado...";
}
configureActionPanel(null, false);
}
function renderEmptyDetail(message) {
detailStatus.textContent = "Nenhum item";
detailTitle.textContent = "Selecione um item da fila";
detailSummary.innerHTML = `<div class="fw-semibold mb-2">Revisao humana aguardando selecao</div><p class="text-secondary mb-0">${escapeHtml(message || "Escolha uma versao da fila para abrir o detalhe governado.")}</p>`;
detailMeta.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Os metadados persistidos e os parametros da versao aparecem aqui.</div>`;
validationList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">As validacoes automaticas da pipeline aparecem aqui.</div>`;
historyList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">As decisoes humanas de revisao, aprovacao e publicacao aparecem aqui.</div>`;
nextStepsList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Selecione uma versao para visualizar os proximos passos recomendados.</div>`;
if (codeField instanceof HTMLTextAreaElement) {
codeField.value = "O codigo completo da funcao gerada aparecera aqui assim que uma versao for selecionada.";
}
if (decisionNotes instanceof HTMLTextAreaElement) {
decisionNotes.value = "";
}
if (reviewedGeneratedCode instanceof HTMLInputElement) {
reviewedGeneratedCode.checked = false;
}
configureActionPanel(null, false);
}
function renderLockedDetail(message) {
detailStatus.textContent = "Bloqueado";
detailTitle.textContent = "Detalhe indisponivel";
detailSummary.innerHTML = `<div class="fw-semibold mb-2">Leitura protegida</div><p class="text-secondary mb-0">${escapeHtml(message || "A sessao atual nao pode visualizar o detalhe de revisao desta versao.")}</p>`;
detailMeta.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Sem acesso aos metadados desta versao.</div>`;
validationList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Sem acesso ao relatorio de validacao automatica.</div>`;
historyList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Sem acesso ao historico de governanca.</div>`;
nextStepsList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Entre com uma sessao com permissao de revisao para continuar.</div>`;
if (codeField instanceof HTMLTextAreaElement) {
codeField.value = message || "A leitura do codigo gerado esta protegida pela permissao de revisao.";
}
if (decisionNotes instanceof HTMLTextAreaElement) {
decisionNotes.value = "";
}
if (reviewedGeneratedCode instanceof HTMLInputElement) {
reviewedGeneratedCode.checked = false;
}
configureActionPanel(null, false);
}
function renderReviewDetail(payload) {
const parameters = Array.isArray(payload?.parameters) ? payload.parameters : [];
const validations = Array.isArray(payload?.automated_validations) ? payload.automated_validations : [];
const history = Array.isArray(payload?.decision_history) ? payload.decision_history : [];
const nextSteps = Array.isArray(payload?.next_steps) ? payload.next_steps : [];
const humanGate = payload?.human_gate || null;
const hasSourceCode = Boolean(String(payload?.generated_source_code || "").trim());
detailStatus.textContent = payload?.status || "versao";
detailTitle.innerHTML = `${escapeHtml(payload?.display_name || payload?.tool_name || "Tool")}&nbsp;<span class="small text-secondary">v${escapeHtml(String(payload?.version_number || 1))}</span>`;
detailSummary.innerHTML = `<div class="fw-semibold mb-2">${escapeHtml(payload?.summary || "Resumo indisponivel")}</div><p class="text-secondary mb-0">${escapeHtml(payload?.description || "Sem descricao detalhada para esta versao.")}</p>`;
const parameterMarkup = parameters.length > 0
? parameters.map((item) => `<div class="admin-tool-inline-note rounded-4 p-3"><div class="d-flex justify-content-between gap-3"><div><div class="fw-semibold">${escapeHtml(item.name || "parametro")}</div><div class="small text-secondary mt-1">${escapeHtml(item.description || "")}</div></div><span class="badge rounded-pill bg-body-tertiary text-secondary border">${escapeHtml(item.parameter_type || "string")}${item.required ? " *" : ""}</span></div></div>`).join("")
: `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Esta versao nao declarou parametros de entrada.</div>`;
detailMeta.innerHTML = `
<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">
<div><strong>Tool:</strong> ${escapeHtml(payload?.tool_name || "-")}</div>
<div><strong>Dominio:</strong> ${escapeHtml(payload?.domain || "-")}</div>
<div><strong>Owner:</strong> ${escapeHtml(payload?.owner_name || "Nao informado")}</div>
<div><strong>Gate atual:</strong> ${escapeHtml(humanGate?.current_gate || payload?.queue_entry?.gate || "governance_required")}</div>
<div><strong>Modulo:</strong> ${escapeHtml(payload?.generated_module || "-")}</div>
<div><strong>Entrypoint:</strong> ${escapeHtml(payload?.generated_callable || "run")}</div>
<div><strong>Resumo da pipeline:</strong> ${escapeHtml(payload?.automated_validation_summary || "Sem resumo de validacao automatica.")}</div>
</div>
${parameterMarkup}
<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary"><strong>Objetivo de negocio:</strong> ${escapeHtml(payload?.business_goal || "Nao informado")}</div>
`;
validationList.innerHTML = validations.length > 0
? validations.map((item) => {
const issues = Array.isArray(item?.blocking_issues) && item.blocking_issues.length > 0
? `<div class="small text-secondary mt-2"><strong>Bloqueios:</strong> ${escapeHtml(item.blocking_issues.join("; "))}</div>`
: `<div class="small text-secondary mt-2">Sem bloqueios nesta checagem.</div>`;
return `<div class="admin-tool-inline-note rounded-4 p-3"><div class="d-flex justify-content-between align-items-start gap-3"><div><div class="fw-semibold">${escapeHtml(item.label || item.key || "Validacao")}</div><div class="small text-secondary mt-1">${escapeHtml(item.summary || "")}</div>${issues}</div><span class="badge rounded-pill ${String(item.status || "").toLowerCase() === "passed" ? "bg-success-subtle text-success-emphasis border border-success-subtle" : "bg-danger-subtle text-danger-emphasis border border-danger-subtle"}">${escapeHtml(item.status || "pendente")}</span></div></div>`;
}).join("")
: `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Nenhuma validacao automatica registrada para esta versao.</div>`;
historyList.innerHTML = history.length > 0
? history.map((item) => {
const statusTransition = item?.previous_status || item?.current_status
? `<div class="small text-secondary mt-2"><strong>Status:</strong> ${escapeHtml(item.previous_status || "-")} -> ${escapeHtml(item.current_status || "-")}</div>`
: "";
const decisionNotesMarkup = item?.decision_notes
? `<div class="small text-secondary mt-2"><strong>Parecer:</strong> ${escapeHtml(item.decision_notes)}</div>`
: "";
const reviewedMarkup = item?.reviewed_generated_code === true
? `<div class="small text-secondary mt-2"><strong>Codigo revisado:</strong> confirmado</div>`
: "";
const actorMarkup = item?.actor_name
? `<div class="small text-secondary mt-1">${escapeHtml(item.actor_name)}${item?.actor_role ? ` ? ${escapeHtml(item.actor_role)}` : ""}${item?.recorded_at ? ` ? ${escapeHtml(formatDateTime(item.recorded_at))}` : ""}</div>`
: "";
return `<div class="admin-tool-inline-note rounded-4 p-3"><div class="fw-semibold">${escapeHtml(item.label || "Governanca registrada")}</div><div class="small text-secondary mt-1">${escapeHtml(item.summary || "")}</div>${actorMarkup}${statusTransition}${decisionNotesMarkup}${reviewedMarkup}</div>`;
}).join("")
: `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Nenhuma decisao humana registrada ainda para esta versao.</div>`;
nextStepsList.innerHTML = nextSteps.length > 0
? nextSteps.map((item) => `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">${escapeHtml(item)}</div>`).join("")
: `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Nenhum proximo passo retornado para esta versao.</div>`;
if (codeField instanceof HTMLTextAreaElement) {
codeField.value = hasSourceCode
? String(payload.generated_source_code)
: "A pipeline ainda nao registrou o codigo completo gerado para esta versao.";
}
if (decisionNotes instanceof HTMLTextAreaElement) {
decisionNotes.value = "";
}
if (reviewedGeneratedCode instanceof HTMLInputElement) {
reviewedGeneratedCode.checked = false;
}
if (decisionHint instanceof HTMLElement) {
decisionHint.textContent = buildDecisionHint(humanGate, hasSourceCode);
}
configureActionPanel(humanGate, hasSourceCode);
}
function configureActionPanel(humanGate, hasSourceCode) {
lastRenderedHumanGate = humanGate;
lastRenderedHasSourceCode = hasSourceCode;
configureActionButton(reviewButton, Boolean(humanGate?.review_action_available) && hasSourceCode);
configureActionButton(approveButton, Boolean(humanGate?.approval_action_available));
configureActionButton(publishButton, Boolean(humanGate?.publication_action_available));
configureActionButton(deactivateButton, Boolean(humanGate?.deactivation_action_available));
configureActionButton(rollbackButton, Boolean(humanGate?.rollback_action_available));
const notesEnabled = Boolean(humanGate?.requires_decision_notes);
if (decisionNotes instanceof HTMLTextAreaElement) {
decisionNotes.disabled = !notesEnabled;
if (!notesEnabled) {
decisionNotes.value = "";
}
}
if (reviewedGeneratedCode instanceof HTMLInputElement) {
reviewedGeneratedCode.disabled = !Boolean(humanGate?.requires_code_review_confirmation);
if (reviewedGeneratedCode.disabled) {
reviewedGeneratedCode.checked = false;
}
}
}
function configureActionButton(button, isEnabled) {
if (!(button instanceof HTMLButtonElement)) {
return;
}
const defaultLabel = button.dataset.defaultLabel || button.textContent || "";
button.textContent = defaultLabel;
button.disabled = !isEnabled;
}
function buildDecisionHint(humanGate, hasSourceCode) {
if (!humanGate) {
return "As notas da decisao ficam persistidas na trilha administrativa da versao.";
}
if (humanGate.review_action_available && !hasSourceCode) {
return "A revisao humana fica habilitada assim que o codigo completo gerado estiver disponivel para leitura.";
}
if (humanGate.review_action_available) {
return "Para validar a versao, registre o parecer e confirme explicitamente que o codigo completo foi revisado.";
}
if (humanGate.approval_action_available) {
return "A aprovacao formal ainda exige um parecer explicito da diretoria antes da publicacao.";
}
if (humanGate.publication_action_available) {
return "A revisao e a aprovacao humanas ja ficaram registradas. Agora a diretoria pode publicar a versao no catalogo.";
}
if (humanGate.deactivation_action_available && humanGate.rollback_action_available) {
return `A versao esta ativa. Registre um parecer para desativar a publicacao ou executar rollback para v${escapeHtml(String(humanGate.rollback_target_version_number || "?"))}.`;
}
if (humanGate.deactivation_action_available) {
return "A versao esta ativa. Registre um parecer para desativar a publicacao ativa com trilha auditavel.";
}
return "As notas da decisao ficam persistidas na trilha administrativa da versao.";
}
}
function mountToolIntakePage(page) {

@ -483,7 +483,10 @@ class AdminPanelToolsWebTests(unittest.TestCase):
},
)
version_id = intake_response.json()["draft_preview"]["version_id"]
response = client.post(f"/admin/panel/tools/review-queue/{version_id}/review")
response = client.post(
f"/admin/panel/tools/review-queue/{version_id}/review",
json={"decision_notes": "Parecer inicial da diretoria para a revisao humana.", "reviewed_generated_code": True},
)
finally:
app.dependency_overrides.clear()
@ -521,6 +524,42 @@ class AdminPanelToolsWebTests(unittest.TestCase):
self.assertEqual(payload["items"][0]["gate"], "generation_pipeline_required")
self.assertEqual(payload["items"][0]["version_number"], 1)
def test_panel_tools_review_detail_returns_generated_source_for_diretor_session(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
try:
intake_response = client.post(
"/admin/panel/tools/drafts/intake",
json={
"domain": "locacao",
"tool_name": "emitir_resumo_locacao",
"display_name": "Emitir resumo de locacao",
"description": "Resume contratos de locacao com filtros operacionais para o time interno.",
"business_goal": "Dar visibilidade rapida aos contratos e aos principais dados da locacao.",
"parameters": [
{
"name": "contrato_id",
"parameter_type": "string",
"description": "Identificador do contrato consultado.",
"required": True,
}
],
},
)
version_id = intake_response.json()["draft_preview"]["version_id"]
pipeline_response = client.post(f"/admin/panel/tools/pipeline/{version_id}/run")
response = client.get(f"/admin/panel/tools/review-queue/{version_id}")
finally:
app.dependency_overrides.clear()
self.assertEqual(intake_response.status_code, 200)
self.assertEqual(pipeline_response.status_code, 200)
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["tool_name"], "emitir_resumo_locacao")
self.assertTrue(payload["human_gate"]["review_action_available"])
self.assertIn("async def run", payload["generated_source_code"])
self.assertEqual(len(payload["automated_validations"]), 4)
def test_panel_tools_collaborator_can_run_generation_pipeline_after_manual_intake(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
try:
@ -665,10 +704,19 @@ class AdminPanelToolsWebTests(unittest.TestCase):
)
version_id = intake_response.json()["draft_preview"]["version_id"]
publish_before_approval = client.post(f"/admin/panel/tools/publications/{version_id}/publish")
review_before_pipeline = client.post(f"/admin/panel/tools/review-queue/{version_id}/review")
review_before_pipeline = client.post(
f"/admin/panel/tools/review-queue/{version_id}/review",
json={"decision_notes": "Tentativa de revisao antes da pipeline.", "reviewed_generated_code": True},
)
pipeline_response = client.post(f"/admin/panel/tools/pipeline/{version_id}/run")
review_response = client.post(f"/admin/panel/tools/review-queue/{version_id}/review")
approve_response = client.post(f"/admin/panel/tools/review-queue/{version_id}/approve")
review_response = client.post(
f"/admin/panel/tools/review-queue/{version_id}/review",
json={"decision_notes": "Analisei o codigo completo gerado antes da validacao humana.", "reviewed_generated_code": True},
)
approve_response = client.post(
f"/admin/panel/tools/review-queue/{version_id}/approve",
json={"decision_notes": "Aprovacao formal da diretoria para seguir com a publicacao.", "reviewed_generated_code": True},
)
pre_publications = client.get("/admin/panel/tools/publications")
publish_response = client.post(f"/admin/panel/tools/publications/{version_id}/publish")
final_publications = client.get("/admin/panel/tools/publications")
@ -714,5 +762,109 @@ class AdminPanelToolsWebTests(unittest.TestCase):
self.assertEqual(publication["parameters"][0]["name"], "contrato_id")
def test_panel_tools_director_can_deactivate_active_publication(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
try:
intake_response = client.post(
"/admin/panel/tools/drafts/intake",
json={
"domain": "locacao",
"tool_name": "emitir_resumo_locacao",
"display_name": "Emitir resumo de locacao",
"description": "Resume contratos de locacao com filtros operacionais para o time interno.",
"business_goal": "Dar visibilidade rapida aos contratos e aos principais dados da locacao.",
"parameters": [],
},
)
version_id = intake_response.json()["draft_preview"]["version_id"]
client.post(f"/admin/panel/tools/pipeline/{version_id}/run")
client.post(
f"/admin/panel/tools/review-queue/{version_id}/review",
json={"decision_notes": "Analisei o codigo completo antes da ativacao.", "reviewed_generated_code": True},
)
client.post(
f"/admin/panel/tools/review-queue/{version_id}/approve",
json={"decision_notes": "Aprovacao formal para disponibilizar a ferramenta."},
)
client.post(f"/admin/panel/tools/publications/{version_id}/publish")
deactivate_response = client.post(
f"/admin/panel/tools/publications/{version_id}/deactivate",
json={"decision_notes": "Desativacao controlada da ferramenta ativa apos teste concluido."},
)
publications_response = client.get("/admin/panel/tools/publications")
finally:
app.dependency_overrides.clear()
self.assertEqual(deactivate_response.status_code, 200)
self.assertEqual(deactivate_response.json()["status"], "archived")
self.assertIsNone(deactivate_response.json()["queue_entry"])
self.assertEqual(publications_response.status_code, 200)
self.assertNotIn("emitir_resumo_locacao", [item["tool_name"] for item in publications_response.json()["publications"]])
def test_panel_tools_director_can_rollback_active_publication(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
try:
first_intake = client.post(
"/admin/panel/tools/drafts/intake",
json={
"domain": "locacao",
"tool_name": "emitir_resumo_locacao",
"display_name": "Emitir resumo de locacao",
"description": "Resume contratos de locacao com filtros operacionais para o time interno.",
"business_goal": "Dar visibilidade rapida aos contratos e aos principais dados da locacao.",
"parameters": [],
},
)
first_version_id = first_intake.json()["draft_preview"]["version_id"]
client.post(f"/admin/panel/tools/pipeline/{first_version_id}/run")
client.post(
f"/admin/panel/tools/review-queue/{first_version_id}/review",
json={"decision_notes": "Primeira revisao completa do codigo gerado.", "reviewed_generated_code": True},
)
client.post(
f"/admin/panel/tools/review-queue/{first_version_id}/approve",
json={"decision_notes": "Primeira aprovacao formal da diretoria."},
)
client.post(f"/admin/panel/tools/publications/{first_version_id}/publish")
second_intake = client.post(
"/admin/panel/tools/drafts/intake",
json={
"domain": "locacao",
"tool_name": "emitir_resumo_locacao",
"display_name": "Emitir resumo de locacao",
"description": "Resume contratos de locacao com mais contexto operacional para o time interno.",
"business_goal": "Dar visibilidade rapida aos contratos com filtros adicionais.",
"parameters": [],
},
)
second_version_id = second_intake.json()["draft_preview"]["version_id"]
client.post(f"/admin/panel/tools/pipeline/{second_version_id}/run")
client.post(
f"/admin/panel/tools/review-queue/{second_version_id}/review",
json={"decision_notes": "Segunda revisao completa do codigo gerado.", "reviewed_generated_code": True},
)
client.post(
f"/admin/panel/tools/review-queue/{second_version_id}/approve",
json={"decision_notes": "Segunda aprovacao formal da diretoria."},
)
client.post(f"/admin/panel/tools/publications/{second_version_id}/publish")
rollback_response = client.post(
f"/admin/panel/tools/publications/{second_version_id}/rollback",
json={"decision_notes": "Rollback controlado para restaurar a versao anterior estavel."},
)
publications_response = client.get("/admin/panel/tools/publications")
finally:
app.dependency_overrides.clear()
self.assertEqual(rollback_response.status_code, 200)
self.assertEqual(rollback_response.json()["status"], "active")
self.assertEqual(rollback_response.json()["version_id"], first_version_id)
publication = next(item for item in publications_response.json()["publications"] if item["tool_name"] == "emitir_resumo_locacao")
self.assertEqual(publication["version_id"], first_version_id)
self.assertTrue(publication["deactivation_action_available"])
if __name__ == "__main__":
unittest.main()

@ -944,6 +944,8 @@ class AdminToolManagementServiceTests(unittest.TestCase):
reviewer_staff_account_id=99,
reviewer_name="Diretoria",
reviewer_role=StaffRole.DIRETOR,
decision_notes="Aguardando a geracao controlada da funcao.",
reviewed_generated_code=True,
)
def test_director_must_review_approve_and_publish_before_activation(self):
@ -979,12 +981,15 @@ class AdminToolManagementServiceTests(unittest.TestCase):
reviewer_staff_account_id=99,
reviewer_name="Diretoria",
reviewer_role=StaffRole.DIRETOR,
decision_notes="Analisei o codigo completo gerado e a estrutura esta aderente ao fluxo governado.",
reviewed_generated_code=True,
)
approve_payload = self.service.approve_version(
version_id,
approver_staff_account_id=99,
approver_name="Diretoria",
approver_role=StaffRole.DIRETOR,
decision_notes="Aprovacao formal registrada apos revisao tecnica e leitura integral do codigo.",
)
publish_payload = self.service.publish_version(
version_id,
@ -1033,6 +1038,61 @@ class AdminToolManagementServiceTests(unittest.TestCase):
publisher_role=StaffRole.DIRETOR,
)
def test_build_review_detail_payload_exposes_generated_source_and_human_history(self):
intake_payload = self.service.create_draft_submission(
{
"domain": "revisao",
"tool_name": "consultar_revisao_aberta",
"display_name": "Consultar revisao aberta",
"description": "Consulta revisoes abertas com filtros administrativos para a oficina.",
"business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.",
"parameters": [
{
"name": "placa",
"parameter_type": "string",
"description": "Placa usada na busca da revisao.",
"required": True,
}
],
},
owner_staff_account_id=8,
owner_name="Operacao de Oficina",
)
version_id = intake_payload["draft_preview"]["version_id"]
self.service.run_generation_pipeline(
version_id,
runner_staff_account_id=8,
runner_name="Operacao de Oficina",
runner_role=StaffRole.COLABORADOR,
)
self.service.review_version(
version_id,
reviewer_staff_account_id=99,
reviewer_name="Diretoria",
reviewer_role=StaffRole.DIRETOR,
decision_notes="Analise completa do codigo gerado antes da validacao humana.",
reviewed_generated_code=True,
)
self.service.approve_version(
version_id,
approver_staff_account_id=99,
approver_name="Diretoria",
approver_role=StaffRole.DIRETOR,
decision_notes="Aprovacao formal da versao apos revisao humana detalhada.",
)
payload = self.service.build_review_detail_payload(version_id)
self.assertEqual(payload["status"], ToolLifecycleStatus.APPROVED)
self.assertEqual(payload["human_gate"]["publication_action_available"], True)
self.assertIn("async def run", payload["generated_source_code"])
self.assertEqual(len(payload["automated_validations"]), 4)
self.assertEqual(len(payload["decision_history"]), 2)
self.assertEqual(payload["decision_history"][0]["action_key"], ToolArtifactKind.DIRECTOR_REVIEW.value)
self.assertTrue(payload["decision_history"][0]["reviewed_generated_code"])
self.assertIn("aprovacao formal", payload["decision_history"][1]["decision_notes"].lower())
def test_publishing_new_version_archives_previous_active_version(self):
first_intake = self.service.create_draft_submission(
{
@ -1048,8 +1108,21 @@ class AdminToolManagementServiceTests(unittest.TestCase):
)
first_version_id = first_intake["draft_preview"]["version_id"]
self.service.run_generation_pipeline(first_version_id, runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR)
self.service.review_version(first_version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR)
self.service.approve_version(first_version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR)
self.service.review_version(
first_version_id,
reviewer_staff_account_id=99,
reviewer_name="Diretoria",
reviewer_role=StaffRole.DIRETOR,
decision_notes="Primeira versao revisada com leitura integral do codigo gerado.",
reviewed_generated_code=True,
)
self.service.approve_version(
first_version_id,
approver_staff_account_id=99,
approver_name="Diretoria",
approver_role=StaffRole.DIRETOR,
decision_notes="Primeira versao aprovada para ativacao controlada.",
)
self.service.publish_version(first_version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR)
second_intake = self.service.create_draft_submission(
@ -1066,8 +1139,21 @@ class AdminToolManagementServiceTests(unittest.TestCase):
)
second_version_id = second_intake["draft_preview"]["version_id"]
self.service.run_generation_pipeline(second_version_id, runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR)
self.service.review_version(second_version_id, reviewer_staff_account_id=99, reviewer_name="Diretoria", reviewer_role=StaffRole.DIRETOR)
self.service.approve_version(second_version_id, approver_staff_account_id=99, approver_name="Diretoria", approver_role=StaffRole.DIRETOR)
self.service.review_version(
second_version_id,
reviewer_staff_account_id=99,
reviewer_name="Diretoria",
reviewer_role=StaffRole.DIRETOR,
decision_notes="Nova versao revisada com comparativo do codigo completo gerado.",
reviewed_generated_code=True,
)
self.service.approve_version(
second_version_id,
approver_staff_account_id=99,
approver_name="Diretoria",
approver_role=StaffRole.DIRETOR,
decision_notes="Nova versao aprovada para substituir a publicacao anterior.",
)
self.service.publish_version(second_version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR)
versions_by_number = {version.version_number: version for version in self.version_repository.versions}
@ -1078,6 +1164,148 @@ class AdminToolManagementServiceTests(unittest.TestCase):
self.assertEqual(metadata_by_number[2].status, ToolLifecycleStatus.ACTIVE)
def test_deactivating_active_version_archives_publication_and_removes_tool_from_catalog(self):
intake = self.service.create_draft_submission(
{
"domain": "locacao",
"tool_name": "emitir_resumo_locacao",
"display_name": "Emitir resumo de locacao",
"description": "Resume contratos de locacao com filtros operacionais para o time interno.",
"business_goal": "Dar visibilidade rapida aos contratos e aos principais dados da locacao.",
"parameters": [],
},
owner_staff_account_id=7,
owner_name="Equipe Interna",
)
version_id = intake["draft_preview"]["version_id"]
self.service.run_generation_pipeline(version_id, runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR)
self.service.review_version(
version_id,
reviewer_staff_account_id=99,
reviewer_name="Diretoria",
reviewer_role=StaffRole.DIRETOR,
decision_notes="Analisei a versao ativa antes da desativacao controlada.",
reviewed_generated_code=True,
)
self.service.approve_version(
version_id,
approver_staff_account_id=99,
approver_name="Diretoria",
approver_role=StaffRole.DIRETOR,
decision_notes="Aprovacao formal para ativar e depois validar a desativacao controlada.",
)
self.service.publish_version(version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR)
payload = self.service.deactivate_version(
version_id,
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes="Desativacao controlada da tool apos encerramento temporario do uso.",
)
self.assertEqual(payload["status"], ToolLifecycleStatus.ARCHIVED)
self.assertIsNone(payload["queue_entry"])
detail = self.service.build_review_detail_payload(version_id)
self.assertEqual(detail["status"], ToolLifecycleStatus.ARCHIVED)
self.assertFalse(detail["human_gate"]["deactivation_action_available"])
self.assertFalse(detail["human_gate"]["rollback_action_available"])
self.assertEqual(detail["decision_history"][-1]["action_key"], ToolArtifactKind.PUBLICATION_DEACTIVATION.value)
publications = self.service.build_publications_payload()
self.assertNotIn("emitir_resumo_locacao", [item["tool_name"] for item in publications["publications"]])
def test_rollback_restores_latest_archived_version_into_active_catalog(self):
first_intake = self.service.create_draft_submission(
{
"domain": "vendas",
"tool_name": "consultar_funil_comercial",
"display_name": "Consultar funil comercial",
"description": "Consulta o funil comercial consolidado para acompanhamento administrativo.",
"business_goal": "Dar visibilidade ao time interno sobre os principais gargalos do funil.",
"parameters": [],
},
owner_staff_account_id=7,
owner_name="Equipe Interna",
)
first_version_id = first_intake["draft_preview"]["version_id"]
self.service.run_generation_pipeline(first_version_id, runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR)
self.service.review_version(
first_version_id,
reviewer_staff_account_id=99,
reviewer_name="Diretoria",
reviewer_role=StaffRole.DIRETOR,
decision_notes="Primeira versao revisada antes da futura ativacao controlada.",
reviewed_generated_code=True,
)
self.service.approve_version(
first_version_id,
approver_staff_account_id=99,
approver_name="Diretoria",
approver_role=StaffRole.DIRETOR,
decision_notes="Primeira versao aprovada para publicacao inicial.",
)
self.service.publish_version(first_version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR)
second_intake = self.service.create_draft_submission(
{
"domain": "vendas",
"tool_name": "consultar_funil_comercial",
"display_name": "Consultar funil comercial",
"description": "Consulta o funil comercial consolidado com campos adicionais para acompanhamento administrativo.",
"business_goal": "Dar visibilidade ao time interno sobre gargalos, volume e conversao do funil.",
"parameters": [],
},
owner_staff_account_id=7,
owner_name="Equipe Interna",
)
second_version_id = second_intake["draft_preview"]["version_id"]
self.service.run_generation_pipeline(second_version_id, runner_staff_account_id=7, runner_name="Equipe Interna", runner_role=StaffRole.COLABORADOR)
self.service.review_version(
second_version_id,
reviewer_staff_account_id=99,
reviewer_name="Diretoria",
reviewer_role=StaffRole.DIRETOR,
decision_notes="Nova versao revisada com leitura integral antes da substituicao.",
reviewed_generated_code=True,
)
self.service.approve_version(
second_version_id,
approver_staff_account_id=99,
approver_name="Diretoria",
approver_role=StaffRole.DIRETOR,
decision_notes="Nova versao aprovada para substituir a publicacao anterior.",
)
self.service.publish_version(second_version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR)
active_detail = self.service.build_review_detail_payload(second_version_id)
self.assertTrue(active_detail["human_gate"]["deactivation_action_available"])
self.assertTrue(active_detail["human_gate"]["rollback_action_available"])
self.assertEqual(active_detail["human_gate"]["rollback_target_version_number"], 1)
payload = self.service.rollback_version(
second_version_id,
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes="Rollback controlado para restaurar a versao anterior mais estavel.",
)
versions_by_number = {version.version_number: version for version in self.version_repository.versions}
metadata_by_number = {metadata.version_number: metadata for metadata in self.metadata_repository.metadata_entries}
self.assertEqual(payload["status"], ToolLifecycleStatus.ACTIVE)
self.assertEqual(payload["version_id"], first_version_id)
self.assertEqual(versions_by_number[1].status, ToolLifecycleStatus.ACTIVE)
self.assertEqual(metadata_by_number[1].status, ToolLifecycleStatus.ACTIVE)
self.assertEqual(versions_by_number[2].status, ToolLifecycleStatus.ARCHIVED)
self.assertEqual(metadata_by_number[2].status, ToolLifecycleStatus.ARCHIVED)
restored_detail = self.service.build_review_detail_payload(first_version_id)
self.assertEqual(restored_detail["decision_history"][-1]["action_key"], ToolArtifactKind.PUBLICATION_ROLLBACK.value)
publications = self.service.build_publications_payload()
restored_publication = next(item for item in publications["publications"] if item["tool_name"] == "consultar_funil_comercial")
self.assertEqual(restored_publication["version_id"], first_version_id)
self.assertTrue(restored_publication["deactivation_action_available"])
class AdminToolManagementTransactionalPersistenceTests(unittest.TestCase):
def setUp(self):
self.engine = create_engine("sqlite:///:memory:")

@ -504,6 +504,10 @@ class AdminToolsWebTests(unittest.TestCase):
response = client.post(
f"/admin/tools/review-queue/{version_id}/review",
headers={"Authorization": "Bearer token"},
json={
"decision_notes": "Parecer inicial da diretoria para a revisao humana.",
"reviewed_generated_code": True,
},
)
finally:
app.dependency_overrides.clear()
@ -544,6 +548,49 @@ class AdminToolsWebTests(unittest.TestCase):
self.assertEqual(payload["items"][0]["version_number"], 1)
self.assertIn("approved", payload["supported_statuses"])
def test_tools_review_detail_returns_generated_source_for_diretor(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
try:
intake_response = client.post(
"/admin/tools/drafts/intake",
headers={"Authorization": "Bearer token"},
json={
"domain": "revisao",
"tool_name": "consultar_revisao_aberta",
"display_name": "Consultar revisao aberta",
"description": "Consulta revisoes abertas com filtros administrativos para a oficina.",
"business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.",
"parameters": [
{
"name": "placa",
"parameter_type": "string",
"description": "Placa usada na busca da revisao.",
"required": True,
}
],
},
)
version_id = intake_response.json()["draft_preview"]["version_id"]
pipeline_response = client.post(
f"/admin/tools/pipeline/{version_id}/run",
headers={"Authorization": "Bearer token"},
)
response = client.get(
f"/admin/tools/review-queue/{version_id}",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(intake_response.status_code, 200)
self.assertEqual(pipeline_response.status_code, 200)
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["tool_name"], "consultar_revisao_aberta")
self.assertTrue(payload["human_gate"]["review_action_available"])
self.assertIn("async def run", payload["generated_source_code"])
self.assertEqual(len(payload["automated_validations"]), 4)
def test_tools_collaborator_can_run_generation_pipeline_after_manual_intake(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
try:
@ -707,6 +754,10 @@ class AdminToolsWebTests(unittest.TestCase):
review_before_pipeline = client.post(
f"/admin/tools/review-queue/{version_id}/review",
headers={"Authorization": "Bearer token"},
json={
"decision_notes": "Tentativa de revisao antes da pipeline.",
"reviewed_generated_code": True,
},
)
pipeline_response = client.post(
f"/admin/tools/pipeline/{version_id}/run",
@ -715,10 +766,18 @@ class AdminToolsWebTests(unittest.TestCase):
review_response = client.post(
f"/admin/tools/review-queue/{version_id}/review",
headers={"Authorization": "Bearer token"},
json={
"decision_notes": "Analisei o codigo completo gerado antes da validacao humana.",
"reviewed_generated_code": True,
},
)
approve_response = client.post(
f"/admin/tools/review-queue/{version_id}/approve",
headers={"Authorization": "Bearer token"},
json={
"decision_notes": "Aprovacao formal da diretoria para seguir com a publicacao.",
"reviewed_generated_code": True,
},
)
pre_publications = client.get("/admin/tools/publications", headers={"Authorization": "Bearer token"})
publish_response = client.post(
@ -769,5 +828,120 @@ class AdminToolsWebTests(unittest.TestCase):
self.assertEqual(publication["parameters"][0]["parameter_type"], "string")
def test_tools_director_can_deactivate_active_publication(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
try:
intake_response = client.post(
"/admin/tools/drafts/intake",
headers={"Authorization": "Bearer token"},
json={
"domain": "revisao",
"tool_name": "consultar_revisao_aberta",
"display_name": "Consultar revisao aberta",
"description": "Consulta revisoes abertas com filtros administrativos para a oficina.",
"business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.",
"parameters": [],
},
)
version_id = intake_response.json()["draft_preview"]["version_id"]
client.post(f"/admin/tools/pipeline/{version_id}/run", headers={"Authorization": "Bearer token"})
client.post(
f"/admin/tools/review-queue/{version_id}/review",
headers={"Authorization": "Bearer token"},
json={"decision_notes": "Analisei o codigo completo antes da ativacao.", "reviewed_generated_code": True},
)
client.post(
f"/admin/tools/review-queue/{version_id}/approve",
headers={"Authorization": "Bearer token"},
json={"decision_notes": "Aprovacao formal para disponibilizar a ferramenta."},
)
client.post(f"/admin/tools/publications/{version_id}/publish", headers={"Authorization": "Bearer token"})
deactivate_response = client.post(
f"/admin/tools/publications/{version_id}/deactivate",
headers={"Authorization": "Bearer token"},
json={"decision_notes": "Desativacao controlada da ferramenta ativa apos teste concluido."},
)
publications_response = client.get("/admin/tools/publications", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(deactivate_response.status_code, 200)
self.assertEqual(deactivate_response.json()["status"], "archived")
self.assertIsNone(deactivate_response.json()["queue_entry"])
self.assertEqual(publications_response.status_code, 200)
self.assertNotIn("consultar_revisao_aberta", [item["tool_name"] for item in publications_response.json()["publications"]])
def test_tools_director_can_rollback_active_publication(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.DIRETOR)
try:
first_intake = client.post(
"/admin/tools/drafts/intake",
headers={"Authorization": "Bearer token"},
json={
"domain": "revisao",
"tool_name": "consultar_revisao_aberta",
"display_name": "Consultar revisao aberta",
"description": "Consulta revisoes abertas com filtros administrativos para a oficina.",
"business_goal": "Ajudar o time a localizar revisoes abertas com mais contexto operacional.",
"parameters": [],
},
)
first_version_id = first_intake.json()["draft_preview"]["version_id"]
client.post(f"/admin/tools/pipeline/{first_version_id}/run", headers={"Authorization": "Bearer token"})
client.post(
f"/admin/tools/review-queue/{first_version_id}/review",
headers={"Authorization": "Bearer token"},
json={"decision_notes": "Primeira revisao completa do codigo gerado.", "reviewed_generated_code": True},
)
client.post(
f"/admin/tools/review-queue/{first_version_id}/approve",
headers={"Authorization": "Bearer token"},
json={"decision_notes": "Primeira aprovacao formal da diretoria."},
)
client.post(f"/admin/tools/publications/{first_version_id}/publish", headers={"Authorization": "Bearer token"})
second_intake = client.post(
"/admin/tools/drafts/intake",
headers={"Authorization": "Bearer token"},
json={
"domain": "revisao",
"tool_name": "consultar_revisao_aberta",
"display_name": "Consultar revisao aberta",
"description": "Consulta revisoes abertas com mais contexto operacional para a oficina.",
"business_goal": "Ajudar o time a localizar revisoes abertas com filtros extras.",
"parameters": [],
},
)
second_version_id = second_intake.json()["draft_preview"]["version_id"]
client.post(f"/admin/tools/pipeline/{second_version_id}/run", headers={"Authorization": "Bearer token"})
client.post(
f"/admin/tools/review-queue/{second_version_id}/review",
headers={"Authorization": "Bearer token"},
json={"decision_notes": "Segunda revisao completa do codigo gerado.", "reviewed_generated_code": True},
)
client.post(
f"/admin/tools/review-queue/{second_version_id}/approve",
headers={"Authorization": "Bearer token"},
json={"decision_notes": "Segunda aprovacao formal da diretoria."},
)
client.post(f"/admin/tools/publications/{second_version_id}/publish", headers={"Authorization": "Bearer token"})
rollback_response = client.post(
f"/admin/tools/publications/{second_version_id}/rollback",
headers={"Authorization": "Bearer token"},
json={"decision_notes": "Rollback controlado para restaurar a versao anterior estavel."},
)
publications_response = client.get("/admin/tools/publications", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(rollback_response.status_code, 200)
self.assertEqual(rollback_response.json()["status"], "active")
self.assertEqual(rollback_response.json()["version_id"], first_version_id)
publication = next(item for item in publications_response.json()["publications"] if item["tool_name"] == "consultar_revisao_aberta")
self.assertEqual(publication["version_id"], first_version_id)
self.assertTrue(publication["deactivation_action_available"])
if __name__ == "__main__":
unittest.main()

@ -158,6 +158,10 @@ class AdminViewBootstrapTests(unittest.TestCase):
self.assertIn('data-contracts-endpoint="/panel/tools/contracts"', response.text)
self.assertIn('data-review-queue-endpoint="/panel/tools/review-queue"', response.text)
self.assertIn('data-publications-endpoint="/panel/tools/publications"', response.text)
self.assertIn('data-tool-review-code', response.text)
self.assertIn('data-tool-review-decision-notes', response.text)
self.assertIn('data-tool-review-action="deactivate"', response.text)
self.assertIn('data-tool-review-action="rollback"', response.text)
self.assertNotIn("Abrir login administrativo", response.text)
def test_collaborator_management_page_redirects_to_login_without_session(self):

Loading…
Cancel
Save