Skip to content

Control Plane vs Data-Preparation

Regra simples

  • se a pergunta é “qual modelo está ativo, por quê e para quem?”, isso pertence ao control plane;
  • se a pergunta é “quais linhas entram no treino, como o snapshot é lido e como os frames são particionados?”, isso pertence ao boundary de data-preparation.

Essa separação existe para deixar o código honesto com o problema real do produto: metadado de modelagem é relativamente pequeno e durável; histórico de demanda é grande, columnar e transitório.

Control plane

Responsável por:

  • ModelDefinition, ModelVersion e ModelInstance;
  • submissão de workflows de treino ao Airflow;
  • auditoria de promoção;
  • promoção transacional de múltiplos campeões;
  • ModelInstance guarda lineage de treino;
  • lineage explícita de treino por snapshot_id em instâncias e resultados de submissão;
  • metadados futuros de feature catalog, lineage e configuração de features.

Fonte de verdade:

  • ClickHouse para estado operacional e governança;
  • MLflow para tracking de treino, packaging e registry sync atrás de adapters de infra.

Tipos centrais:

  • SeriesKey
  • ModelDefinition
  • ModelInstance

Portas importantes:

  • ModelingRepositoryPort
  • DemandSnapshotManifestRepositoryPort
  • ForecastingDatasetOperationsPort

Data-preparation

Responsável por:

  • descrever snapshots canônicos;
  • persistir manifests de snapshot explicitamente;
  • validar schema e contagens declaradas do snapshot;
  • validar schema e invariantes estruturais sobre frames por uma única porta;
  • carregar e persistir frames canônicos de treino por snapshot_id;
  • fazer split temporal de holdout;
  • preparar a base para future feature frames e joins de catálogo.

Fonte de verdade:

  • parquet / filesystem / S3 para snapshots e outros payloads pesados;
  • memória apenas como working set de execução.

Tipos centrais:

  • DemandSnapshotManifest
  • TRAINING_FEATURE_FRAME_SCHEMA
  • PredictResult.forecast_frame

Contrato compartilhado:

  • os schemas e nomes de colunas canônicos vivem em control/data_preparation/frame_schemas.py;
  • adapters concretos podem validar e converter frames, mas não devem virar fonte de verdade para outros adapters ou estratégias;
  • não há adaptação HTTP para previsão online no desenho atual; a inferência suportada é batch/offline, via DAG model_prediction, e existe como exceção explícita do serviço command-side.

Implementações concretas hoje:

  • DemandSnapshotManifestRepository
  • SparkTabularDataRepository
  • SparkDemandIngestionPipeline
  • write_projected_series_frame
  • PolarsTrainingDatasetOperations

Validação de input e frames

O único motor de validação de demanda é DemandValidationCatalog em control/data_preparation/validation. Ele executa as regras concretas registradas pelo adapter usando um contexto tipado de validação. O contexto define a fase e o runtime disponível: ingestão recebe frame e organization_id; materialização também carrega DemandSnapshotManifest; treino e predição carregam manifesto e series_projection_recipe.

DemandValidationCatalog.validate(context=..., rule_set=...) rejeita regras incompatíveis com a fase do contexto. A strategy dessa fase resolve os params finais da regra: usa params declarados na configuração quando existem e deriva params do runtime tipado quando a regra depende da fase. Regras de manifesto (schema_version, manifest_row_count, manifest_series_count) recebem valores derivados do DemandSnapshotManifest; regras de projeção (required_projection_dimensions) recebem valores derivados da series_projection_recipe. A configuração externa pode declarar validation_policies agrupadas por fase, e o validador rejeita regras que exigem contexto indisponível naquela fase.

O catálogo de regras é semanticamente único, mas cada adapter registra apenas o que executa no runtime atual. Spark cobre ingestão, projeção e materialização do parquet particionado por SeriesKey.series_id; Polars cobre leitura dessas partições, treino e predição. A execução concreta fica organizada em infra/data_preparation/{polars,spark}/validation com contexts.py, rules.py e catalog.py.

No runtime de jobs, a materialização carrega o payload e entrega o frame ao pipeline de ingestão do adapter. Esse pipeline executa a política da fase de ingestão pelo catálogo de regras e só depois canonicaliza o frame para persistência. Não há validador legado de input bruto fora do sistema ValidationRules.

Onde SeriesKey cruza os dois lados

SeriesKey continua no domínio porque é a identidade estável usada para:

  • versionamento de instâncias;
  • promoção;
  • nomes derivados de artefatos;
  • auditoria.

Ao mesmo tempo, essa identidade aparece embutida nos frames canônicos do data data-preparation. Isso é esperado: o frame carrega a identidade sem virar aggregate de domínio.

O vínculo entre treino e dados agora é outro: snapshot_id identifica o snapshot exato usado para gerar uma instância treinada. SeriesKey continua identificando a série; snapshot_id identifica o dataset.

O que não volta para o domínio

O histórico de demanda não é mais representado como aggregate rico no domínio. Ele entra, é validado e circula como frame canônico via control/data_preparation e infra/data_preparation/spark.

Como isso prepara o feature catalog

Com o feature catalog atual:

  • o control plane guarda definição, versão, lineage e vínculo de features com ModelDefinition;
  • FeatureRecipe contém chamadas configuradas a definições FeatureStep canônicas, que validam o payload contra o schema tipado do próprio step;
  • FeatureEngineeringCatalog resolve essas chamadas para executores concretos do adapter;
  • Polars e Spark registram os mesmos step keys (target.lags.v1, target.rolling.v1, target.ewm.v1, calendar.date_parts.v1, calendar.fourier.v1, calendar.br_holidays.v1);
  • o adapter de data-preparation lê a base canônica, projeta as chaves mínimas do schema feature_catalog_base e materializa o frame enriquecido de treino;
  • as estratégias continuam recebendo um feature_frame canônico na borda final.

Regra prática para novas abstrações

  • não criar port para helper interno;
  • criar port quando houver boundary real de runtime ou integração externa;
  • manter schemas nomeados em control/data_preparation/frame_schemas.py;
  • manter regras de ciclo de vida e governança no control plane.

Onde continuar