use std::path::PathBuf;
use crate::tui::runner::{required_sections_for, StageEvent, StageResult, TokenUsageSample};
use crate::tui::stage::Stage;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum AgentSlotStatus {
Aguardando,
EmExecucao,
Concluido,
Erro,
}
impl AgentSlotStatus {
pub fn is_terminal(self) -> bool {
matches!(self, AgentSlotStatus::Concluido | AgentSlotStatus::Erro)
}
pub fn label(self) -> &'static str {
match self {
AgentSlotStatus::Aguardando => "Aguardando",
AgentSlotStatus::EmExecucao => "Em execução",
AgentSlotStatus::Concluido => "Concluído",
AgentSlotStatus::Erro => "Erro",
}
}
}
pub struct AgentHandle {
pub task_id: String,
pub task_label: String,
pub rx: std::sync::mpsc::Receiver<StageEvent>,
pub status: AgentSlotStatus,
pub trace: Vec<String>,
pub usage: Option<TokenUsageSample>,
pub result: Option<StageResult>,
pub fragment_path: PathBuf,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TaskRef {
pub id: String,
pub title: String,
pub independent: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SlotResult {
pub task_id: String,
pub task_label: String,
pub status: AgentSlotStatus,
pub fragment: String,
}
pub fn supports_parallel(stage: Stage) -> bool {
matches!(stage, Stage::Execution | Stage::Review)
}
pub fn plan_parallel_slots(tasks_md: &str) -> Vec<TaskRef> {
let independent_ids = extract_independent_ids(tasks_md);
parse_backlog_table(tasks_md, &independent_ids)
}
fn extract_independent_ids(md: &str) -> std::collections::HashSet<String> {
let mut ids = std::collections::HashSet::new();
let mut in_section = false;
for line in md.lines() {
let trimmed = line.trim();
if trimmed.starts_with('#') {
let heading = trimmed.trim_start_matches('#').trim().to_lowercase();
in_section = heading.contains("independentes");
continue;
}
if !in_section {
continue;
}
let mut rest = trimmed;
while let Some(pos) = rest.find("T-") {
let after = &rest[pos + 2..];
let digits: String = after.chars().take_while(|c| c.is_ascii_digit()).collect();
if !digits.is_empty() {
ids.insert(format!("T-{}", digits));
}
rest = &rest[pos + 2..];
}
}
ids
}
fn parse_backlog_table(
md: &str,
independent_ids: &std::collections::HashSet<String>,
) -> Vec<TaskRef> {
let mut tasks = Vec::new();
let mut in_backlog = false;
for line in md.lines() {
let trimmed = line.trim();
if trimmed.starts_with('#') {
let heading = trimmed.trim_start_matches('#').trim().to_lowercase();
in_backlog = heading == "backlog";
continue;
}
if !in_backlog {
continue;
}
if !trimmed.starts_with("| T-") {
continue;
}
let cols: Vec<&str> = trimmed.split('|').collect();
if cols.len() < 5 {
continue;
}
let id = cols[1].trim().to_string();
let title = cols[2].trim().to_string();
let deps = cols[4].trim();
if !id.starts_with("T-") || id[2..].chars().any(|c| !c.is_ascii_digit()) {
continue;
}
let independent = independent_ids.contains(&id) || has_no_declared_dependency(deps);
tasks.push(TaskRef {
id,
title,
independent,
});
}
tasks
}
fn has_no_declared_dependency(deps: &str) -> bool {
let normalized = deps.trim();
normalized.is_empty() || matches!(normalized, "—" | "-" | "–" | "Nenhuma" | "nenhuma")
}
pub fn compose_parallel_artifact(stage: Stage, results: &[SlotResult]) -> String {
let mut out = String::new();
out.push_str(&format!("# {} — Agent Teams Paralelos\n\n", stage.label()));
out.push_str("## Rastreabilidade\n");
out.push_str("- Origem: `docs/<slug>/04-tasks.md`\n");
out.push_str(&format!("- Etapa: `{}`\n", stage.key()));
out.push_str("- Modo: execução paralela por AgentSlot\n\n");
out.push_str(&traceability_table(results));
out.push('\n');
match stage {
Stage::Execution => append_execution_sections(&mut out, results),
Stage::Review => append_review_sections(&mut out, results),
_ => append_generic_sections(&mut out, stage, results),
}
ensure_required_sections(&mut out, stage);
out
}
fn append_execution_sections(out: &mut String, results: &[SlotResult]) {
out.push_str("## Tarefa\n");
out.push_str(&format!(
"- Executar {} task(s) por Agent Teams paralelos.\n",
results.len()
));
out.push_str("- Cada task recebeu exatamente um AgentSlot no artefato consolidado.\n\n");
out.push_str("## Resumo da implementação\n");
append_slot_fragments(out, results);
out.push_str("## Arquivos alterados\n");
out.push_str(
"- Consolidado a partir dos fragmentos emitidos pelos AgentSlots; detalhes ficam nos fragmentos de cada task.\n\n",
);
out.push_str("## Testes e evidências\n");
append_status_evidence(out, results);
out.push_str("## Riscos e pendências\n");
append_risks(out, results);
}
fn append_review_sections(out: &mut String, results: &[SlotResult]) {
out.push_str("## Escopo revisado\n");
out.push_str(&format!(
"- Review paralela de {} task(s) planejadas.\n\n",
results.len()
));
out.push_str("## Evidências\n");
append_status_evidence(out, results);
out.push_str("## Achados\n");
append_slot_fragments(out, results);
out.push_str("## Testes\n");
let errors = results
.iter()
.filter(|result| result.status == AgentSlotStatus::Erro)
.count();
if errors == 0 {
out.push_str("- Todos os AgentSlots concluíram sem erro reportado.\n\n");
} else {
out.push_str(&format!(
"- {} AgentSlot(s) falharam; revisar pendências antes de aprovar.\n\n",
errors
));
}
out.push_str("## Veredito\n");
if errors == 0 {
out.push_str("- Aprovado para checkpoint humano.\n\n");
} else {
out.push_str("- Aprovado com ressalvas: há AgentSlots em erro registrados acima.\n\n");
}
}
fn append_generic_sections(out: &mut String, stage: Stage, results: &[SlotResult]) {
for section in required_sections_for(stage) {
if *section == "Rastreabilidade" {
continue;
}
out.push_str(&format!("## {section}\n"));
append_slot_fragments(out, results);
}
}
fn append_slot_fragments(out: &mut String, results: &[SlotResult]) {
if results.is_empty() {
out.push_str("- Nenhum AgentSlot foi planejado.\n\n");
return;
}
for result in results {
out.push_str(&format!(
"### {} — {}\n\n",
result.task_id, result.task_label
));
match result.status {
AgentSlotStatus::Concluido => {
let fragment = result.fragment.trim();
if fragment.is_empty() {
out.push_str("> [ERRO] slot concluiu sem fragmento materializado\n\n");
} else {
out.push_str(fragment);
out.push_str("\n\n");
}
}
_ => {
out.push_str("> [ERRO] slot falhou — fragmento ausente\n\n");
}
}
}
}
fn append_status_evidence(out: &mut String, results: &[SlotResult]) {
if results.is_empty() {
out.push_str("- Nenhum AgentSlot executado.\n\n");
return;
}
for result in results {
out.push_str(&format!(
"- `{}`: {}.\n",
result.task_id,
result.status.label()
));
}
out.push('\n');
}
fn append_risks(out: &mut String, results: &[SlotResult]) {
let failed = results
.iter()
.filter(|result| result.status == AgentSlotStatus::Erro)
.collect::<Vec<_>>();
if failed.is_empty() {
out.push_str("- Nenhuma pendência automática registrada pelos AgentSlots.\n\n");
return;
}
out.push_str("- Resultado parcial aceito ou barreira concluída com falhas isoladas.\n");
for result in failed {
out.push_str(&format!(
"- `{}` — {}: AgentSlot em erro; fragmento não disponível.\n",
result.task_id, result.task_label
));
}
out.push('\n');
}
fn ensure_required_sections(out: &mut String, stage: Stage) {
for section in required_sections_for(stage) {
if out.contains(&format!("## {section}\n")) {
continue;
}
out.push_str(&format!("## {section}\n"));
out.push_str("- Seção preservada para compatibilidade com o contrato do artefato.\n\n");
}
}
pub fn traceability_table(results: &[SlotResult]) -> String {
let mut out = String::new();
out.push_str("| Task ID | AgentSlot | Título | Status |\n");
out.push_str("|---------|-----------|--------|--------|\n");
for r in results {
out.push_str(&format!(
"| {} | agent-{} | {} | {} |\n",
escape_markdown_cell(&r.task_id),
escape_markdown_cell(&r.task_id),
escape_markdown_cell(&r.task_label),
r.status.label()
));
}
out
}
fn escape_markdown_cell(input: &str) -> String {
input.replace('|', "\\|").replace('\n', " ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn supports_parallel_returns_true_for_execution() {
assert!(supports_parallel(Stage::Execution));
}
#[test]
fn supports_parallel_returns_true_for_review() {
assert!(supports_parallel(Stage::Review));
}
#[test]
fn supports_parallel_returns_false_for_idea() {
assert!(!supports_parallel(Stage::Idea));
}
#[test]
fn supports_parallel_returns_false_for_prd() {
assert!(!supports_parallel(Stage::Prd));
}
#[test]
fn supports_parallel_returns_false_for_techspec() {
assert!(!supports_parallel(Stage::Techspec));
}
#[test]
fn supports_parallel_returns_false_for_tasks() {
assert!(!supports_parallel(Stage::Tasks));
}
#[test]
fn supports_parallel_returns_false_for_refinement() {
assert!(!supports_parallel(Stage::Refinement));
}
#[test]
fn supports_parallel_returns_false_for_memory() {
assert!(!supports_parallel(Stage::Memory));
}
#[test]
fn empty_string_returns_empty() {
assert!(plan_parallel_slots("").is_empty());
}
#[test]
fn no_table_returns_empty() {
let md = "# Título\n\nAlgum texto sem tabela.\n";
assert!(plan_parallel_slots(md).is_empty());
}
#[test]
fn identifies_independent_tasks() {
let md = "\
## Backlog
| ID | Título | Estimativa | Dependências | Arquivos | Status |
|----|--------|-----------|-------------|----------|--------|
| T-01 | Criar módulo base | P (2h) | — | src/lib.rs | Todo |
| T-02 | Implementar parser | P (2h) | T-01 | src/parser.rs | Todo |
| T-03 | Adicionar testes | P (1h) | T-01 | tests/mod.rs | Todo |
### Tasks independentes entre si (podem rodar em paralelo)
- **Grupo A** (após T-01 concluída): T-02 e T-03 são independentes entre si.
";
let result = plan_parallel_slots(md);
assert_eq!(result.len(), 3);
let t01 = result
.iter()
.find(|t| t.id == "T-01")
.expect("T-01 presente");
let t02 = result
.iter()
.find(|t| t.id == "T-02")
.expect("T-02 presente");
let t03 = result
.iter()
.find(|t| t.id == "T-03")
.expect("T-03 presente");
assert!(t01.independent, "T-01 deve ser independent (dep=—)");
assert!(t02.independent, "T-02 deve ser independent (seção)");
assert!(t03.independent, "T-03 deve ser independent (seção)");
}
#[test]
fn task_with_explicit_dep_not_independent() {
let md = "\
## Backlog
| ID | Título | Estimativa | Dependências | Arquivos | Status |
|----|--------|-----------|-------------|----------|--------|
| T-01 | Base | P (1h) | — | src/lib.rs | Todo |
| T-02 | Dependente | P (1h) | T-01 | src/dep.rs | Todo |
";
let result = plan_parallel_slots(md);
let t02 = result
.iter()
.find(|t| t.id == "T-02")
.expect("T-02 presente");
assert!(
!t02.independent,
"T-02 tem dep explícita, nao deve ser independent"
);
}
#[test]
fn preserves_declaration_order() {
let md = "\
## Backlog
| ID | Título | Estimativa | Dependências | Arquivos | Status |
|----|--------|-----------|-------------|----------|--------|
| T-03 | Terceira | P (1h) | — | a.rs | Todo |
| T-01 | Primeira | P (1h) | — | b.rs | Todo |
| T-02 | Segunda | P (1h) | — | c.rs | Todo |
";
let result = plan_parallel_slots(md);
assert_eq!(result.len(), 3);
assert_eq!(result[0].id, "T-03");
assert_eq!(result[1].id, "T-01");
assert_eq!(result[2].id, "T-02");
}
#[test]
fn malformed_markdown_no_panic() {
let result = plan_parallel_slots("não é um markdown válido\n### cabeçalho\n| lixo |");
let _ = result;
}
fn slot(id: &str, label: &str, status: AgentSlotStatus, fragment: &str) -> SlotResult {
SlotResult {
task_id: id.to_string(),
task_label: label.to_string(),
status,
fragment: fragment.to_string(),
}
}
#[test]
fn traceability_table_has_header() {
let table = traceability_table(&[]);
assert!(table.contains("| Task ID | AgentSlot | Título | Status |"));
assert!(table.contains("|---------|-----------|--------|--------|"));
}
#[test]
fn traceability_table_maps_all_statuses() {
let results = vec![
slot("T-01", "Alpha", AgentSlotStatus::Aguardando, ""),
slot("T-02", "Beta", AgentSlotStatus::EmExecucao, ""),
slot("T-03", "Gamma", AgentSlotStatus::Concluido, "ok"),
slot("T-04", "Delta", AgentSlotStatus::Erro, ""),
];
let table = traceability_table(&results);
assert!(table.contains("Aguardando"));
assert!(table.contains("Em execução"));
assert!(table.contains("Concluído"));
assert!(table.contains("Erro"));
}
#[test]
fn compose_includes_rastreabilidade_no_topo() {
let results = vec![
slot("T-01", "Slot A", AgentSlotStatus::Concluido, "fragmento A"),
slot("T-02", "Slot B", AgentSlotStatus::Concluido, "fragmento B"),
];
let out = compose_parallel_artifact(Stage::Execution, &results);
let table_pos = out.find("| Task ID |").expect("tabela presente");
let frag_pos = out.find("### T-01").expect("fragmento presente");
assert!(
table_pos < frag_pos,
"tabela de rastreabilidade deve aparecer antes dos fragmentos"
);
for section in required_sections_for(Stage::Execution) {
assert!(
out.contains(&format!("## {section}\n")),
"seção obrigatória ausente: {section}"
);
}
}
#[test]
fn compose_marks_error_slot() {
let results = vec![
slot("T-01", "Slot OK", AgentSlotStatus::Concluido, "conteúdo ok"),
slot("T-02", "Slot Falho", AgentSlotStatus::Erro, ""),
];
let out = compose_parallel_artifact(Stage::Review, &results);
assert!(
out.contains("[ERRO]"),
"slot de erro deve conter marcador [ERRO]"
);
assert!(
out.contains("conteúdo ok"),
"slot concluído deve preservar fragmento"
);
}
#[test]
fn compose_is_pure_same_output_twice() {
let results = vec![
slot("T-01", "Alpha", AgentSlotStatus::Concluido, "frag"),
slot("T-02", "Beta", AgentSlotStatus::Erro, ""),
];
let first = compose_parallel_artifact(Stage::Execution, &results);
let second = compose_parallel_artifact(Stage::Execution, &results);
assert_eq!(
first, second,
"função pura: saída idêntica em chamadas repetidas"
);
}
#[test]
fn compose_empty_results() {
let out = compose_parallel_artifact(Stage::Execution, &[]);
assert!(out.contains("# Execution"));
}
}