Migrate all docs scattered across hidden folders into docs/: - .compozy/tasks/ → docs/planning/ (PRDs, tech specs, tasks, memory) and docs/adr/ (ADRs by phase) - .specs/ → docs/specs/ (codebase, features, project specs) - .docs/ → docs/legacy/ (legacy reference implementation docs) - .discussions/ → docs/discussions/ (design discussions and open questions) Delete .compozy/, .specs/, .docs/, and .discussions/. Update AGENTS.md: add explicit docs/ location table, hard rule against hidden doc folders, and new AI-assisted development section (context lazy loading, scope control, verification). Update CLAUDE.md: fix .docs/ references to docs/legacy/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15 KiB
Discussões de Design — Camada de Embarcação
Questões arquiteturais abertas para a Fase 1. Antes de ler as perguntas, leia o contexto abaixo — sem ele as opções não fazem sentido.
Contexto geral: o que já existe e o que vamos construir
O tick hoje (sem controlador)
Cada chamada a NavalSimulation::step(dt) executa esta sequência:
1. buildContext(dt) → monta NavalContext com posição, velocidade, ambiente
2. sim_.step(dt) → chama cada ForceModel, depois CVODE integra
Os ForceModels (ThrustForces, RudderForces, TugForces) lêem o NavalContext para saber
a cinemática atual da embarcação (velocidade, heading) e produzem forças no referencial do corpo.
O CVODE integra essas forças e atualiza a posição/velocidade.
O tick depois da Fase 1 (com controlador)
1. vessel.updateControl(t, dt) → controlador ativo calcula e escreve demandas
2. vessel.updateStates(t, dt) → atuadores aplicam filtros, computa cinemática derivada
3. buildContext(dt) → monta NavalContext (agora inclui demandas aplicadas)
4. sim_.step(dt) → ForceModels leem NavalContext + CVODE integra
As três structs que aparecem nas perguntas
ThrusterConfig — struct que descreve um propulsor. Tem tanto parâmetros físicos fixos
quanto o campo que muda a cada tick:
struct ThrusterConfig {
double diameter = 1.0; // fixo: tamanho físico do propulsor
double pitchRatio = 1.0; // fixo: geometria da hélice
double rotationTime = 50.0; // fixo: constante de tempo de 1ª ordem
double commandRPM = 0.0; // MUDA A CADA TICK: demanda do controlador
// ... mais parâmetros físicos fixos
};
NavalContext — snapshot read-only montado no início de cada tick. Contém o estado
cinemático atual da embarcação (posição, velocidades no referencial do corpo, ambiente).
Os ForceModels recebem isso como const NavalContext& — não podem modificar.
VesselState — vai ser criado na Fase 1. Vai carregar o estado operacional visível
da embarcação: luzes de navegação acesas, modo operacional (Underway, Anchored, Moored...),
fase atual da FSM de atracação, etc.
Q1 — Como o controlador entrega a demanda de RPM ao modelo de força?
O problema concreto
ManeuverController calcula que o propulsor precisa de 80 RPM para atingir a velocidade
demandada. ThrustForces precisa saber disso no mesmo tick para calcular a força de empuxo.
Hoje ThrustForces já lê commandRPM de dentro de ThrusterConfig. Mas quem escreve
esse valor? Hoje ninguém — é configurado uma vez e fica fixo. Na Fase 1, o controlador
precisa escrevê-lo a cada tick.
A tensão real: ThrusterConfig mistura dados físicos fixos (diâmetro, curva de
rendimento) com estado dinâmico de runtime (commandRPM). Essa mistura é um smell —
mas já existe no código. A questão é: resolvemos agora ou aceitamos e seguimos?
Opção A — Controlador escreve direto na config
void ManeuverController::compute(ThrusterConfig& thruster, RudderConfig& rudder) {
thruster.commandRPM = pid_speed_.compute(speedError);
rudder.angle_deg = pid_heading_.compute(headingError);
}
ThrustForces não muda nada — já lê commandRPM de onde sempre leu.
- Prós: zero refatoração. Funciona agora.
- Contras:
ThrusterConfigvira um objeto com dupla personalidade — parâmetros imutáveis de design + estado mutável de operação. Dificulta salvar a configuração original e comparar com o estado atual.
Opção B — NavalContext ganha seção mutável de demandas
struct NavalContext {
BodyState state; // cinemática — read-only
Vector6 speedToWater; // ambiente — read-only
// NOVO:
struct ControlDemand {
std::vector<double> rpmDemand;
std::vector<double> rudderDemand;
} demand;
};
Controlador escreve em ctx.demand. ThrustForces lê de ctx.demand.rpmDemand[i].
- Prós: NavalContext vira o canal único entre controle e física.
- Contras: NavalContext era uma abstração limpa de "snapshot de estado". Agora tem seção mutável — a semântica quebra. Todo ForceModel recebe um campo
demandque não usa.
Opção C — Separar config estática de estado dinâmico
struct ThrusterConfig { // imutável após init
double diameter = 1.0;
double pitchRatio = 1.0;
double rotationTime = 50.0;
// SEM commandRPM aqui
};
struct ThrusterState { // muda a cada tick
double commandRPM = 0.0;
double currentRPM = 0.0; // estado interno do filtro de 1ª ordem
};
Controlador escreve em ThrusterState. ThrustForces recebe ambos.
- Prós: separação clara. Config pode ser
const. State é explicitamente mutável. Mais fácil de testar. - Contras: refatoração de
ThrustForceseRudderForces— que já existem e funcionam.
Opinião do Claude — Q1
Opção C é a mais limpa mas requer refatorar código que já funciona. Opção B quebra uma abstração que vale a pena preservar. Opção A é um atalho com dívida técnica explícita.
Preferência: A agora, com comentário claro marcando a dívida, e C quando for necessário serializar ou comparar configurações.
Sua opinião — Q1
Escreva aqui
Eu tenho uma opinião diferente, me fala sua opinião sobre isso. Eu modelaria diferente, vc está modelando o thruster apenas como um modelo de força, para mim ele é mais que uma força, claro que a interação entre ele e o corpo é dada por uma força, mas o thruster tem comportamente, tem lógica propria, no caso ele tem tres grandezes que podem ser modificadas, a rotação, pitch e azimuth, e nem uma delas vai direto para um valor, se eu demando um rpm de 100% e ele está em 0%, tem um tempo para chegar nos 100%, isso deve calculado a cada tick, portanto o thruster e de forma analoga, o rudder devem ser entidades de um vessel. Eu modelaria uma aggregate root chamado DynamicVessel, que é um navio com todas os graus de liberdade e tudo mais, esse DynamicVesse possui rudders e thrusters, e esses rudders e thrusters não são apenas uma config, eles representam um ente de verdade, são realmente entidades nesse sentido.
Q2 — Como a classe Vessel escolhe qual controlador rodar a cada tick?
O problema concreto
Vessel vai ter três controladores implementados. A cada tick, exatamente um está ativo.
O usuário pode trocar o modo entre ticks (ex: de maneuver para berthManeuver).
Internamente, Vessel::updateControl precisa:
- Saber qual controlador está ativo
- Chamar o método
compute()desse controlador - Escrever os resultados nos atuadores
A questão é a estrutura de dados que guarda os controladores e o mecanismo de dispatch.
Opção A — std::variant + std::visit
class Vessel {
using Controller = std::variant<
Drift, ManeuverController, BerthManeuverSystem, PrescribedController
>;
Controller controller_;
};
void Vessel::updateControl(double t, double dt) {
std::visit([&](auto& ctrl) { ctrl.compute(t, dt, config_); }, controller_);
}
// Trocar de modo:
void Vessel::setMode(ControlMode m) {
if (m == ControlMode::Maneuver)
controller_ = ManeuverController{};
}
- Prós: sem virtual, sem heap, tipo-seguro em compilação.
- Contras: conjunto de controladores é fechado — não dá pra plugar controlador externo em runtime sem mudar o tipo do variant.
Opção B — Interface abstrata IController
struct IController {
virtual void compute(double t, double dt, VesselConfig& cfg) = 0;
virtual ~IController() = default;
};
class Vessel {
std::unique_ptr<IController> controller_;
};
void Vessel::updateControl(double t, double dt) {
controller_->compute(t, dt, config_);
}
- Prós: aberto para extensão — alguém cria
MyCustomController : IControllersem tocar emVessel. - Contras:
make_uniquea cada troca de modo = alocação. Virtual call por tick. Interface abstrata antes de saber se extensibilidade vai ser necessária.
Opção C — std::optional por controlador + if/else
class Vessel {
ControlMode mode_ = ControlMode::Drift;
std::optional<ManeuverController> maneuver_;
std::optional<BerthManeuverSystem> berth_;
std::optional<PrescribedController> prescribed_;
};
void Vessel::updateControl(double t, double dt) {
if (mode_ == ControlMode::Maneuver && maneuver_)
maneuver_->compute(t, dt, config_);
else if (mode_ == ControlMode::BerthManeuver && berth_)
berth_->compute(t, dt, config_);
}
- Prós: explícito, sem abstração, fácil de ler.
- Contras: adicionar controlador = modificar
Vesselem 4+ lugares. Não escala.
Opinião do Claude — Q2
O conjunto de controladores da Fase 1 é fechado e conhecido. std::variant é o fit
natural para C++17 com conjunto fechado de tipos. Se surgir necessidade de
extensibilidade externa em fases futuras, migra para B — a mudança é localizada em Vessel.
Preferência: A (std::variant + std::visit).
Sua opinião — Q2
Escreva aqui
Eu não entendi esses controllers, mas eu aceito a opção A.
Q3 — Como o sistema de âncora (Fase 4) vai avisar o VesselState que a âncora prendeu?
O problema concreto
VesselState mantém o estado operacional da embarcação: Underway, Anchored, Moored, etc.
Quando a âncora prende no fundo, o estado deve mudar automaticamente para Anchored.
O problema: o sistema de âncora não existe ainda (é Fase 4). Mas VesselState precisa ser
projetado agora. Se projetarmos mal a interface de notificação, vamos ter que reescrever
VesselState na Fase 4 quando a âncora chegar.
Na Fase 1, as únicas mudanças de estado operacional são manuais (o usuário chama uma API). A questão é: qual interface de notificação projetar já pensando na Fase 4?
Opção A — Métodos diretos no VesselState
// Fase 4: sistema de âncora chama diretamente:
vesselState.onAnchorHolding();
vesselState.onMooringAttached();
// Fase 1: usuário chama diretamente:
vesselState.setOperationalState(OperationalState::Anchored);
- Prós: simples, direto.
- Contras: sistema de âncora (Fase 4) vai precisar incluir
VesselState. Dependendo de onde o sistema de âncora viver na hierarquia de bounded contexts, isso pode conflitar com as regras de dependência doAGENTS.md.
Opção B — Callbacks std::function registrados
vesselState.onAnchorHolding([]{ /* transiciona para Anchored */ });
// Fase 4: âncora dispara um callback genérico, não conhece VesselState:
anchorSystem.setOnHoldingCallback([&]{ vesselState.onAnchorHolding(); });
- Prós: sistema de âncora não precisa incluir
VesselState. - Contras:
std::functioncapturando referências tem risco de dangling pointer. Ordem de registro importa.
Opção C — Fila de eventos tipada drenada pelo Vessel a cada tick
enum class VesselEvent { AnchorHolding, MooringAttached };
// Fase 4: âncora empurra evento na fila:
events.push(VesselEvent::AnchorHolding);
// Vessel drena no início do tick:
void Vessel::updateControl(double t, double dt) {
while (auto ev = events_.pop()) {
if (*ev == VesselEvent::AnchorHolding)
state_.setOperationalState(OperationalState::Anchored);
}
// ... dispatch para controlador ativo
}
- Prós: desacoplamento total. Produtor (âncora) e consumidor (VesselState) não se conhecem.
- Contras: infraestrutura que ficará ociosa até a Fase 4. A fila em si precisa ser projetada (thread-safe? por valor ou ponteiro?).
Opinião do Claude — Q3
Na Fase 1 não existe sistema de âncora — toda infraestrutura de eventos ficaria ociosa.
Porém a opção A tem um problema real: âncora em libs/simulation/ precisando incluir
VesselState de libs/vessel/ pode ser inválido nas regras de bounded context.
Preferência: A para Fase 1 (só API manual). Decidir entre B e C quando o sistema de âncora for especificado na Fase 4 — o desenho correto depende de onde ele vai viver.
Sua opinião — Q3
Escreva aqui
Acho que aqui temos um problema, a relação entre a ancora e o vessel é uma linha, um tipo de interação que é calculada, é a ancora que está unhada, não o vessel, a ancora unhada transmite uma força para o vessel através de uma linha que conecta os dois, portanto o que vamos ter que modelar é a linha no vessel como uma força, o resto ja está feito é só integrar essa força, portanto não se deve ter uma função no vessel de anchorHolding ou qualquer outra coisa, o vessel não deve saber da ancora especificamente. Vai existir um modelo de guincho que controla o tamanho da linha, mas esse é o único controle que terá a ver com a ancora dentro do vessel, mas no fim é tratado como uma força do mesmo jeito.
Q4 — Quem cria e possui a instância de Vessel?
O problema concreto
NavalSimulation::step(dt) precisa chamar vessel.updateControl(t, dt) e
vessel.updateStates(t, dt) a cada tick. Mas o usuário também precisa de acesso
ao Vessel entre ticks — para trocar waypoints, ler VesselState, mudar o modo de controle.
A questão é: quem é o dono do objeto Vessel? Isso determina quem controla o lifetime,
quem pode chamar métodos nele, e como os dois objetos são criados.
Opção A — NavalSimulation é dono, recebe via unique_ptr
auto vessel = std::make_unique<Vessel>(config);
Vessel* vessel_ptr = vessel.get(); // guarda ponteiro não-owning ANTES de mover
NavalSimulation sim(std::move(vessel));
// Entre ticks, usuário usa o ponteiro não-owning:
vessel_ptr->setWaypoints(waypoints);
sim.step(dt);
- Prós:
NavalSimulationgarante queVesselexiste enquanto a simulação existe. - Contras: usuário precisa guardar ponteiro não-owning separado antes do
move. Se esquecer, perde acesso direto. Lifetime implícito.
Opção B — Usuário é dono, passa referência para NavalSimulation
Vessel vessel(config);
NavalSimulation sim(body, vessel); // sim recebe Vessel& — não-owning
// Entre ticks, usuário usa diretamente:
vessel.setWaypoints(waypoints);
auto state = vessel.state();
sim.step(dt);
- Prós: acesso direto e limpo. Lifetime explícito. Testável sem
NavalSimulation. - Contras: usuário gerencia lifetimes. Se destruir
vesselantes desim, comportamento indefinido.
Opção C — Fábrica cria os dois juntos
auto [sim, vessel] = makeNavalVessel(body, config);
vessel.setWaypoints(waypoints);
sim.step(dt);
- Prós: conveniente, impossível ter um sem o outro.
- Contras: os dois têm lifetimes diferentes por natureza. Acoplá-los em fábrica é artificial.
Opinião do Claude — Q4
Opção B segue o mesmo padrão que VesselConfig já usa no projeto: usuário cria, simulação
usa como referência. Lifetime explícito é melhor que lifetime escondido dentro de
NavalSimulation. Em testes de unidade do controlador, você cria um Vessel, chama
updateControl diretamente, e não precisa montar uma NavalSimulation inteira.
Preferência: B. Usuário possui Vessel. NavalSimulation recebe Vessel&.
Sua opinião — Q4
Escreva aqui
Acho que a gente ja falou disso, o modelo que temos hj de um único vessel está errado, uma simulação deve ter N corpos, e o modelo de ter NavalSimulation restringe futuras integrações entre dominios diferentes de simulação, o ideal é ter um world, nele estão todos os objetos, e através desse world que controlamos tudo.