mirror of
https://github.com/mcloh/decision-models-benchmark.git
synced 2026-11-05 22:04:12 +00:00
Proteger dados sensíveis e atualizar andamento do plano
- Caminho do compartimento pai e CIDR do SSH saem do código para .secrets/oci-settings.json (fora do Git), com override por DMB_*. - Relatórios de sanidade deixam de gravar o hostname local. - tests/test_sensitive.py falha se OCIDs, chaves, IPs públicos ou valores de .secrets aparecerem em arquivos versionados. - tools/plan_progress.py gera o bloco de andamento por fase no plano. - Plano: status reais das tarefas de F0, F3 e F6; README e docs/05 com GPU em sa-saopaulo-1, runtime CUDA do Rizzo e pipeline sintético. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
13
README.md
13
README.md
@@ -24,6 +24,12 @@ O resultado é um relatório técnico com uma recomendação. Todos os resultado
|
||||
|
||||
Cada candidato é exposto por um adaptador local com o mesmo contrato JEV (`POST /v1/systemone`). Assim, o *harness* não sabe qual modelo está respondendo.
|
||||
|
||||
## Andamento
|
||||
|
||||
O andamento por fase fica no topo de [docs/03-plano-de-tarefas.md](docs/03-plano-de-tarefas.md), regenerado com `python tools/plan_progress.py` a cada atualização do plano.
|
||||
|
||||
**Primeiro ciclo:** classificação de intenções (D1) no atendimento de telefonia móvel, com dados 100% sintéticos anotados por três modelos generativos. CPU em us-chicago-1 (sub-rede privada) e GPU A10 em sa-saopaulo-1.
|
||||
|
||||
## Documentação
|
||||
|
||||
| Documento | Conteúdo |
|
||||
@@ -32,6 +38,7 @@ Cada candidato é exposto por um adaptador local com o mesmo contrato JEV (`POST
|
||||
| [docs/02-desenho-experimental.md](docs/02-desenho-experimental.md) | Tarefas de decisão, formato dos dados, partições, métricas e regras de isolamento |
|
||||
| [docs/03-plano-de-tarefas.md](docs/03-plano-de-tarefas.md) | As 78 microtarefas organizadas em fases, com dependências e adaptações ao contexto de roteamento |
|
||||
| [docs/04-questoes-em-aberto.md](docs/04-questoes-em-aberto.md) | Decisões de projeto (Q1–Q13) e os mecanismos automatizados que as aplicam |
|
||||
| [docs/06-guia-de-anotacao.md](docs/06-guia-de-anotacao.md) | Guia de anotação, usado literalmente pelos anotadores |
|
||||
| [docs/05-ambiente-de-execucao.md](docs/05-ambiente-de-execucao.md) | Onde e como os scripts rodam: jobs e notebooks do OCI Data Science (a máquina local é só IDE) |
|
||||
| [config/benchmark.yaml](config/benchmark.yaml) | Parâmetros e critérios pré-registrados, congelados na tarefa 62 |
|
||||
|
||||
@@ -46,12 +53,14 @@ infra/ds/ Jobs do Data Science: build dos ambientes, execução do ben
|
||||
infra/vm/ remote.sh para a VM de trabalho (bloqueada; ver docs/05)
|
||||
env/ Aquisição de artefatos, manifesto, Dockerfile por candidato e dependências fixadas (11–16)
|
||||
contracts/jev/ Contrato JEV jev-compat-v1: esquemas, mapeamento de tipos, fixtures de conformidade (36–41)
|
||||
data/taxonomy/ Taxonomia de intenções/agentes e definição das tarefas de decisão (20–23)
|
||||
data/taxonomy/ Taxonomia de intenções (intents-v1.yaml: 42 intenções, 13 domínios) (20–23)
|
||||
datagen/ Pipeline sintético: geração, mascaramento/deduplicação, anotação por 3 LLMs, partições (24–36)
|
||||
data/raw/ Enunciados coletados e anonimizados — não versionar dados sensíveis (24–25)
|
||||
data/annotated/ Anotações, adjudicação e referência (29–32)
|
||||
data/splits/ Partições dev / calibração / teste congeladas (33–35)
|
||||
adapters/<modelo>/ Adaptadores JEV locais por candidato (42–51)
|
||||
harness/ Execução, controle de seeds/lotes/truncamento, coleta de medições (52–66)
|
||||
harness/ Execução nos jobs (job.py), sanidade, servidor, auditoria do bucket (18–19, 41, 51–66)
|
||||
tools/ Utilitários do repositório (andamento do plano)
|
||||
analysis/ Métricas, cortes, análise de erros e gráficos (67–74)
|
||||
reports/ Relatório técnico e anexos de reprodução (75–76)
|
||||
```
|
||||
|
||||
@@ -19,13 +19,34 @@ flowchart LR
|
||||
|
||||
As fases F0–F1 (infra) e F2–F3 (dados) podem andar em paralelo.
|
||||
|
||||
<!-- PROGRESSO:INICIO -->
|
||||
**Andamento em 2026-10-07:** 40 de 78 tarefas concluídas (51%), 8 em andamento.
|
||||
|
||||
| Fase | Concluídas | Em andamento | Pendentes |
|
||||
|------|-----------|--------------|-----------|
|
||||
| F0 — Infraestrutura OCI | 6 | 2 | 2 |
|
||||
| F1 — Ambiente e artefatos | 9 | 0 | 0 |
|
||||
| F2 — Tarefas de decisão e taxonomia | 4 | 0 | 0 |
|
||||
| F3 — Dados e anotação | 5 | 6 | 1 |
|
||||
| F4 — Contrato JEV e adaptadores | 16 | 0 | 0 |
|
||||
| F5 — Controle de execução | 0 | 0 | 3 |
|
||||
| F6 — Piloto | 0 | 0 | 4 |
|
||||
| F7 — Desenvolvimento e calibração | 0 | 0 | 4 |
|
||||
| F8 — Teste final | 0 | 0 | 4 |
|
||||
| F9 — Métricas e análise | 0 | 0 | 8 |
|
||||
| F10 — Relatório e encerramento | 0 | 0 | 4 |
|
||||
| **Total** | **40** | **8** | **30** |
|
||||
|
||||
Legenda: `[x]` concluída · `[~]` em andamento ou parcial · `[ ]` pendente.
|
||||
<!-- PROGRESSO:FIM -->
|
||||
|
||||
---
|
||||
|
||||
## F0 — Infraestrutura OCI
|
||||
|
||||
- [x] **1.** Criar um compartment OCI dedicado ao benchmark.
|
||||
- ↳ `root/<compartimento-pai>/decision-models`. Recursos em `us-chicago-1`. OCIDs em `.secrets/artefatos-OCI.md` (fora do Git).
|
||||
- [ ] **2.** Aplicar as tags `projeto=benchmark-decisoes-ptbr`, `ambiente=benchmark` e `centro_custo=<definir>` aos recursos do projeto. *(Q13)*
|
||||
- ↳ `<compartimento-pai>/decision-models` (caminho em `.secrets/oci-settings.json`). Recursos em `us-chicago-1`. OCIDs em `.secrets/artefatos-OCI.md` (fora do Git).
|
||||
- [~] **2.** Aplicar as tags `projeto=benchmark-decisoes-ptbr`, `ambiente=benchmark` e `centro_custo=<definir>` aos recursos do projeto. *(Q13)*
|
||||
- ↳ Parcial: `projeto` e `ambiente` aplicadas como tags livres por `infra/oci/provision.py`. Falta `centro_custo`.
|
||||
- [ ] **3.** Criar um orçamento para o compartment.
|
||||
- ↳ **Em aberto por decisão (2026-10-07):** o uso está coberto por cota interna de engenharia. Não será criado no primeiro ciclo.
|
||||
@@ -33,7 +54,7 @@ As fases F0–F1 (infra) e F2–F3 (dados) podem andar em paralelo.
|
||||
- ↳ Em aberto, porque depende da tarefa 3.
|
||||
- [x] **5.** Criar um projeto no OCI Data Science.
|
||||
- ↳ `benchmark-decisoes-ptbr`.
|
||||
- [ ] **6.** Criar buckets no Object Storage para `entrada`, `artefatos`, `resultados` e `logs-imutaveis`.
|
||||
- [~] **6.** Criar buckets no Object Storage para `entrada`, `artefatos`, `resultados` e `logs-imutaveis`.
|
||||
- ↳ Criados com o prefixo `dmb-` (nomes de bucket são únicos no namespace), privados e com versionamento.
|
||||
- ↳ Pendente: a regra de retenção (WORM) de `dmb-logs-imutaveis`, que depende do prazo de retenção (tarefa 77).
|
||||
- [x] **7.** Definir políticas IAM de menor privilégio para leitura dos dados, escrita dos resultados e execução dos jobs.
|
||||
@@ -42,8 +63,9 @@ As fases F0–F1 (infra) e F2–F3 (dados) podem andar em paralelo.
|
||||
- ↳ `dmb-jobs-menor-privilegio`: os jobs (`request.principal.type='datasciencejob'`) leem `dmb-entrada`/`dmb-artefatos`, escrevem em `dmb-resultados`/`dmb-logs-imutaveis`, usam logs e repos. Inclui a política de serviço do Data Science para a VCN.
|
||||
- [x] **8.** Definir uma VCN privada para os jobs, com acesso ao Object Storage por Service Gateway.
|
||||
- ↳ `dmb-vcn` 10.20.0.0/16, sub-rede privada `dmb-subnet-jobs`. A rota e a security list só permitem tráfego para serviços OCI pelo Service Gateway.
|
||||
- [ ] **9.** Criar uma rota temporária de saída por NAT Gateway, só para a aquisição inicial de dependências e pesos.
|
||||
- [x] **9.** Criar uma rota temporária de saída por NAT Gateway, só para a aquisição inicial de dependências e pesos.
|
||||
- ↳ Remover a rota logo após a tarefa 16 e passar pelo gate de egresso antes de qualquer execução. *(Q3)*
|
||||
- ↳ **Não foi necessário:** a aquisição (tarefas 13–16) rodou fora da VCN e enviou os artefatos direto ao bucket; os ambientes são montados por jobs de build com rede gerenciada. `infra/oci/nat.py` fica disponível.
|
||||
- ↳ Criar e ativar **só quando necessário**, com `infra/oci/nat.py on`. Para desligar: `nat.py off`, que fecha a rota e bloqueia o NAT. Na tarefa 78: `nat.py off --delete`.
|
||||
- [x] **10.** Validar que os jobs acessam os buckets sem internet pública.
|
||||
- ↳ Validado em 2026-10-07 com jobs do Data Science em `dmb-subnet-jobs`. Os pesos do Rizzo (4,4 GB) desceram pela Service Gateway em 28 s, com SHA-256 conferido.
|
||||
@@ -85,26 +107,29 @@ As fases F0–F1 (infra) e F2–F3 (dados) podem andar em paralelo.
|
||||
|
||||
## F3 — Dados e anotação
|
||||
|
||||
- [ ] **24.** Coletar textos PT-BR representativos do domínio de destino. *(Q7, Q15)*
|
||||
- ↳ **100% sintéticos** (`datagen.generate`, na VM).
|
||||
- [x] **24.** Coletar textos PT-BR representativos do domínio de destino. *(Q7, Q15)*
|
||||
- ↳ **100% sintéticos** (`datagen.generate`, na VM): 4.068 enunciados em 339 lotes (42 intenções × 7 perfis + 5 categorias fora de escopo × 9 lotes), geradores `llama-4-maverick` e `command-a` alternados.
|
||||
- ↳ A unidade é a **conversa com vários turnos**, não o enunciado isolado. Cada turno guarda o estado (agente ativo e sessões suspensas).
|
||||
- [ ] **25.** Remover ou mascarar dados pessoais, segredos e identificadores que o benchmark não precisa.
|
||||
- [ ] **26.** Separar os textos por domínio, comprimento, grau de ambiguidade e classe esperada.
|
||||
- [x] **25.** Remover ou mascarar dados pessoais, segredos e identificadores que o benchmark não precisa.
|
||||
- [~] **26.** Separar os textos por domínio, comprimento, grau de ambiguidade e classe esperada.
|
||||
- ↳ Acrescentar a posição do turno e a presença de agente ativo ou de sessões suspensas.
|
||||
- [ ] **27.** Criar exemplos adversariais com abreviações, erros de ortografia, regionalismos, textos curtos e várias intenções.
|
||||
- ↳ Primeiro ciclo (só D1): metadados `domain`, `length_bucket`, `ambiguity`, `profile`, `sentiment`, `kind` e `n_options` gravados por `datagen.build`.
|
||||
- [x] **27.** Criar exemplos adversariais com abreviações, erros de ortografia, regionalismos, textos curtos e várias intenções.
|
||||
- ↳ Acrescentar respostas curtas que dependem do contexto ("sim", "o segundo"), troca de assunto, retorno a um assunto anterior e pedidos fora de escopo.
|
||||
- [ ] **28.** Definir os critérios de inclusão e exclusão dos exemplos.
|
||||
- ↳ Perfis de geração: informal/abreviado, erros de digitação, regionalismo, emocional, longo, indireto/múltiplo; fora de escopo em 5 categorias (inclui desabafo emocional sem pedido).
|
||||
- [x] **28.** Definir os critérios de inclusão e exclusão dos exemplos.
|
||||
- ↳ Exclui: duplicatas exatas e aproximadas, comprimento fora de 2–700 caracteres, três rótulos distintos na anotação e falha de anotação. Resultado do `prepare`: 4.057 de 4.068 mantidos (9 duplicatas exatas, 2 aproximadas; 4 trechos mascarados).
|
||||
- [x] **29.** Produzir o guia de anotação humana.
|
||||
- ↳ [06-guia-de-anotacao.md](06-guia-de-anotacao.md), usado literalmente pelos anotadores.
|
||||
- ↳ O guia cobre D1–D4 com exemplos-limite (por exemplo, quando um enunciado curto continua a tarefa do agente ativo e quando inicia um novo agente).
|
||||
- [ ] **30.** Anotar cada exemplo com dois avaliadores independentes.
|
||||
- [~] **30.** Anotar cada exemplo com dois avaliadores independentes.
|
||||
- ↳ Avaliadores = modelos generativos em rodízio (`openai.gpt-5.5`, `google.gemini-2.5-pro`, `xai.grok-4.3`). *(Q15)*
|
||||
- [ ] **31.** Resolver as discordâncias com a revisão de um terceiro avaliador.
|
||||
- [ ] **32.** Registrar, para cada exemplo, a resposta de referência, a justificativa e a versão da taxonomia.
|
||||
- [~] **31.** Resolver as discordâncias com a revisão de um terceiro avaliador.
|
||||
- [~] **32.** Registrar, para cada exemplo, a resposta de referência, a justificativa e a versão da taxonomia.
|
||||
- ↳ Reportar também a concordância entre anotadores.
|
||||
- [ ] **33.** Separar os dados em desenvolvimento, calibração e teste final por grupo de origem. *(Q12)*
|
||||
- [~] **33.** Separar os dados em desenvolvimento, calibração e teste final por grupo de origem. *(Q12)*
|
||||
- ↳ O grupo é `group_id`, ou seja, a conversa.
|
||||
- [ ] **34.** Impedir que textos derivados do mesmo caso apareçam em mais de uma partição.
|
||||
- [~] **34.** Impedir que textos derivados do mesmo caso apareçam em mais de uma partição.
|
||||
- [ ] **35.** Congelar o conjunto de teste final antes de qualquer ajuste de parâmetros.
|
||||
- ↳ Registrar o hash do arquivo congelado e guardar uma cópia em `logs-imutaveis`.
|
||||
|
||||
@@ -143,9 +168,10 @@ As fases F0–F1 (infra) e F2–F3 (dados) podem andar em paralelo.
|
||||
|
||||
## F6 — Piloto
|
||||
|
||||
- [ ] **55.** Executar um piloto de 50 exemplos por candidato em CPU. *(Q11)*
|
||||
- [ ] **55.** Executar um piloto de 50 exemplos por candidato em CPU.
|
||||
- ↳ Sanidade em CPU (us-chicago-1, sub-rede privada) aprovada nos 4 candidatos: Laya 125 ms, GLiNER 194 ms, Rizzo Flow 7,3 s, SemIf 16,4 s (latência mediana). *(Q11)*
|
||||
- [ ] **56.** Executar um piloto de 50 exemplos por candidato em GPU.
|
||||
- ↳ GPU em **sa-saopaulo-1, A10.1**, uma por vez *(Q14)*. Sanidade em GPU já em andamento.
|
||||
- ↳ GPU em **sa-saopaulo-1, A10.1**, uma por vez *(Q14)*. Sanidade em GPU: Laya 21 ms, GLiNER 16 ms, SemIf 66 ms (latência mediana). Rizzo Flow exigiu compilar o llama.cpp com CUDA para a glibc da imagem dos jobs (`infra/ds/build_llama_cuda.sh`).
|
||||
- [ ] **57.** Medir memória de GPU, memória RAM, tempo de carga, tempo de aquecimento e falhas do piloto.
|
||||
- [ ] **58.** Ajustar apenas os parâmetros operacionais necessários para eliminar falhas de execução.
|
||||
|
||||
|
||||
@@ -31,6 +31,33 @@ PYTHONPATH=infra/oci .venv/bin/python infra/ds/jobs.py run --name <nome> --entry
|
||||
--env DMB_CANDIDATE=laya DMB_MODE=sanity --wait
|
||||
```
|
||||
|
||||
## GPU (sa-saopaulo-1)
|
||||
|
||||
| Item | Valor |
|
||||
|------|-------|
|
||||
| Shape | `VM.GPU.A10.1`, **uma GPU por vez** (limite da região) — `infra/ds/gpu_queue.py` enfileira |
|
||||
| Dados | `dmb-artefatos-gru` (réplica server-side com `infra/oci/replicate.py`), resultados em `dmb-resultados-gru` |
|
||||
| Rede | Gerenciada (limite de VCNs da região esgotado). Isolamento no processo + autoteste `process_guard` (`DMB_ISOLATION=app`) |
|
||||
| Rizzo Flow em CUDA | O binário oficial do llama.cpp exige GLIBC 2.38; a imagem dos jobs tem 2.34. `infra/ds/build_llama_cuda.sh` compila o mesmo commit (`161755f`) com CUDA 12.8, sysroot glibc 2.17 e libstdc++ estática (maior GLIBC exigida: 2.27), publicado em `runtimes/rizzo_flow/cuda-latest.json` |
|
||||
|
||||
```bash
|
||||
PYTHONPATH=infra/oci:infra/ds .venv/bin/python infra/ds/gpu_queue.py sanity laya gliner semif rizzo_flow
|
||||
```
|
||||
|
||||
## Dados sintéticos (na VM)
|
||||
|
||||
```bash
|
||||
export DMB_OCI_AUTH=instance_principal DMB_COMPARTMENT_OCID=<ocid do compartment>
|
||||
python -m datagen.generate --out /data/runs/datagen-v1 # OCI Generative AI (us-chicago-1)
|
||||
python -m datagen.prepare --run /data/runs/datagen-v1
|
||||
python -m datagen.annotate --run /data/runs/datagen-v1
|
||||
python -m datagen.build --run /data/runs/datagen-v1 --freeze
|
||||
```
|
||||
|
||||
## Configuração local sensível
|
||||
|
||||
Fora do Git, em `.secrets/`: credenciais OCI, chaves SSH, o catálogo de artefatos com OCIDs (`artefatos-OCI.md`) e `oci-settings.json` (caminho do compartimento pai e CIDR autorizado para SSH; variáveis `DMB_PARENT_COMPARTMENT` e `DMB_SSH_CIDR` têm precedência). `tests/test_sensitive.py` falha se OCIDs, chaves, IPs públicos ou valores de `.secrets/oci-settings.json` aparecerem em arquivos versionados.
|
||||
|
||||
## VM de trabalho
|
||||
|
||||
| Item | Valor |
|
||||
|
||||
@@ -9,7 +9,6 @@ Uso: python -m harness.sanity --candidate laya --models-root models --device cpu
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import platform
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
@@ -76,7 +75,7 @@ def main():
|
||||
sanity.append(row)
|
||||
|
||||
report = {
|
||||
"candidate": adapter.describe(), "device": args.device, "host": platform.node(),
|
||||
"candidate": adapter.describe(), "device": args.device,
|
||||
"load_seconds": round(load_s, 2), **guard,
|
||||
"conformance": conformance, "sanity": sanity,
|
||||
"summary": {
|
||||
|
||||
@@ -13,16 +13,17 @@ from datetime import datetime, timezone
|
||||
|
||||
import oci
|
||||
|
||||
from session import CRED_DIR, REGION, load_config
|
||||
from session import CRED_DIR, REGION, load_config, settings
|
||||
|
||||
PARENT_PATH = "root/<compartimento-pai>"
|
||||
# Caminho do compartimento pai e CIDR do SSH ficam fora do Git: .secrets/oci-settings.json
|
||||
PARENT_PATH = settings()["parent_compartment_path"]
|
||||
COMPARTMENT_NAME = "decision-models"
|
||||
TAGS = {"projeto": "benchmark-decisoes-ptbr", "ambiente": "benchmark"} # centro_custo pendente (Q13)
|
||||
BUCKETS = ["entrada", "artefatos", "resultados", "logs-imutaveis"]
|
||||
VCN_CIDR = "10.20.0.0/16"
|
||||
SUBNET_CIDR = "10.20.1.0/24"
|
||||
DEV_SUBNET_CIDR = "10.20.2.0/24"
|
||||
SSH_ALLOWED_CIDR = os.environ.get("DMB_SSH_CIDR", "203.0.113.1/32")
|
||||
SSH_ALLOWED_CIDR = settings()["ssh_allowed_cidr"]
|
||||
# VM de trabalho criada pelo usuário no console (o lançamento via API é negado neste compartment).
|
||||
VM_NAME = "instance-20261007-1206"
|
||||
VM_SSH_KEY = ".secrets/ssh/ssh-key-oci.key"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Sessão OCI do benchmark a partir das credenciais em .secrets/oci-credentials/."""
|
||||
import configparser
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import oci
|
||||
@@ -9,6 +11,16 @@ CRED_DIR = ROOT / ".secrets" / "oci-credentials"
|
||||
REGION = "us-chicago-1"
|
||||
|
||||
|
||||
def settings() -> dict:
|
||||
"""Configuração local fora do Git (.secrets/oci-settings.json); variáveis DMB_* têm precedência."""
|
||||
path = CRED_DIR.parent / "oci-settings.json"
|
||||
data = json.loads(path.read_text()) if path.is_file() else {}
|
||||
return {
|
||||
"parent_compartment_path": os.environ.get("DMB_PARENT_COMPARTMENT", data.get("parent_compartment_path")),
|
||||
"ssh_allowed_cidr": os.environ.get("DMB_SSH_CIDR", data.get("ssh_allowed_cidr")),
|
||||
}
|
||||
|
||||
|
||||
def load_config(region: str = REGION, profile: str = "DEFAULT") -> dict:
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read(CRED_DIR / "profile.config")
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"probabilities_source": "native"
|
||||
},
|
||||
"device": "cpu",
|
||||
"host": "host-local",
|
||||
"load_seconds": 15.93,
|
||||
"conformance": [
|
||||
{
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"probabilities_source": "native"
|
||||
},
|
||||
"device": "cpu",
|
||||
"host": "host-local",
|
||||
"load_seconds": 8.4,
|
||||
"conformance": [
|
||||
{
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"probabilities_source": "native"
|
||||
},
|
||||
"device": "cpu",
|
||||
"host": "host-local",
|
||||
"load_seconds": 6.18,
|
||||
"conformance": [
|
||||
{
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"probabilities_source": "native"
|
||||
},
|
||||
"device": "cpu",
|
||||
"host": "host-local",
|
||||
"load_seconds": 6.55,
|
||||
"conformance": [
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ def test_external_connection_is_blocked():
|
||||
s = socket.socket()
|
||||
try:
|
||||
with pytest.raises(NetworkAccessBlocked):
|
||||
s.connect(("93.184.216.34", 443))
|
||||
s.connect(("203.0.113.10", 443)) # TEST-NET-3 (RFC 5737)
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
52
tests/test_sensitive.py
Normal file
52
tests/test_sensitive.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Nada sensível nos arquivos versionados: OCIDs, chaves, IPs públicos, namespace ou dados de .secrets/."""
|
||||
import ipaddress
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PATTERNS = [
|
||||
re.compile(r"ocid1\.[a-z0-9]+\.oc1\."),
|
||||
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
|
||||
re.compile(r"\bfingerprint\s*=\s*[0-9a-f:]{20,}"),
|
||||
]
|
||||
IPV4 = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
|
||||
|
||||
|
||||
def tracked_files():
|
||||
out = subprocess.run(["git", "-C", str(ROOT), "ls-files", "-co", "--exclude-standard"],
|
||||
capture_output=True, text=True, check=True).stdout.split()
|
||||
return [ROOT / f for f in out if (ROOT / f).is_file() and not f.startswith(".secrets/")]
|
||||
|
||||
|
||||
def local_secrets() -> list[str]:
|
||||
"""Valores de .secrets/oci-settings.json não podem aparecer em arquivos versionados."""
|
||||
path = ROOT / ".secrets" / "oci-settings.json"
|
||||
if not path.is_file():
|
||||
return []
|
||||
values = []
|
||||
for v in json.loads(path.read_text()).values():
|
||||
values += [v] + [part for part in str(v).replace("/", " ").split() if len(part) > 5 and part != "root"]
|
||||
return values
|
||||
|
||||
|
||||
def test_no_sensitive_content():
|
||||
secrets = local_secrets()
|
||||
problems = []
|
||||
for f in tracked_files():
|
||||
try:
|
||||
text = f.read_text()
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
rel = f.relative_to(ROOT)
|
||||
problems += [f"{rel}: {p.pattern}" for p in PATTERNS if p.search(text)]
|
||||
problems += [f"{rel}: valor de .secrets/oci-settings.json" for s in secrets if s and s in text]
|
||||
for ip in IPV4.findall(text):
|
||||
try:
|
||||
addr = ipaddress.ip_address(ip)
|
||||
except ValueError:
|
||||
continue
|
||||
if addr.is_global:
|
||||
problems.append(f"{rel}: IP público {ip}")
|
||||
assert not problems, "\n".join(problems)
|
||||
42
tools/plan_progress.py
Normal file
42
tools/plan_progress.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Atualiza o bloco de andamento de docs/03-plano-de-tarefas.md a partir das caixas de cada tarefa.
|
||||
|
||||
Legenda: [x] concluída · [~] em andamento/parcial · [ ] pendente.
|
||||
Uso: python tools/plan_progress.py
|
||||
"""
|
||||
import re
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
PLAN = Path(__file__).resolve().parents[1] / "docs" / "03-plano-de-tarefas.md"
|
||||
START, END = "<!-- PROGRESSO:INICIO -->", "<!-- PROGRESSO:FIM -->"
|
||||
|
||||
|
||||
def main():
|
||||
text = PLAN.read_text()
|
||||
phases, current = [], None
|
||||
for line in text.splitlines():
|
||||
if line.startswith("## F"):
|
||||
current = [line[3:].strip(), 0, 0, 0]
|
||||
phases.append(current)
|
||||
m = re.match(r"- \[(x| |~)\] \*\*\d+\.\*\*", line)
|
||||
if m and current:
|
||||
current[{"x": 1, "~": 2, " ": 3}[m.group(1)]] += 1
|
||||
rows = ["| Fase | Concluídas | Em andamento | Pendentes |", "|------|-----------|--------------|-----------|"]
|
||||
totals = [0, 0, 0]
|
||||
for name, done, doing, todo in phases:
|
||||
rows.append(f"| {name} | {done} | {doing} | {todo} |")
|
||||
totals = [totals[0] + done, totals[1] + doing, totals[2] + todo]
|
||||
rows.append(f"| **Total** | **{totals[0]}** | **{totals[1]}** | **{totals[2]}** |")
|
||||
block = (f"{START}\n**Andamento em {date.today():%Y-%m-%d}:** {totals[0]} de {sum(totals)} tarefas concluídas "
|
||||
f"({100 * totals[0] // sum(totals)}%), {totals[1]} em andamento.\n\n" + "\n".join(rows)
|
||||
+ "\n\nLegenda: `[x]` concluída · `[~]` em andamento ou parcial · `[ ]` pendente.\n" + END)
|
||||
if START in text:
|
||||
text = re.sub(re.escape(START) + ".*?" + re.escape(END), block, text, flags=re.S)
|
||||
else:
|
||||
text = text.replace("\n---\n", f"\n{block}\n\n---\n", 1)
|
||||
PLAN.write_text(text)
|
||||
print(block)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user