feat(admin): alinhar esteira governada de propostas e iteracoes de tools

Implementa a nova semantica da esteira administrativa para propostas de tools, separando triagem humana, execucao da pipeline, revisao de codigo e ativacao governada.

Tambem consolida iteracoes de geracao na mesma versao funcional, conecta refatoracao guiada por feedback da diretoria e adiciona compatibilidade com artefatos legados de revisao, aprovacao e source publicado no runtime.
feat/self-evolving-tools-foundation
parent 7e380a9c65
commit 3a7bfcf59b

@ -15,6 +15,7 @@ from admin_app.api.schemas import (
AdminToolGovernanceTransitionResponse,
AdminToolManagementActionResponse,
AdminToolOverviewResponse,
AdminToolOptionalGovernanceDecisionRequest,
AdminToolPublicationListResponse,
AdminToolReviewDecisionRequest,
AdminToolReviewDetailResponse,
@ -154,6 +155,66 @@ def panel_tool_pipeline_run(
return _build_pipeline_response(payload)
@router.post(
"/drafts/{version_id}/authorize-generation",
response_model=AdminToolGovernanceTransitionResponse,
)
def panel_tool_draft_authorize_generation(
version_id: str,
decision: AdminToolGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.authorize_generation(
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(
"/drafts/{version_id}/close",
response_model=AdminToolGovernanceTransitionResponse,
)
def panel_tool_draft_close(
version_id: str,
decision: AdminToolOptionalGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.close_proposal(
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.get(
"/review-queue",
response_model=AdminToolReviewQueueResponse,
@ -224,6 +285,66 @@ def panel_tool_review_queue_review(
return _build_governance_transition_response(payload)
@router.post(
"/review-queue/{version_id}/request-changes",
response_model=AdminToolGovernanceTransitionResponse,
)
def panel_tool_review_queue_request_changes(
version_id: str,
decision: AdminToolGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.request_changes(
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(
"/review-queue/{version_id}/close",
response_model=AdminToolGovernanceTransitionResponse,
)
def panel_tool_review_queue_close(
version_id: str,
decision: AdminToolOptionalGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.close_proposal(
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(
"/review-queue/{version_id}/approve",
response_model=AdminToolGovernanceTransitionResponse,
@ -414,6 +535,7 @@ def _build_review_detail_response(payload: dict) -> AdminToolReviewDetailRespons
generated_callable=payload["generated_callable"],
generated_source_code=payload["generated_source_code"],
execution=payload.get("execution"),
generation_context=payload["generation_context"],
human_gate=payload["human_gate"],
decision_history=payload["decision_history"],
next_steps=payload["next_steps"],

@ -15,6 +15,7 @@ from admin_app.api.schemas import (
AdminToolGovernanceTransitionResponse,
AdminToolManagementActionResponse,
AdminToolOverviewResponse,
AdminToolOptionalGovernanceDecisionRequest,
AdminToolPublicationListResponse,
AdminToolReviewDecisionRequest,
AdminToolReviewDetailResponse,
@ -24,6 +25,8 @@ from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from admin_app.services import ToolManagementService
from shared.contracts import AdminPermission, StaffRole, role_has_permission
# API de intake (processo de captação e triagem inicial de demandas, requisitos ou solicitações antes do início efetivo do projeto), pipeline, review, publish, deactivate e rollback de tools.
router = APIRouter(prefix="/tools", tags=["tools"])
@ -154,6 +157,66 @@ def tool_pipeline_run(
return _build_pipeline_response(payload)
@router.post(
"/drafts/{version_id}/authorize-generation",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_draft_authorize_generation(
version_id: str,
decision: AdminToolGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.authorize_generation(
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(
"/drafts/{version_id}/close",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_draft_close(
version_id: str,
decision: AdminToolOptionalGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.close_proposal(
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.get(
"/review-queue",
response_model=AdminToolReviewQueueResponse,
@ -224,6 +287,66 @@ def tool_review_queue_review(
return _build_governance_transition_response(payload)
@router.post(
"/review-queue/{version_id}/request-changes",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_review_queue_request_changes(
version_id: str,
decision: AdminToolGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.request_changes(
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(
"/review-queue/{version_id}/close",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_review_queue_close(
version_id: str,
decision: AdminToolOptionalGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.close_proposal(
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(
"/review-queue/{version_id}/approve",
response_model=AdminToolGovernanceTransitionResponse,
@ -414,6 +537,7 @@ def _build_review_detail_response(payload: dict) -> AdminToolReviewDetailRespons
generated_callable=payload["generated_callable"],
generated_source_code=payload["generated_source_code"],
execution=payload.get("execution"),
generation_context=payload["generation_context"],
human_gate=payload["human_gate"],
decision_history=payload["decision_history"],
next_steps=payload["next_steps"],

@ -20,6 +20,7 @@ from shared.contracts import (
ToolParameterType,
)
# Contratos de request/response.
class AdminRootResponse(BaseModel):
service: str
@ -795,8 +796,24 @@ class AdminToolGovernanceDecisionRequest(BaseModel):
return value.strip()
class AdminToolOptionalGovernanceDecisionRequest(BaseModel):
decision_notes: str | None = Field(default=None, max_length=2000)
@field_validator("decision_notes", mode="before")
@classmethod
def normalize_decision_notes(cls, value: str | None) -> str | None:
if value is None:
return None
normalized = str(value).strip()
return normalized or None
class AdminToolReviewHumanGateResponse(BaseModel):
current_gate: str
authorize_generation_action_available: bool
run_pipeline_action_available: bool
request_changes_action_available: bool
close_proposal_action_available: bool
review_action_available: bool
approval_action_available: bool
publication_action_available: bool
@ -808,6 +825,18 @@ class AdminToolReviewHumanGateResponse(BaseModel):
requires_code_review_confirmation: bool
class AdminToolGenerationContextResponse(BaseModel):
latest_generation_iteration: int = Field(ge=0)
next_generation_iteration: int = Field(ge=1)
latest_generation_mode: str | None = None
next_generation_mode: str
generation_iterations_count: int = Field(ge=0)
has_previous_generation: bool
pending_change_request: bool
latest_generated_source_checksum: str | None = None
latest_change_request_notes: str | None = None
class AdminToolReviewHistoryEntryResponse(BaseModel):
action_key: str
label: str
@ -841,6 +870,7 @@ class AdminToolReviewDetailResponse(BaseModel):
generated_callable: str
generated_source_code: str
execution: AdminToolPipelineExecutionResponse | None = None
generation_context: AdminToolGenerationContextResponse
human_gate: AdminToolReviewHumanGateResponse
decision_history: list[AdminToolReviewHistoryEntryResponse] = Field(default_factory=list)
next_steps: list[str] = Field(default_factory=list)
@ -993,9 +1023,12 @@ class AdminToolDraftSubmissionPolicyResponse(BaseModel):
mode: str
submitter_role: StaffRole | None = None
submitter_can_publish_now: bool
submitter_can_authorize_generation_now: bool
direct_publication_blocked: bool
requires_generation_authorization: bool
requires_director_approval: bool
required_approver_role: StaffRole
required_generation_permission: AdminPermission
required_review_permission: AdminPermission
required_publish_permission: AdminPermission

@ -18,6 +18,9 @@ class ToolArtifactStage(str, Enum):
class ToolArtifactKind(str, Enum):
GENERATION_REQUEST = "generation_request"
VALIDATION_REPORT = "validation_report"
GENERATION_AUTHORIZATION = "generation_authorization"
GENERATION_CHANGE_REQUEST = "generation_change_request"
PROPOSAL_CLOSURE = "proposal_closure"
DIRECTOR_REVIEW = "director_review"
DIRECTOR_APPROVAL = "director_approval"
PUBLICATION_RELEASE = "publication_release"

@ -140,6 +140,9 @@ class ToolGenerationService:
description: str,
business_goal: str,
parameters: list[dict],
previous_source_code: str | None = None,
change_request_notes: str | None = None,
generation_iteration: int = 1,
) -> str:
"""Monta o prompt estruturado de geração enviado ao modelo.
@ -203,7 +206,37 @@ class ToolGenerationService:
"O bot atua em um sistema de atendimento automatizado.",
)
normalized_previous_source = str(previous_source_code or "").strip()
normalized_change_request_notes = str(change_request_notes or "").strip()
prompt_mode = "geracao_inicial"
refinement_block = ""
if normalized_previous_source and normalized_change_request_notes:
prompt_mode = "refatoracao_guiada_por_feedback"
refinement_block = (
"MODO DE EXECUCAO:\n"
"- Esta nao e uma geracao do zero. Refatore a implementacao existente.\n"
"- Preserve o contrato governado, o objetivo de negocio e os parametros da tool.\n"
"- Corrija explicitamente os pontos apontados pela revisao humana.\n\n"
"FEEDBACK HUMANO:\n"
f"{normalized_change_request_notes}\n\n"
"CODIGO ANTERIOR A SER REFATORADO:\n"
f"```python\n{normalized_previous_source}\n```\n\n"
)
elif normalized_previous_source:
prompt_mode = "regeneracao_com_contexto_previo"
refinement_block = (
"MODO DE EXECUCAO:\n"
"- Existe um codigo anterior para esta mesma versao.\n"
"- Use-o como referencia para manter continuidade e consistencia na implementacao.\n\n"
"CODIGO ANTERIOR DE REFERENCIA:\n"
f"```python\n{normalized_previous_source}\n```\n\n"
)
return (
"CONTEXTO DA EXECUCAO:\n"
f"- Iteracao de geracao: {int(generation_iteration)}\n"
f"- Modo do prompt: {prompt_mode}\n\n"
f"{refinement_block}"
"Você é um especialista em Python que gera implementações realistas de tools "
"para um bot de atendimento.\n\n"
f"CONTEXTO DO DOMÍNIO:\n{domain_context}\n\n"
@ -263,6 +296,9 @@ class ToolGenerationService:
business_goal: str,
parameters: list[dict],
preferred_model: str | None = None,
previous_source_code: str | None = None,
change_request_notes: str | None = None,
generation_iteration: int = 1,
) -> dict[str, Any]:
"""Gera o código Python da tool a partir dos metadados do draft.
@ -281,6 +317,9 @@ class ToolGenerationService:
description=description,
business_goal=business_goal,
parameters=parameters,
previous_source_code=previous_source_code,
change_request_notes=change_request_notes,
generation_iteration=generation_iteration,
)
model_sequence = self._build_model_sequence(preferred_model)

File diff suppressed because it is too large Load Diff

@ -1,4 +1,4 @@
from html import escape
from html import escape
from admin_app.view.view_models import (
AdminBotMonitoringPageView,
@ -554,9 +554,9 @@ def render_tool_review_page(
<span class="badge rounded-pill bg-white text-dark border">Governanca de tools</span>
<span class="badge rounded-pill bg-success-subtle text-success-emphasis border border-success-subtle">Revisao e ativacao</span>
</div>
<h2 class="display-5 fw-semibold mb-3">Fluxo visual de aprovacao no painel</h2>
<h2 class="display-5 fw-semibold mb-3">Fluxo governado de proposta, codigo e ativacao</h2>
<p class="lead text-secondary mb-0">
Esta tela conecta a sessao web do painel aos snapshots administrativos de tools para que o time consiga revisar a fila, conferir contratos e acompanhar o catalogo ativo.
Esta tela conecta a sessao web do painel a cada etapa da proposta: triagem para gerar, iteracoes de codigo, decisao humana e catalogo ativo do produto.
</p>
</div>
<div class="d-grid gap-2 admin-quick-actions">
@ -578,7 +578,7 @@ def render_tool_review_page(
<div class="card-body p-4">
<p class="small text-uppercase fw-semibold text-secondary mb-3">Fila de revisao</p>
<div class="display-6 fw-semibold mb-2" data-tool-review-queue-count>0</div>
<p class="text-secondary mb-0">Items aguardando leitura tecnica ou aprovacao humana.</p>
<p class="text-secondary mb-0">Propostas aguardando triagem, leitura tecnica ou aprovacao humana.</p>
</div>
</div>
</div>
@ -607,7 +607,7 @@ def render_tool_review_page(
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Pipeline visual</p>
<h3 class="h3 fw-semibold mb-2">Etapas que a tela acompanha</h3>
<p class="text-secondary mb-0">Os cards abaixo resumem o trajeto de uma tool desde a analise ate a ativacao no produto.</p>
<p class="text-secondary mb-0">Os cards abaixo resumem o trajeto da proposta, da triagem para geracao e das iteracoes de codigo ate a ativacao no produto.</p>
</div>
</div>
<div class="row g-3">
@ -623,8 +623,8 @@ def render_tool_review_page(
<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">Fila atual</p>
<h3 class="h3 fw-semibold mb-2">Revisao tecnica e aprovacao</h3>
<p class="text-secondary mb-0">A fila abaixo e lida da superficie web do painel e respeita o papel da sessao autenticada.</p>
<h3 class="h3 fw-semibold mb-2">Triagem e revisao por etapa</h3>
<p class="text-secondary mb-0">A fila abaixo mostra em que gate cada proposta esta e qual decisao humana ou tecnica vem a seguir.</p>
</div>
<span class="badge rounded-pill bg-body-tertiary text-secondary border" data-tool-review-queue-mode>Bootstrap</span>
</div>
@ -644,7 +644,7 @@ def render_tool_review_page(
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Checklist de aprovacao</p>
<h3 class="h3 fw-semibold mb-2">Playbook para a decisao humana</h3>
<p class="text-secondary mb-0">Aprovacao e ativacao continuam controladas pelo papel administrativo e pela leitura do contrato compartilhado.</p>
<p class="text-secondary mb-0">Triagem, leitura do codigo, aprovacao e ativacao continuam controladas pelo papel administrativo e pelo contrato compartilhado.</p>
</div>
<div class="admin-tool-review-note p-4">
@ -678,7 +678,7 @@ def render_tool_review_page(
<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>
<p class="text-secondary mb-0">Selecione uma proposta da fila para triar a geracao, inspecionar a iteracao atual do codigo 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>
@ -723,14 +723,14 @@ def render_tool_review_page(
<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>
<label class="form-label fw-semibold" for="admin-tool-review-generated-code">Codigo completo da iteracao atual</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 class="form-text">Use este campo para revisar a implementacao completa da iteracao atual antes de solicitar ajustes, aprovar ou ativar a 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>
<textarea class="form-control rounded-4" id="admin-tool-review-decision-notes" rows="5" placeholder="Registre o racional da triagem, da revisao, do pedido de ajustes 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>
@ -742,9 +742,13 @@ def render_tool_review_page(
</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-primary rounded-pill" type="button" data-tool-review-action="authorize-generation" disabled>Autorizar geracao</button>
<button class="btn btn-outline-info rounded-pill" type="button" data-tool-review-action="run-pipeline" disabled>Executar pipeline</button>
<button class="btn btn-outline-dark rounded-pill" type="button" data-tool-review-action="review" disabled>Validar codigo</button>
<button class="btn btn-outline-warning rounded-pill" type="button" data-tool-review-action="request-changes" disabled>Solicitar ajustes e refatorar</button>
<button class="btn btn-dark rounded-pill" type="button" data-tool-review-action="approve" disabled>Aprovar para ativacao</button>
<button class="btn btn-success rounded-pill" type="button" data-tool-review-action="publish" disabled>Ativar no catalogo</button>
<button class="btn btn-outline-secondary rounded-pill" type="button" data-tool-review-action="close" disabled>Encerrar proposta</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>
@ -1009,7 +1013,7 @@ def render_tool_intake_page(
<div class="d-flex flex-wrap gap-3">
<button class="btn btn-dark btn-lg rounded-pill px-4 d-inline-flex align-items-center gap-2" type="submit">
<span data-intake-submit-label>Validar pre-cadastro</span>
<span data-intake-submit-label>Criar proposta</span>
<span class="spinner-border spinner-border-sm d-none" data-intake-submit-spinner aria-hidden="true"></span>
</button>
<a class="btn btn-outline-secondary btn-lg rounded-pill px-4" href="{escape(view.review_href, quote=True)}">Ir para revisao</a>
@ -1068,14 +1072,14 @@ def render_tool_intake_page(
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Preview do draft</p>
<h3 class="h4 fw-semibold mb-1">Resultado da validacao</h3>
<h3 class="h4 fw-semibold mb-1">Resultado da proposta</h3>
</div>
<span class="badge rounded-pill bg-body-tertiary text-secondary border" data-tool-intake-storage-status>Aguardando</span>
</div>
<div data-tool-intake-preview>
<div class="admin-tool-empty-state rounded-4 p-4">
<h4 class="h5 fw-semibold mb-2">Nenhum pre-cadastro validado ainda</h4>
<p class="text-secondary mb-0">Assim que o formulario for validado, o resumo do draft aparece aqui com avisos e proximos passos.</p>
<h4 class="h5 fw-semibold mb-2">Nenhuma proposta criada ainda</h4>
<p class="text-secondary mb-0">Assim que o formulario for enviado, o resumo do draft aparece aqui com os proximos passos de triagem antes da geracao.</p>
</div>
</div>
</div>

@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, Request
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse, RedirectResponse, Response
from admin_app.api.dependencies import get_optional_panel_staff_context
@ -673,7 +673,7 @@ def _build_tool_intake_view(
app_name=settings.admin_app_name,
title="Cadastro de nova tool",
subtitle=(
"Formulario guiado para o colaborador estruturar uma nova tool, validar o pre-draft e encaminhar a proposta para revisao de diretor."
"Formulario guiado para estruturar uma proposta de tool, registrar o draft versionado e encaminhar a triagem humana antes de qualquer geracao de codigo."
),
environment=settings.admin_environment,
version=settings.admin_version,
@ -711,9 +711,9 @@ def _build_tool_review_view(request: Request, settings: AdminSettings) -> AdminT
return AdminToolReviewPageView(
app_name=settings.admin_app_name,
title="Revisao, aprovacao e ativacao",
title="Triagem, revisao e ativacao",
subtitle=(
"Hub visual para o time interno acompanhar a fila de revisao, validar o contrato compartilhado e inspecionar o catalogo de tools ativas antes da ativacao."
"Hub visual para acompanhar a proposta da tool desde a triagem para geracao, passando pelas iteracoes de codigo, ate a ativacao controlada no catalogo do produto."
),
environment=settings.admin_environment,
version=settings.admin_version,
@ -725,54 +725,54 @@ def _build_tool_review_view(request: Request, settings: AdminSettings) -> AdminT
workflow=(
AdminToolReviewWorkflowStep(
eyebrow="Cadastro manual",
title="Persistir o draft",
description="Receber o cadastro manual da tool e consolidar a versao administrativa inicial.",
title="Criar a proposta",
description="Receber o cadastro manual da tool e consolidar o draft administrativo sem consumir geracao de codigo.",
status_label="Draft",
status_variant="info",
),
AdminToolReviewWorkflowStep(
eyebrow="Pipeline",
title="Executar geracao",
description="Rodar a etapa de geracao da implementacao isolada antes da validacao da versao.",
status_label="Geracao",
eyebrow="Triagem humana",
title="Triar antes de gerar",
description="Decidir se a proposta realmente merece consumir geracao de codigo ou deve ser encerrada antes disso.",
status_label="Triagem",
status_variant="warning",
),
AdminToolReviewWorkflowStep(
eyebrow="Validacao",
title="Conferir a versao gerada",
description="Validar a versao produzida pelo pipeline antes da aprovacao humana da diretoria.",
status_label="Validacao",
eyebrow="Pipeline",
title="Gerar ou refatorar",
description="Executar a geracao isolada, registrar a iteracao correspondente e validar automaticamente o codigo antes da leitura humana.",
status_label="Geracao",
status_variant="info",
),
AdminToolReviewWorkflowStep(
eyebrow="Decisao humana",
title="Aprovar com criterio",
description="A diretoria revisa a versao validada e decide se ela pode seguir para publicacao controlada.",
status_label="Aprovacao",
title="Ler e decidir",
description="A diretoria revisa a iteracao atual do codigo, pede ajustes quando necessario ou valida a versao para aprovacao final.",
status_label="Revisao",
status_variant="warning",
),
AdminToolReviewWorkflowStep(
eyebrow="Publicacao",
title="Ativar no catalogo",
description="Usar o catalogo publicado como referencia para a versao que chega ao runtime de produto.",
title="Ativar com governanca",
description="Publicar apenas a iteracao aprovada e sincronizar o catalogo que abastece o runtime de produto.",
status_label="Ativacao",
status_variant="success",
),
),
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.",
"Ler o codigo completo gerado antes de validar manualmente a versao.",
"Conferir se a proposta esta no gate correto: triagem para geracao, ajustes solicitados ou leitura do codigo atual.",
"Observar se a descricao, o objetivo operacional e os parametros deixam claro o valor de negocio da tool.",
"Ler o codigo completo da iteracao atual antes de validar, solicitar ajustes ou encerrar a proposta.",
),
approval_notes=(
"Verificar nome, descricao e semantica dos parametros antes da aprovacao.",
"Confirmar se a tool respeita a separacao entre admin e product definida nas ADRs.",
"Checar se a publicacao planejada e auditavel e segura para o runtime de produto.",
"Verificar nome, descricao, semantica dos parametros e a iteracao que esta sendo aprovada antes da ativacao.",
"Confirmar se a tool respeita a separacao entre admin e product definida nas ADRs e nos contratos compartilhados.",
"Checar se a publicacao planejada e auditavel, segura e vinculada ao codigo mais recente revisado pela diretoria.",
),
activation_notes=(
"Publicacoes ativas exigem papel com permissao publish_tools.",
"A leitura do catalogo e feita via sessao web do painel para facilitar a operacao do navegador.",
"Sem permissao de publicacao, a tela continua util para revisao, mas bloqueia o catalogo ativo.",
"Publicacoes ativas exigem papel com permissao publish_tools e aprovacao humana vinculada a iteracao mais recente.",
"A leitura do catalogo e feita via sessao web do painel para facilitar a operacao do navegador e a auditoria do fluxo.",
"Sem permissao de publicacao, a tela continua util para triagem e revisao, mas bloqueia a ativacao no catalogo ativo.",
),
)

@ -1,4 +1,4 @@
document.documentElement.dataset.panelReady = "true";
document.documentElement.dataset.panelReady = "true";
const loginForm = document.querySelector('[data-admin-login-form="true"]');
const reviewBoard = document.querySelector('[data-admin-tool-review-board="true"]');
@ -126,9 +126,13 @@ function mountToolReviewBoard(board) {
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 authorizeGenerationButton = board.querySelector('[data-tool-review-action="authorize-generation"]');
const runPipelineButton = board.querySelector('[data-tool-review-action="run-pipeline"]');
const reviewButton = board.querySelector('[data-tool-review-action="review"]');
const requestChangesButton = board.querySelector('[data-tool-review-action="request-changes"]');
const approveButton = board.querySelector('[data-tool-review-action="approve"]');
const publishButton = board.querySelector('[data-tool-review-action="publish"]');
const closeProposalButton = board.querySelector('[data-tool-review-action="close"]');
const deactivateButton = board.querySelector('[data-tool-review-action="deactivate"]');
const rollbackButton = board.querySelector('[data-tool-review-action="rollback"]');
@ -178,7 +182,7 @@ function mountToolReviewBoard(board) {
});
}
[reviewButton, approveButton, publishButton, deactivateButton, rollbackButton].forEach((button) => {
[authorizeGenerationButton, runPipelineButton, reviewButton, requestChangesButton, approveButton, publishButton, closeProposalButton, deactivateButton, rollbackButton].forEach((button) => {
if (!(button instanceof HTMLButtonElement)) {
return;
}
@ -293,10 +297,13 @@ function mountToolReviewBoard(board) {
decision_notes: String(decisionNotes?.value || "").trim(),
reviewed_generated_code: Boolean(reviewedGeneratedCode?.checked),
};
} else if (actionKey === "approve" || actionKey === "deactivate" || actionKey === "rollback") {
} else if (["authorize-generation", "request-changes", "approve", "deactivate", "rollback"].includes(actionKey)) {
payload = {
decision_notes: String(decisionNotes?.value || "").trim(),
};
} else if (actionKey === "close") {
const normalizedNotes = String(decisionNotes?.value || "").trim();
payload = normalizedNotes ? { decision_notes: normalizedNotes } : {};
}
toggleActionLoading(actionKey, true);
@ -337,11 +344,33 @@ function mountToolReviewBoard(board) {
if (!encodedVersionId) {
return "";
}
const reviewQueueBase = String(board.dataset.reviewQueueEndpoint || "").replace(/\/+$/, "");
const publicationsBase = String(board.dataset.publicationsEndpoint || "").replace(/\/+$/, "");
const draftsBase = reviewQueueBase.endsWith("/review-queue")
? `${reviewQueueBase.slice(0, -"/review-queue".length)}/drafts`
: "";
const pipelineBase = reviewQueueBase.endsWith("/review-queue")
? `${reviewQueueBase.slice(0, -"/review-queue".length)}/pipeline`
: "";
const currentGate = String(lastRenderedHumanGate?.current_gate || "").trim();
const usesDraftGovernanceRoute = ["generation_decision_required", "generation_pipeline_required", "changes_requested"].includes(currentGate);
if (actionKey === "publish" || actionKey === "deactivate" || actionKey === "rollback") {
return `${board.dataset.publicationsEndpoint}/${encodedVersionId}/${actionKey}`;
return `${publicationsBase}/${encodedVersionId}/${actionKey}`;
}
if (actionKey === "review" || actionKey === "approve" || actionKey === "request-changes") {
return `${reviewQueueBase}/${encodedVersionId}/${actionKey}`;
}
if (actionKey === "review" || actionKey === "approve") {
return `${board.dataset.reviewQueueEndpoint}/${encodedVersionId}/${actionKey}`;
if (actionKey === "authorize-generation") {
return draftsBase ? `${draftsBase}/${encodedVersionId}/authorize-generation` : "";
}
if (actionKey === "run-pipeline") {
return pipelineBase ? `${pipelineBase}/${encodedVersionId}/run` : "";
}
if (actionKey === "close") {
if (usesDraftGovernanceRoute && draftsBase) {
return `${draftsBase}/${encodedVersionId}/close`;
}
return `${reviewQueueBase}/${encodedVersionId}/close`;
}
return "";
}
@ -357,9 +386,13 @@ function mountToolReviewBoard(board) {
function toggleActionLoading(actionKey, isLoading) {
const buttonsByAction = {
"authorize-generation": authorizeGenerationButton,
"run-pipeline": runPipelineButton,
review: reviewButton,
"request-changes": requestChangesButton,
approve: approveButton,
publish: publishButton,
close: closeProposalButton,
deactivate: deactivateButton,
rollback: rollbackButton,
};
@ -410,6 +443,76 @@ function mountToolReviewBoard(board) {
parameterTypes.innerHTML = `<span class="badge rounded-pill bg-body-tertiary text-secondary border">Bloqueado</span>`;
}
function describeReviewGate(gate) {
const normalizedGate = String(gate || "").trim();
const gateMap = {
generation_decision_required: {
label: "Aguardando triagem para gerar",
description: "A proposta foi criada, mas ainda nao deve consumir geracao de codigo."
},
generation_pipeline_required: {
label: "Pronta para gerar",
description: "A triagem humana autorizou a pipeline. Falta executar a geracao isolada."
},
generation_pipeline_queued: {
label: "Pipeline enfileirada",
description: "O worker do admin ja recebeu a geracao e vai iniciar a iteracao assim que houver slot."
},
generation_pipeline_running: {
label: "Pipeline em execucao",
description: "O worker do admin esta gerando ou validando a iteracao atual desta proposta."
},
generation_worker_failed: {
label: "Worker falhou",
description: "A execucao ass?ncrona falhou antes de concluir a pipeline."
},
changes_requested: {
label: "Ajustes solicitados",
description: "A mesma versao aguarda uma nova iteracao guiada pelo ultimo parecer humano."
},
validation_required: {
label: "Codigo aguardando leitura",
description: "A geracao terminou e o codigo atual precisa de leitura humana da diretoria."
},
director_approval_required: {
label: "Aguardando aprovacao final",
description: "A leitura do codigo ja aconteceu e falta a aprovacao formal antes da ativacao."
},
director_publication_required: {
label: "Pronta para ativacao",
description: "A iteracao atual ja foi revisada e aprovada. Falta ativar no catalogo governado."
},
publication_active: {
label: "Publicacao ativa",
description: "A versao ja abastece o catalogo governado do produto."
},
pipeline_retry_required: {
label: "Retry tecnico necessario",
description: "A pipeline falhou tecnicamente e precisa de nova execucao antes de voltar para a revisao humana."
},
archived_history: {
label: "Proposta arquivada",
description: "A proposta saiu da esteira ativa e permanece apenas para historico e auditoria."
}
};
return gateMap[normalizedGate] || {
label: normalizedGate || "Governanca pendente",
description: "A proposta continua no fluxo governado aguardando a proxima decisao humana ou tecnica."
};
}
function describeGenerationMode(mode) {
const normalizedMode = String(mode || "").trim();
const modeMap = {
initial_generation: "Geracao inicial",
change_request_refinement: "Refatoracao guiada por feedback",
failed_pipeline_retry: "Retry tecnico da pipeline",
regeneration_with_context: "Regeneracao com contexto anterior",
legacy_generation: "Geracao legada"
};
return modeMap[normalizedMode] || normalizedMode || "Nenhuma geracao concluida ainda";
}
function renderReviewQueue(payload, preferredVersionId = "") {
const items = Array.isArray(payload?.items) ? payload.items : [];
setText("[data-tool-review-queue-count]", String(items.length));
@ -417,13 +520,17 @@ function mountToolReviewBoard(board) {
queueList.innerHTML = items.length > 0
? items.map((item) => {
const isSelected = String(item?.version_id || "") === String(preferredVersionId || selectedVersionId || "");
const gatePresentation = describeReviewGate(item?.gate);
const validationSummary = item?.automated_validation_summary
? `<div class="small text-secondary mt-2"><strong>Pipeline:</strong> ${escapeHtml(item.automated_validation_summary)}</div>`
: "";
const ownerMarkup = item?.owner_name
? `<div class="small text-secondary mt-2"><strong>Owner:</strong> ${escapeHtml(item.owner_name)}</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>`;
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(gatePresentation.label)}</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-2">${escapeHtml(item.summary || payload?.message || "Item aguardando analise do time.")}</p><div class="small text-secondary">${escapeHtml(gatePresentation.description)}</div>${validationSummary}${ownerMarkup}${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>`;
}
@ -542,6 +649,7 @@ function mountToolReviewBoard(board) {
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 generationContext = payload?.generation_context || null;
const hasSourceCode = Boolean(String(payload?.generated_source_code || "").trim());
detailStatus.textContent = payload?.status || "versao";
@ -551,6 +659,11 @@ function mountToolReviewBoard(board) {
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>`;
const latestGenerationMode = describeGenerationMode(generationContext?.latest_generation_mode);
const nextGenerationMode = describeGenerationMode(generationContext?.next_generation_mode || "initial_generation");
const changeRequestMarkup = generationContext?.latest_change_request_notes
? `<div><strong>Ultimo parecer para ajuste:</strong> ${escapeHtml(generationContext.latest_change_request_notes)}</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>
@ -560,6 +673,12 @@ function mountToolReviewBoard(board) {
<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><strong>Ultima iteracao de geracao:</strong> ${escapeHtml(String(generationContext?.latest_generation_iteration || 0))}</div>
<div><strong>Modo da ultima geracao:</strong> ${escapeHtml(latestGenerationMode)}</div>
<div><strong>Proxima iteracao prevista:</strong> ${escapeHtml(String(generationContext?.next_generation_iteration || 1))}</div>
<div><strong>Proxima execucao da pipeline:</strong> ${escapeHtml(nextGenerationMode)}</div>
<div><strong>Total de iteracoes registradas:</strong> ${escapeHtml(String(generationContext?.generation_iterations_count || 0))}</div>
${changeRequestMarkup}
</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>
@ -608,7 +727,7 @@ function mountToolReviewBoard(board) {
reviewedGeneratedCode.checked = false;
}
if (decisionHint instanceof HTMLElement) {
decisionHint.textContent = buildDecisionHint(humanGate, hasSourceCode);
decisionHint.textContent = buildDecisionHint(humanGate, hasSourceCode, generationContext);
}
configureActionPanel(humanGate, hasSourceCode);
}
@ -616,9 +735,13 @@ function mountToolReviewBoard(board) {
function configureActionPanel(humanGate, hasSourceCode) {
lastRenderedHumanGate = humanGate;
lastRenderedHasSourceCode = hasSourceCode;
configureActionButton(authorizeGenerationButton, Boolean(humanGate?.authorize_generation_action_available));
configureActionButton(runPipelineButton, Boolean(humanGate?.run_pipeline_action_available));
configureActionButton(reviewButton, Boolean(humanGate?.review_action_available) && hasSourceCode);
configureActionButton(requestChangesButton, Boolean(humanGate?.request_changes_action_available) && hasSourceCode);
configureActionButton(approveButton, Boolean(humanGate?.approval_action_available));
configureActionButton(publishButton, Boolean(humanGate?.publication_action_available));
configureActionButton(closeProposalButton, Boolean(humanGate?.close_proposal_action_available));
configureActionButton(deactivateButton, Boolean(humanGate?.deactivation_action_available));
configureActionButton(rollbackButton, Boolean(humanGate?.rollback_action_available));
@ -646,16 +769,31 @@ function mountToolReviewBoard(board) {
button.disabled = !isEnabled;
}
function buildDecisionHint(humanGate, hasSourceCode) {
function buildDecisionHint(humanGate, hasSourceCode, generationContext) {
if (!humanGate) {
return "As notas da decisao ficam persistidas na trilha administrativa da versao.";
}
if (humanGate.authorize_generation_action_available) {
return "A proposta ainda esta em draft. Registre um parecer e autorize a geracao somente quando fizer sentido consumir codigo.";
}
if (humanGate.run_pipeline_action_available) {
return `A proposta ja pode seguir para a pipeline. A proxima execucao prevista e ${escapeHtml(describeGenerationMode(generationContext?.next_generation_mode || "initial_generation")).toLowerCase()} na iteracao ${escapeHtml(String(generationContext?.next_generation_iteration || 1))}.`;
}
if (humanGate.current_gate === "generation_pipeline_required") {
return `A triagem humana ja autorizou a pipeline. A proxima execucao prevista e ${escapeHtml(describeGenerationMode(generationContext?.next_generation_mode || "initial_generation")).toLowerCase()} na iteracao ${escapeHtml(String(generationContext?.next_generation_iteration || 1))}.`;
}
if (humanGate.current_gate === "changes_requested") {
return `A diretoria solicitou ajustes. A proxima execucao prevista e ${escapeHtml(describeGenerationMode(generationContext?.next_generation_mode || "change_request_refinement")).toLowerCase()} na iteracao ${escapeHtml(String(generationContext?.next_generation_iteration || 1))}.`;
}
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.request_changes_action_available) {
return "Se o codigo ainda nao estiver bom, registre o parecer para solicitar ajustes ou encerre a proposta sem seguir para ativacao.";
}
if (humanGate.approval_action_available) {
return "A aprovacao formal ainda exige um parecer explicito da diretoria antes da publicacao.";
}
@ -790,7 +928,7 @@ function mountToolIntakePage(page) {
function toggleSubmitting(isLoading) {
submitButton.disabled = isLoading;
submitSpinner.classList.toggle("d-none", !isLoading);
submitLabel.textContent = isLoading ? "Validando..." : "Validar pre-cadastro";
submitLabel.textContent = isLoading ? "Criando..." : "Criar proposta";
}
function clearFeedback() {
@ -831,7 +969,7 @@ function mountToolIntakePage(page) {
<div><strong>Aprovacao:</strong> ${draft?.requires_director_approval ? "Diretor obrigatorio" : "Nao"}</div>
</div>
${submissionPolicy
? `<div class="admin-tool-inline-note rounded-4 p-3 mb-3"><div class="fw-semibold mb-1">Governanca desta submissao</div><div class="small text-secondary">Modo: ${escapeHtml(submissionPolicy.mode || "draft_only")}. Papel atual: ${escapeHtml(submissionPolicy.submitter_role || "nao informado")}. Publicacao direta: ${submissionPolicy.direct_publication_blocked ? "bloqueada neste fluxo" : "permitida"}. Permissao final de publicacao: ${escapeHtml(submissionPolicy.required_publish_permission || "publish_tools")}.<\/div><\/div>`
? `<div class="admin-tool-inline-note rounded-4 p-3 mb-3"><div class="fw-semibold mb-1">Governanca desta proposta</div><div class="small text-secondary">Modo: ${escapeHtml(submissionPolicy.mode || "draft_only")}. Papel atual: ${escapeHtml(submissionPolicy.submitter_role || "nao informado")}. Esta tela apenas cria o draft administrativo; a geracao de codigo continua bloqueada ate a triagem humana. Permissao final de publicacao: ${escapeHtml(submissionPolicy.required_publish_permission || "publish_tools")}.<\/div><\/div>`
: ""}
<div class="vstack gap-2 mb-3">
${parameters.length > 0
@ -843,7 +981,7 @@ function mountToolIntakePage(page) {
<div class="small text-uppercase fw-semibold text-secondary mb-2">Avisos</div>
${warnings.length > 0
? `<ul class="small text-secondary ps-3 mb-0">${warnings.map((item) => `<li class="mb-2">${escapeHtml(item)}</li>`).join("")}</ul>`
: `<div class="small text-secondary">Nenhum aviso extra para este pre-cadastro.</div>`}
: `<div class="small text-secondary">Nenhum aviso extra para esta proposta.</div>`}
</div>
<div>
<div class="small text-uppercase fw-semibold text-secondary mb-2">Proximos passos</div>

@ -1,4 +1,4 @@
import unittest
import unittest
from datetime import datetime, timezone
from fastapi.testclient import TestClient
@ -397,8 +397,11 @@ class AdminPanelToolsWebTests(unittest.TestCase):
self.assertEqual(payload["submission_policy"]["mode"], "draft_only")
self.assertEqual(payload["submission_policy"]["submitter_role"], "colaborador")
self.assertFalse(payload["submission_policy"]["submitter_can_publish_now"])
self.assertFalse(payload["submission_policy"]["submitter_can_authorize_generation_now"])
self.assertTrue(payload["submission_policy"]["direct_publication_blocked"])
self.assertTrue(payload["submission_policy"]["requires_generation_authorization"])
self.assertEqual(payload["submission_policy"]["required_approver_role"], "diretor")
self.assertEqual(payload["submission_policy"]["required_generation_permission"], "review_tool_generations")
self.assertEqual(payload["submission_policy"]["required_publish_permission"], "publish_tools")
self.assertEqual(payload["draft_preview"]["status"], "draft")
self.assertEqual(payload["draft_preview"]["tool_name"], "consultar_vendas_periodo")
@ -557,10 +560,13 @@ class AdminPanelToolsWebTests(unittest.TestCase):
payload = response.json()
self.assertEqual(payload["tool_name"], "emitir_resumo_locacao")
self.assertTrue(payload["human_gate"]["review_action_available"])
self.assertEqual(payload["generation_context"]["latest_generation_iteration"], 1)
self.assertEqual(payload["generation_context"]["generation_iterations_count"], 1)
self.assertEqual(payload["generation_context"]["latest_generation_mode"], "initial_generation")
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):
def test_panel_tools_director_can_authorize_generation_for_collaborator_draft(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
try:
intake_response = client.post(
@ -575,26 +581,117 @@ class AdminPanelToolsWebTests(unittest.TestCase):
},
)
version_id = intake_response.json()["draft_preview"]["version_id"]
response = client.post(f"/admin/panel/tools/pipeline/{version_id}/run")
app.dependency_overrides[get_current_panel_staff_principal] = lambda: AuthenticatedStaffPrincipal(
id=11,
email="diretor@empresa.com",
display_name="Diretoria",
role=StaffRole.DIRETOR,
is_active=True,
)
response = client.post(
f"/admin/panel/tools/drafts/{version_id}/authorize-generation",
json={"decision_notes": "A proposta pode seguir para geracao governada apos triagem inicial."},
)
detail_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(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["status"], "generated")
self.assertEqual(payload["current_step"], "validation")
self.assertEqual(payload["queue_entry"]["gate"], "validation_required")
self.assertEqual(payload["queue_entry"]["automated_validation_status"], "passed")
self.assertEqual(payload["queue_entry"]["automated_validation_summary"], "4/4 validacoes automaticas passaram antes da revisao humana.")
automated_checks = {check["key"]: check for check in payload["automated_validations"]}
self.assertEqual(automated_checks["tool_contract"]["status"], "passed")
self.assertEqual(automated_checks["tool_signature_schema"]["status"], "passed")
self.assertEqual(automated_checks["tool_import_loading"]["status"], "passed")
self.assertEqual(automated_checks["tool_smoke_tests"]["status"], "passed")
steps_by_key = {step["key"]: step for step in payload["steps"]}
self.assertEqual(steps_by_key["generation"]["state"], "completed")
self.assertEqual(steps_by_key["validation"]["state"], "current")
self.assertEqual(response.json()["status"], "draft")
self.assertEqual(response.json()["queue_entry"]["gate"], "generation_pipeline_required")
self.assertEqual(detail_response.status_code, 200)
self.assertEqual(detail_response.json()["human_gate"]["current_gate"], "generation_pipeline_required")
self.assertTrue(detail_response.json()["human_gate"]["run_pipeline_action_available"])
def test_panel_tools_director_can_request_changes_after_generation(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")
response = client.post(
f"/admin/panel/tools/review-queue/{version_id}/request-changes",
json={"decision_notes": "O codigo precisa de ajustes antes de voltar para revisao humana."},
)
detail_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(response.status_code, 200)
self.assertEqual(response.json()["status"], "draft")
self.assertEqual(response.json()["queue_entry"]["gate"], "changes_requested")
self.assertEqual(detail_response.status_code, 200)
self.assertEqual(detail_response.json()["human_gate"]["current_gate"], "changes_requested")
self.assertTrue(detail_response.json()["human_gate"]["run_pipeline_action_available"])
self.assertTrue(detail_response.json()["generation_context"]["pending_change_request"])
self.assertEqual(detail_response.json()["generation_context"]["next_generation_mode"], "change_request_refinement")
self.assertIn("ajustes", (detail_response.json()["generation_context"]["latest_change_request_notes"] or "").lower())
def test_panel_tools_director_can_close_generated_proposal_without_notes(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")
response = client.post(
f"/admin/panel/tools/review-queue/{version_id}/close",
json={},
)
detail_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(response.status_code, 200)
self.assertEqual(response.json()["status"], "archived")
self.assertEqual(detail_response.status_code, 200)
self.assertEqual(detail_response.json()["status"], "archived")
self.assertFalse(detail_response.json()["human_gate"]["close_proposal_action_available"])
def test_panel_tools_collaborator_cannot_run_generation_pipeline_before_director_authorization(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
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"]
response = client.post(f"/admin/panel/tools/pipeline/{version_id}/run")
finally:
app.dependency_overrides.clear()
self.assertEqual(intake_response.status_code, 200)
self.assertEqual(response.status_code, 409)
self.assertIn("autorizacao de diretor", response.json()["detail"].lower())
def test_panel_tools_publications_require_director_publication_permission(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)

@ -532,7 +532,7 @@ class AdminToolManagementServiceTests(unittest.TestCase):
self.assertEqual(len(self.draft_repository.drafts), 1)
self.assertEqual(len(self.version_repository.versions), 2)
self.assertEqual(len(self.metadata_repository.metadata_entries), 2)
self.assertEqual(len(self.artifact_repository.artifacts), 4)
self.assertEqual(len(self.artifact_repository.artifacts), 6)
self.assertEqual(self.draft_repository.drafts[0].current_version_number, 2)
self.assertEqual(self.draft_repository.drafts[0].version_count, 2)
self.assertEqual(self.draft_repository.drafts[0].owner_display_name, "Coordenacao de Locacao")
@ -672,6 +672,638 @@ class AdminToolManagementServiceTests(unittest.TestCase):
self.assertEqual(payload["items"][0]["gate"], "generation_pipeline_required")
self.assertIn(ToolLifecycleStatus.APPROVED, payload["supported_statuses"])
def test_collaborator_draft_requires_director_authorization_before_generation(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": [],
},
owner_staff_account_id=8,
owner_name="Operacao de Oficina",
owner_role=StaffRole.COLABORADOR,
)
payload = self.service.build_review_queue_payload()
self.assertEqual(payload["items"][0]["version_id"], intake_payload["draft_preview"]["version_id"])
self.assertEqual(payload["items"][0]["gate"], "generation_decision_required")
detail = self.service.build_review_detail_payload(intake_payload["draft_preview"]["version_id"])
self.assertEqual(detail["human_gate"]["current_gate"], "generation_decision_required")
self.assertTrue(detail["human_gate"]["authorize_generation_action_available"])
self.assertFalse(detail["human_gate"]["run_pipeline_action_available"])
transition = self.service.authorize_generation(
intake_payload["draft_preview"]["version_id"],
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes="A proposta esta alinhada e pode consumir a etapa de geracao governada.",
)
self.assertEqual(transition["status"], ToolLifecycleStatus.DRAFT)
self.assertEqual(transition["queue_entry"]["gate"], "generation_pipeline_required")
detail_after_authorization = self.service.build_review_detail_payload(intake_payload["draft_preview"]["version_id"])
self.assertTrue(detail_after_authorization["human_gate"]["run_pipeline_action_available"])
def test_request_changes_returns_generated_version_to_draft_with_changes_gate(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": [],
},
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,
)
payload = self.service.request_changes(
version_id,
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes="A proposta precisa ajustar o codigo e a estrutura antes de nova validacao.",
)
self.assertEqual(payload["status"], ToolLifecycleStatus.DRAFT)
self.assertEqual(payload["queue_entry"]["gate"], "changes_requested")
detail = self.service.build_review_detail_payload(version_id)
self.assertEqual(detail["human_gate"]["current_gate"], "changes_requested")
self.assertTrue(detail["human_gate"]["run_pipeline_action_available"])
self.assertEqual(detail["decision_history"][-1]["action_key"], ToolArtifactKind.GENERATION_CHANGE_REQUEST.value)
def test_close_proposal_archives_generated_version_without_notes(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": [],
},
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,
)
payload = self.service.close_proposal(
version_id,
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes=None,
)
self.assertEqual(payload["status"], ToolLifecycleStatus.ARCHIVED)
self.assertEqual(payload["queue_entry"]["gate"], "archived_history")
detail = self.service.build_review_detail_payload(version_id)
self.assertFalse(detail["human_gate"]["close_proposal_action_available"])
self.assertEqual(detail["decision_history"][-1]["action_key"], ToolArtifactKind.PROPOSAL_CLOSURE.value)
self.assertIsNone(detail["decision_history"][-1]["decision_notes"])
def test_rerun_generation_after_change_request_tracks_new_iteration_without_new_version(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",
owner_role=StaffRole.COLABORADOR,
)
version_id = intake_payload["draft_preview"]["version_id"]
version_number = intake_payload["draft_preview"]["version_number"]
self.service.authorize_generation(
version_id,
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes="A proposta pode consumir a primeira geracao governada.",
)
self.service.run_generation_pipeline(
version_id,
runner_staff_account_id=8,
runner_name="Operacao de Oficina",
runner_role=StaffRole.COLABORADOR,
)
feedback_notes = "Ajuste o retorno para destacar o resumo operacional e preserve a assinatura atual."
self.service.request_changes(
version_id,
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes=feedback_notes,
)
second_generation = self.service.run_generation_pipeline(
version_id,
runner_staff_account_id=8,
runner_name="Operacao de Oficina",
runner_role=StaffRole.COLABORADOR,
)
self.assertEqual(second_generation["version_id"], version_id)
self.assertEqual(second_generation["version_number"], version_number)
self.assertEqual(second_generation["status"], ToolLifecycleStatus.GENERATED)
generation_artifact = next(
artifact
for artifact in self.artifact_repository.artifacts
if artifact.artifact_kind == ToolArtifactKind.GENERATION_REQUEST
)
self.assertEqual(generation_artifact.payload_json["generation_iteration"], 2)
self.assertEqual(generation_artifact.payload_json["generation_mode"], "change_request_refinement")
self.assertEqual(generation_artifact.payload_json["feedback_notes"], feedback_notes)
self.assertEqual(len(generation_artifact.payload_json["generation_iterations"]), 2)
self.assertEqual(
generation_artifact.payload_json["generation_iterations"][-1]["generation_iteration"],
2,
)
validation_artifact = next(
artifact
for artifact in self.artifact_repository.artifacts
if artifact.artifact_kind == ToolArtifactKind.VALIDATION_REPORT
)
self.assertEqual(validation_artifact.payload_json["generation_iteration"], 2)
self.assertEqual(validation_artifact.payload_json["generation_mode"], "change_request_refinement")
self.assertEqual(validation_artifact.payload_json["change_request_notes"], feedback_notes)
self.assertEqual(len(validation_artifact.payload_json["validation_iterations"]), 2)
self.assertEqual(
validation_artifact.payload_json["validation_iterations"][-1]["generation_iteration"],
2,
)
def test_review_detail_and_runtime_snapshot_recover_legacy_generated_source_from_metadata(self):
intake_payload = 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": [
{
"name": "contrato_id",
"parameter_type": "string",
"description": "Identificador do contrato consultado.",
"required": True,
}
],
},
owner_staff_account_id=7,
owner_name="Equipe Interna",
owner_role=StaffRole.COLABORADOR,
)
version_id = intake_payload["draft_preview"]["version_id"]
self.service.authorize_generation(
version_id,
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes="A proposta foi autorizada para gerar codigo governado.",
)
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="Revisao humana concluida para publicar a iteracao legada.",
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 final registrada para publicacao governada.",
)
version = self.version_repository.get_by_version_id(version_id)
validation_artifact = self.artifact_repository.get_by_tool_version_and_kind(
version.id,
ToolArtifactKind.VALIDATION_REPORT,
)
generation_artifact = self.artifact_repository.get_by_tool_version_and_kind(
version.id,
ToolArtifactKind.GENERATION_REQUEST,
)
validation_payload = dict(validation_artifact.payload_json or {})
validation_payload.pop("generated_source_code", None)
validation_payload.pop("generated_source_checksum", None)
validation_artifact.payload_json = validation_payload
generation_payload = dict(generation_artifact.payload_json or {})
generation_payload.pop("generation_iterations", None)
generation_payload.pop("latest_generation", None)
generation_payload.pop("generation_mode", None)
generation_artifact.payload_json = generation_payload
detail = self.service.build_review_detail_payload(version_id)
self.assertIn("async def run", detail["generated_source_code"])
self.assertEqual(detail["generation_context"]["latest_generation_mode"], "legacy_generation")
self.assertTrue(self.service._version_has_generated_source(version_id))
import shutil
from pathlib import Path
sandbox_root = Path.cwd() / ".tmp_test_admin_legacy_runtime_snapshot"
shutil.rmtree(sandbox_root, ignore_errors=True)
package_dir = sandbox_root / GENERATED_TOOLS_PACKAGE
manifest_path = package_dir / "published_runtime_tools.json"
def build_file_path(tool_name: str):
return package_dir / f"{tool_name}.py"
try:
with patch(
"admin_app.services.tool_management_service.get_generated_tools_runtime_dir",
return_value=package_dir,
), patch(
"admin_app.services.tool_management_service.get_generated_tool_publication_manifest_path",
return_value=manifest_path,
), patch(
"admin_app.services.tool_management_service.build_generated_tool_file_path",
side_effect=build_file_path,
):
payload = self.service.publish_version(
version_id,
publisher_staff_account_id=99,
publisher_name="Diretoria",
publisher_role=StaffRole.DIRETOR,
)
self.assertEqual(payload["status"], ToolLifecycleStatus.ACTIVE)
self.assertTrue(build_file_path("emitir_resumo_locacao").exists())
self.assertIn(
"async def run",
build_file_path("emitir_resumo_locacao").read_text(encoding="utf-8"),
)
finally:
shutil.rmtree(sandbox_root, ignore_errors=True)
def test_failed_generation_without_validation_does_not_reconstruct_legacy_source(self):
class _FailingGenerationService:
async def generate_tool_source(self, **kwargs):
return {
"generated_source_code": None,
"generation_model_used": "gemini-3-pro-preview",
"issues": ["Vertex AI indisponivel para gerar o codigo nesta tentativa."],
"prompt_rendered": "prompt de geracao com falha",
"elapsed_ms": 17.0,
}
self.service.tool_generation_service = _FailingGenerationService()
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": [],
},
owner_staff_account_id=8,
owner_name="Operacao de Oficina",
owner_role=StaffRole.COLABORADOR,
)
version_id = intake_payload["draft_preview"]["version_id"]
self.service.authorize_generation(
version_id,
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes="A proposta pode seguir para geracao governada apos triagem inicial.",
)
payload = self.service.run_generation_pipeline(
version_id,
runner_staff_account_id=8,
runner_name="Operacao de Oficina",
runner_role=StaffRole.COLABORADOR,
)
self.assertEqual(payload["status"], ToolLifecycleStatus.FAILED)
detail = self.service.build_review_detail_payload(version_id)
self.assertEqual(detail["generated_source_code"], "")
self.assertFalse(detail["generation_context"]["has_previous_generation"])
self.assertFalse(self.service._version_has_generated_source(version_id))
version = self.version_repository.get_by_version_id(version_id)
with self.assertRaisesRegex(RuntimeError, "codigo gerado"):
self.service._get_generated_source_code_for_version(version.id)
def test_publish_allows_legacy_single_iteration_review_and_approval_metadata(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": [],
},
owner_staff_account_id=99,
owner_name="Diretoria",
owner_role=StaffRole.DIRETOR,
)
version_id = intake_payload["draft_preview"]["version_id"]
version = self.version_repository.get_by_version_id(version_id)
self.service.run_generation_pipeline(
version_id,
runner_staff_account_id=99,
runner_name="Diretoria",
runner_role=StaffRole.DIRETOR,
)
self.service.review_version(
version_id,
reviewer_staff_account_id=99,
reviewer_name="Diretoria",
reviewer_role=StaffRole.DIRETOR,
decision_notes="Revisao humana registrada antes da migracao do contexto de iteracao.",
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 humana registrada antes da migracao do contexto de iteracao.",
)
generation_artifact = self.artifact_repository.get_by_tool_version_and_kind(version.id, ToolArtifactKind.GENERATION_REQUEST)
validation_artifact = self.artifact_repository.get_by_tool_version_and_kind(version.id, ToolArtifactKind.VALIDATION_REPORT)
review_artifact = self.artifact_repository.get_by_tool_version_and_kind(version.id, ToolArtifactKind.DIRECTOR_REVIEW)
approval_artifact = self.artifact_repository.get_by_tool_version_and_kind(version.id, ToolArtifactKind.DIRECTOR_APPROVAL)
generation_payload = dict(generation_artifact.payload_json or {})
generation_payload.pop("generation_iterations", None)
generation_payload.pop("latest_generation", None)
generation_payload.pop("generation_mode", None)
generation_artifact.payload_json = generation_payload
validation_payload = dict(validation_artifact.payload_json or {})
validation_payload.pop("validation_iterations", None)
validation_payload.pop("latest_validation", None)
validation_payload.pop("generation_mode", None)
validation_artifact.payload_json = validation_payload
review_payload = dict(review_artifact.payload_json or {})
review_payload.pop("reviewed_generation_iteration", None)
review_payload.pop("reviewed_generation_mode", None)
review_payload.pop("reviewed_generation_checksum", None)
review_artifact.payload_json = review_payload
approval_payload = dict(approval_artifact.payload_json or {})
approval_payload.pop("approved_generation_iteration", None)
approval_payload.pop("approved_generation_mode", None)
approval_payload.pop("approved_generation_checksum", None)
approval_artifact.payload_json = approval_payload
payload = self.service.publish_version(
version_id,
publisher_staff_account_id=99,
publisher_name="Diretoria",
publisher_role=StaffRole.DIRETOR,
)
self.assertEqual(payload["status"], ToolLifecycleStatus.ACTIVE)
self.assertEqual(payload["version_id"], version_id)
def test_rollback_allows_legacy_single_iteration_review_and_approval_metadata(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 migracao do contexto de iteracao.",
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 antes da migracao do contexto de iteracao.",
)
self.service.publish_version(first_version_id, publisher_staff_account_id=99, publisher_name="Diretoria", publisher_role=StaffRole.DIRETOR)
first_version = self.version_repository.get_by_version_id(first_version_id)
generation_artifact = self.artifact_repository.get_by_tool_version_and_kind(first_version.id, ToolArtifactKind.GENERATION_REQUEST)
validation_artifact = self.artifact_repository.get_by_tool_version_and_kind(first_version.id, ToolArtifactKind.VALIDATION_REPORT)
review_artifact = self.artifact_repository.get_by_tool_version_and_kind(first_version.id, ToolArtifactKind.DIRECTOR_REVIEW)
approval_artifact = self.artifact_repository.get_by_tool_version_and_kind(first_version.id, ToolArtifactKind.DIRECTOR_APPROVAL)
generation_payload = dict(generation_artifact.payload_json or {})
generation_payload.pop("generation_iterations", None)
generation_payload.pop("latest_generation", None)
generation_payload.pop("generation_mode", None)
generation_artifact.payload_json = generation_payload
validation_payload = dict(validation_artifact.payload_json or {})
validation_payload.pop("validation_iterations", None)
validation_payload.pop("latest_validation", None)
validation_payload.pop("generation_mode", None)
validation_artifact.payload_json = validation_payload
review_payload = dict(review_artifact.payload_json or {})
review_payload.pop("reviewed_generation_iteration", None)
review_payload.pop("reviewed_generation_mode", None)
review_payload.pop("reviewed_generation_checksum", None)
review_artifact.payload_json = review_payload
approval_payload = dict(approval_artifact.payload_json or {})
approval_payload.pop("approved_generation_iteration", None)
approval_payload.pop("approved_generation_mode", None)
approval_payload.pop("approved_generation_checksum", None)
approval_artifact.payload_json = approval_payload
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 novos filtros administrativos.",
"business_goal": "Dar visibilidade ao time interno sobre gargalos do funil com cortes adicionais.",
"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="Segunda versao revisada para substituir a publicacao anterior.",
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="Segunda 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)
payload = self.service.rollback_version(
second_version_id,
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes="Rollback compativel com artefatos legados de revisao e aprovacao.",
)
self.assertEqual(payload["status"], ToolLifecycleStatus.ACTIVE)
self.assertEqual(payload["version_id"], first_version_id)
def test_publish_requires_review_and_approval_for_latest_generation_iteration(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=99,
owner_name="Diretoria",
owner_role=StaffRole.DIRETOR,
)
version_id = intake_payload["draft_preview"]["version_id"]
version = self.version_repository.get_by_version_id(version_id)
draft = self.draft_repository.get_by_tool_name("consultar_revisao_aberta")
metadata = self.metadata_repository.get_by_tool_version_id(version.id)
self.service.run_generation_pipeline(
version_id,
runner_staff_account_id=99,
runner_name="Diretoria",
runner_role=StaffRole.DIRETOR,
)
review_payload = self.service.review_version(
version_id,
reviewer_staff_account_id=99,
reviewer_name="Diretoria",
reviewer_role=StaffRole.DIRETOR,
decision_notes="Revisao inicial completa do codigo 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 inicial completa da versao governada.",
)
self.assertEqual(review_payload["status"], ToolLifecycleStatus.VALIDATED)
self.assertEqual(approve_payload["status"], ToolLifecycleStatus.APPROVED)
self.service._persist_generation_pipeline_artifact(
draft=draft,
version=version,
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
llm_generated_source=None,
llm_generation_model=None,
llm_generation_issues=[],
generation_iteration=2,
generation_mode="change_request_refinement",
feedback_notes="Foi identificada uma nova iteracao tecnica apos a aprovacao inicial.",
previous_source_checksum=self.service._compute_source_checksum(
self.service._get_generated_source_code_for_version(version.id)
),
prompt_rendered="prompt de refatoracao tecnica",
generation_elapsed_ms=10.0,
commit=None,
)
self.service._execute_automated_contract_validation(
draft=draft,
version=version,
metadata=metadata,
actor_staff_account_id=99,
actor_name="Diretoria",
llm_generated_source=None,
generation_iteration=2,
generation_mode="change_request_refinement",
change_request_notes="Foi identificada uma nova iteracao tecnica apos a aprovacao inicial.",
previous_source_checksum=self.service._compute_source_checksum(
self.service._get_generated_source_code_for_version(version.id)
),
commit=None,
)
with self.assertRaisesRegex(ValueError, "iteracao mais recente"):
self.service.publish_version(
version_id,
publisher_staff_account_id=99,
publisher_name="Diretoria",
publisher_role=StaffRole.DIRETOR,
)
def test_run_generation_pipeline_promotes_version_to_generated(self):
intake_payload = self.service.create_draft_submission(
{
@ -694,6 +1326,14 @@ class AdminToolManagementServiceTests(unittest.TestCase):
owner_role=StaffRole.COLABORADOR,
)
self.service.authorize_generation(
intake_payload["draft_preview"]["version_id"],
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes="A proposta pode seguir para geracao governada apos triagem inicial.",
)
payload = self.service.run_generation_pipeline(
intake_payload["draft_preview"]["version_id"],
runner_staff_account_id=8,
@ -763,6 +1403,14 @@ class AdminToolManagementServiceTests(unittest.TestCase):
)
self.metadata_repository.metadata_entries[0].display_name = "No"
self.service.authorize_generation(
intake_payload["draft_preview"]["version_id"],
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes="A proposta pode seguir para geracao governada apos triagem inicial.",
)
payload = self.service.run_generation_pipeline(
intake_payload["draft_preview"]["version_id"],
runner_staff_account_id=8,
@ -773,6 +1421,8 @@ class AdminToolManagementServiceTests(unittest.TestCase):
self.assertEqual(payload["status"], ToolLifecycleStatus.FAILED)
self.assertEqual(payload["current_step"], "generation")
self.assertEqual(payload["queue_entry"]["gate"], "pipeline_retry_required")
failed_detail = self.service.build_review_detail_payload(intake_payload["draft_preview"]["version_id"])
self.assertTrue(failed_detail["human_gate"]["run_pipeline_action_available"])
self.assertEqual(payload["queue_entry"]["automated_validation_status"], "failed")
self.assertIn("revisar contrato da tool", payload["queue_entry"]["automated_validation_summary"].lower())
automated_checks = {check["key"]: check for check in payload["automated_validations"]}
@ -814,6 +1464,14 @@ class AdminToolManagementServiceTests(unittest.TestCase):
owner_role=StaffRole.COLABORADOR,
)
self.service.authorize_generation(
intake_payload["draft_preview"]["version_id"],
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes="A proposta pode seguir para geracao governada apos triagem inicial.",
)
payload = self.service.run_generation_pipeline(
intake_payload["draft_preview"]["version_id"],
runner_staff_account_id=8,
@ -864,6 +1522,14 @@ class AdminToolManagementServiceTests(unittest.TestCase):
"_render_generated_tool_module_source",
return_value="async def run(:\n pass\n",
):
self.service.authorize_generation(
intake_payload["draft_preview"]["version_id"],
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes="A proposta pode seguir para geracao governada apos triagem inicial.",
)
payload = self.service.run_generation_pipeline(
intake_payload["draft_preview"]["version_id"],
runner_staff_account_id=8,
@ -915,6 +1581,14 @@ class AdminToolManagementServiceTests(unittest.TestCase):
"_render_generated_tool_module_source",
return_value='async def run(*, placa):\n raise RuntimeError("smoke test boom")\n',
):
self.service.authorize_generation(
intake_payload["draft_preview"]["version_id"],
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes="A proposta pode seguir para geracao governada apos triagem inicial.",
)
payload = self.service.run_generation_pipeline(
intake_payload["draft_preview"]["version_id"],
runner_staff_account_id=8,
@ -1100,12 +1774,18 @@ class AdminToolManagementServiceTests(unittest.TestCase):
self.assertEqual(payload["status"], ToolLifecycleStatus.APPROVED)
self.assertEqual(payload["human_gate"]["publication_action_available"], True)
self.assertEqual(payload["generation_context"]["latest_generation_iteration"], 1)
self.assertEqual(payload["generation_context"]["generation_iterations_count"], 1)
self.assertEqual(payload["generation_context"]["latest_generation_mode"], "initial_generation")
self.assertTrue(payload["generation_context"]["has_previous_generation"])
self.assertIsNone(payload["generation_context"]["latest_change_request_notes"])
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())
self.assertEqual(len(payload["decision_history"]), 3)
self.assertEqual(payload["decision_history"][0]["action_key"], ToolArtifactKind.GENERATION_AUTHORIZATION.value)
self.assertEqual(payload["decision_history"][1]["action_key"], ToolArtifactKind.DIRECTOR_REVIEW.value)
self.assertTrue(payload["decision_history"][1]["reviewed_generated_code"])
self.assertIn("aprovacao formal", payload["decision_history"][2]["decision_notes"].lower())
def test_publishing_new_version_archives_previous_active_version(self):
first_intake = self.service.create_draft_submission(
@ -1362,6 +2042,13 @@ class AdminToolManagementTransactionalPersistenceTests(unittest.TestCase):
)
version_id = intake_payload["draft_preview"]["version_id"]
service.authorize_generation(
version_id,
actor_staff_account_id=99,
actor_name="Diretoria",
actor_role=StaffRole.DIRETOR,
decision_notes="A proposta foi autorizada pela diretoria antes da geracao governada.",
)
service.run_generation_pipeline(
version_id,
runner_staff_account_id=7,

@ -454,8 +454,11 @@ class AdminToolsWebTests(unittest.TestCase):
self.assertEqual(payload["submission_policy"]["mode"], "draft_only")
self.assertEqual(payload["submission_policy"]["submitter_role"], "colaborador")
self.assertFalse(payload["submission_policy"]["submitter_can_publish_now"])
self.assertFalse(payload["submission_policy"]["submitter_can_authorize_generation_now"])
self.assertTrue(payload["submission_policy"]["direct_publication_blocked"])
self.assertTrue(payload["submission_policy"]["requires_generation_authorization"])
self.assertEqual(payload["submission_policy"]["required_approver_role"], "diretor")
self.assertEqual(payload["submission_policy"]["required_generation_permission"], "review_tool_generations")
self.assertEqual(payload["submission_policy"]["required_publish_permission"], "publish_tools")
self.assertEqual(payload["draft_preview"]["status"], "draft")
self.assertEqual(payload["draft_preview"]["domain"], "orquestracao")
@ -588,10 +591,14 @@ class AdminToolsWebTests(unittest.TestCase):
payload = response.json()
self.assertEqual(payload["tool_name"], "consultar_revisao_aberta")
self.assertTrue(payload["human_gate"]["review_action_available"])
self.assertFalse(payload["human_gate"]["run_pipeline_action_available"])
self.assertEqual(payload["generation_context"]["latest_generation_iteration"], 1)
self.assertEqual(payload["generation_context"]["generation_iterations_count"], 1)
self.assertEqual(payload["generation_context"]["latest_generation_mode"], "initial_generation")
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):
def test_tools_collaborator_cannot_run_generation_pipeline_before_director_authorization(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)
try:
intake_response = client.post(
@ -615,21 +622,8 @@ class AdminToolsWebTests(unittest.TestCase):
app.dependency_overrides.clear()
self.assertEqual(intake_response.status_code, 200)
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["status"], "generated")
self.assertEqual(payload["current_step"], "validation")
self.assertEqual(payload["queue_entry"]["gate"], "validation_required")
self.assertEqual(payload["queue_entry"]["automated_validation_status"], "passed")
self.assertEqual(payload["queue_entry"]["automated_validation_summary"], "4/4 validacoes automaticas passaram antes da revisao humana.")
automated_checks = {check["key"]: check for check in payload["automated_validations"]}
self.assertEqual(automated_checks["tool_contract"]["status"], "passed")
self.assertEqual(automated_checks["tool_signature_schema"]["status"], "passed")
self.assertEqual(automated_checks["tool_import_loading"]["status"], "passed")
self.assertEqual(automated_checks["tool_smoke_tests"]["status"], "passed")
steps_by_key = {step["key"]: step for step in payload["steps"]}
self.assertEqual(steps_by_key["generation"]["state"], "completed")
self.assertEqual(steps_by_key["validation"]["state"], "current")
self.assertEqual(response.status_code, 409)
self.assertIn("autorizacao de diretor", response.json()["detail"].lower())
def test_tools_publications_require_director_publication_permission(self):
client, app, _, _, _, _ = self._build_client_with_role(StaffRole.COLABORADOR)

@ -1,4 +1,4 @@
import unittest
import unittest
from fastapi.testclient import TestClient
@ -131,7 +131,8 @@ class AdminViewBootstrapTests(unittest.TestCase):
self.assertIn('data-intake-endpoint="/panel/tools/drafts/intake"', response.text)
self.assertIn('data-admin-tool-intake-form="true"', response.text)
self.assertIn("Adicionar parametro", response.text)
self.assertIn("Validar pre-cadastro", response.text)
self.assertIn("Criar proposta", response.text)
self.assertIn("Nenhuma proposta criada ainda", response.text)
def test_tool_review_page_redirects_to_login_without_session(self):
app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0"))
@ -152,7 +153,8 @@ class AdminViewBootstrapTests(unittest.TestCase):
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
self.assertIn("Revisao, aprovacao e ativacao", response.text)
self.assertIn("Triagem, revisao e ativacao", response.text)
self.assertIn("Fluxo governado de proposta, codigo e ativacao", response.text)
self.assertIn('data-admin-tool-review-board="true"', response.text)
self.assertIn('data-overview-endpoint="/panel/tools/overview"', response.text)
self.assertIn('data-contracts-endpoint="/panel/tools/contracts"', response.text)
@ -160,8 +162,14 @@ class AdminViewBootstrapTests(unittest.TestCase):
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="authorize-generation"', response.text)
self.assertIn('data-tool-review-action="run-pipeline"', response.text)
self.assertIn('data-tool-review-action="request-changes"', response.text)
self.assertIn('data-tool-review-action="close"', response.text)
self.assertIn('data-tool-review-action="deactivate"', response.text)
self.assertIn('data-tool-review-action="rollback"', response.text)
self.assertIn("Codigo completo da iteracao atual", response.text)
self.assertIn("Ativar no catalogo", response.text)
self.assertNotIn("Abrir login administrativo", response.text)
def test_collaborator_management_page_redirects_to_login_without_session(self):

Loading…
Cancel
Save