baucli 1.0.152__tar.gz → 1.0.154__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {baucli-1.0.152 → baucli-1.0.154}/PKG-INFO +1 -1
- {baucli-1.0.152 → baucli-1.0.154}/baucli/__init__.py +1 -1
- {baucli-1.0.152 → baucli-1.0.154}/baucli/cli.py +2 -1
- {baucli-1.0.152 → baucli-1.0.154}/baucli/mcp_server.py +19 -2
- {baucli-1.0.152 → baucli-1.0.154}/baucli/skills.py +90 -2
- {baucli-1.0.152 → baucli-1.0.154}/baucli/sync.py +118 -10
- {baucli-1.0.152 → baucli-1.0.154}/baucli.egg-info/PKG-INFO +1 -1
- {baucli-1.0.152 → baucli-1.0.154}/baucli.egg-info/SOURCES.txt +3 -0
- baucli-1.0.154/tests/test_mcp_conventions.py +87 -0
- baucli-1.0.154/tests/test_skills_security.py +79 -0
- baucli-1.0.154/tests/test_sync_prefix_msg.py +72 -0
- {baucli-1.0.152 → baucli-1.0.154}/MANIFEST.in +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/README.md +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/apex_rebuild.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/apexlang.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/apexlang_profile.json +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/apexlint.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/api.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/canon.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/config.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/db/__init__.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/db/mssql.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/db/mysql.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/db/oracle.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/db/postgresql.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/db_check_pdf.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/db_check_report.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm_sql/mssql/crm_data_mssql.sql +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm_sql/mssql/crm_demo_mssql.sql +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm_sql/mysql/crm_data_mysql.sql +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm_sql/mysql/crm_demo_mysql.sql +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm_sql/oracle/crm_demo_oracle.sql +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm_sql/postgresql/crm_data_postgresql.sql +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm_sql/postgresql/crm_demo_postgresql.sql +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/diffenvs.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig_baseline.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig_build.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig_install.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig_shell.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig_spec_schema.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig_spec_tools.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig_spec_workspace.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/git.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/legacy/__init__.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/legacy/delphi.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/legacy/delphi_canon.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/legacy/delphi_evidence.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/legacy/magic.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/project.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/rag_embed.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/sample_scan.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/scope_scan.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/screenshot.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/seed_check.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/stress.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/templates/app_shell/.apex/apexlang.json +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/templates/app_shell/pages/login.apx.tpl +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/templates/app_shell/shared-components/lists/navigation-bar.apx +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/templates/app_shell/shared-components/themes/universal-theme/theme.apx +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/templates/logo-bau.jpg +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/uikit.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli/unwrap.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli.egg-info/dependency_links.txt +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli.egg-info/entry_points.txt +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli.egg-info/requires.txt +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/baucli.egg-info/top_level.txt +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/pyproject.toml +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/setup.cfg +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/setup.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_apexlint_binario.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_apexlint_dialogo.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_apexlint_dominio.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_api_request_db.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_canon_anchored_markers.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_canon_multiengine.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_db_check_pdf.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_db_check_report.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_db_tenant_guard.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_delphi_canon.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_delphi_datasnap.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_delphi_evidence.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_delphi_fmx.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_delta_clock.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_deploy_alias.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_baseline.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_build.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_build_extend.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_build_new.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_expressividade.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_install.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_install_extend.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_install_shell.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_project.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_project_new.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_shell.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_spec_analista.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_spec_schema.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_spec_schema_11.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_spec_schema_app.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_spec_use_baseline.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_sqlcl_padrao.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_support_objects.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_docrev_dismiss.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_doctab_split.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_documentation_list_db_label.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_mcp_doc_changeset.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_mcp_doc_tags.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_mcp_doctab_text.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_mcp_schema_context.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_packaging_deps.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_post_apply_uikit.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_profiles_add_prompt.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_project_guarda_chuva.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_scope_scan_call_wrapper.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_scope_scan_datasnap_reach.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_scope_scan_from_depth.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_scope_scan_owner.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_scope_scan_pkg_func_from.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_skills_apex_domain.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_skills_lessons.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_skills_size_budget.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_spec_sem_beg.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_sync_413.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_sync_charset.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_table_status.py +0 -0
- {baucli-1.0.152 → baucli-1.0.154}/tests/test_uikit_scope.py +0 -0
|
@@ -8843,7 +8843,8 @@ def project_skills(schema, force):
|
|
|
8843
8843
|
raise RuntimeError((lr or {}).get("error") or "payload invalido")
|
|
8844
8844
|
res2 = write_edit_safety_skill(
|
|
8845
8845
|
proj.root, lr, target,
|
|
8846
|
-
datetime.now().strftime("%Y-%m-%d %H:%M"), force=force
|
|
8846
|
+
datetime.now().strftime("%Y-%m-%d %H:%M"), force=force,
|
|
8847
|
+
guidelines_payload=guidelines_payload or None)
|
|
8847
8848
|
console.print(f"[green]Skill gerado em[/green] "
|
|
8848
8849
|
f"{res2['dir'].relative_to(proj.root)}")
|
|
8849
8850
|
console.print(f" Rotinas CAUTION/UNSAFE no schema: "
|
|
@@ -1552,12 +1552,29 @@ def build_server():
|
|
|
1552
1552
|
usadas, coluna de flag ativo + valores válidos, amostras de nomenclatura
|
|
1553
1553
|
de FK, domínios semânticos mais comuns, prefixos e o AI_CONTEXT do schema
|
|
1554
1554
|
(padrão de forms/reports). Use para gerar tabela/coluna/tela que 'parece'
|
|
1555
|
-
o resto do schema.
|
|
1555
|
+
o resto do schema. Traz também `security_guidelines`: a régua de
|
|
1556
|
+
segurança VIGENTE (OWASP Top 10:2025 — código, regra, referência,
|
|
1557
|
+
escopo) que o DOCREV cobra; leia antes de SQL dinâmico, acesso a
|
|
1558
|
+
dado, chamada HTTP, senha/token ou tratamento de erro."""
|
|
1556
1559
|
try:
|
|
1557
1560
|
s = ctx().schema(schema)
|
|
1558
|
-
|
|
1561
|
+
out = ctx().api.conventions(s)
|
|
1559
1562
|
except (BauApiError, ValueError) as e:
|
|
1560
1563
|
return _err(e)
|
|
1564
|
+
if not isinstance(out, dict):
|
|
1565
|
+
return out
|
|
1566
|
+
out = dict(out)
|
|
1567
|
+
# Best-effort: a régua de segurança nunca derruba as convenções.
|
|
1568
|
+
try:
|
|
1569
|
+
from .skills import security_guidelines
|
|
1570
|
+
gl = ctx().api.code_guidelines(schema=s)
|
|
1571
|
+
if not isinstance(gl, dict) or gl.get("error"):
|
|
1572
|
+
raise RuntimeError((gl or {}).get("error") or "payload inválido")
|
|
1573
|
+
out["security_guidelines"] = security_guidelines(gl)
|
|
1574
|
+
except Exception as e: # noqa: BLE001
|
|
1575
|
+
out["security_guidelines"] = []
|
|
1576
|
+
out["security_guidelines_error"] = str(e)
|
|
1577
|
+
return out
|
|
1561
1578
|
|
|
1562
1579
|
@mcp.tool()
|
|
1563
1580
|
def bau_object(object_name: str, schema: str = "", parent: str = "",
|
|
@@ -140,11 +140,34 @@ def _fmt_guidelines(guidelines: list) -> str:
|
|
|
140
140
|
for g in sorted(by_cat[cat], key=lambda x: x.get("code") or ""):
|
|
141
141
|
scope = g.get("scope") or "GLOBAL"
|
|
142
142
|
tag = "" if scope == "GLOBAL" else f" _(regra da casa: {scope})_"
|
|
143
|
-
out.append(f"- **`{g.get('code')}`** — {g.get('guideline')}
|
|
143
|
+
out.append(f"- **`{g.get('code')}`** — {g.get('guideline')}"
|
|
144
|
+
f"{_ref_tag(g)}{tag}")
|
|
144
145
|
out.append("")
|
|
145
146
|
return "\n".join(out)
|
|
146
147
|
|
|
147
148
|
|
|
149
|
+
def _ref_tag(g: dict) -> str:
|
|
150
|
+
"""Referencia externa da regra (ex.: "OWASP A05:2025 Injeção") como
|
|
151
|
+
sufixo -- vazio quando ausente ou quando o texto da regra ja a cita."""
|
|
152
|
+
ref = (g.get("reference") or "").strip()
|
|
153
|
+
if not ref or ref in (g.get("guideline") or ""):
|
|
154
|
+
return ""
|
|
155
|
+
return f" _[{ref}]_"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def security_guidelines(payload: dict | None) -> list[dict]:
|
|
159
|
+
"""Regua de seguranca do schema: as diretrizes de categoria SECURITY do
|
|
160
|
+
payload de `GET /code-guidelines` (mutes/overrides do tenant ja resolvidos
|
|
161
|
+
no servidor), ordenadas por codigo. Usado pelo MCP (`bau_conventions`) e
|
|
162
|
+
pela skill bau-edit-safety (`resolved/security.md`)."""
|
|
163
|
+
items = (payload or {}).get("guidelines") or []
|
|
164
|
+
sec = [g for g in items if (g.get("category") or "").upper() == "SECURITY"]
|
|
165
|
+
sec.sort(key=lambda g: g.get("code") or "")
|
|
166
|
+
return [{"code": g.get("code"), "guideline": g.get("guideline"),
|
|
167
|
+
"reference": g.get("reference"), "scope": g.get("scope")}
|
|
168
|
+
for g in sec]
|
|
169
|
+
|
|
170
|
+
|
|
148
171
|
def build_guidelines_md(payload: dict, schema: str | None,
|
|
149
172
|
generated_at: str) -> str:
|
|
150
173
|
guidelines = payload.get("guidelines") or []
|
|
@@ -551,6 +574,19 @@ estar em UTC e o BAUCTX em -3h).
|
|
|
551
574
|
mesmo dominio (`ORA-SIZ-01`, passo "orcamento de tamanho" da skill
|
|
552
575
|
`bau-plsql-header`). O package que ja passou do limite nunca cresce.
|
|
553
576
|
|
|
577
|
+
## Segurança (OWASP Top 10:2025)
|
|
578
|
+
|
|
579
|
+
Antes de mexer em SQL dinâmico, acesso a dado, chamada HTTP, senha/token ou
|
|
580
|
+
tratamento de erro, leia `resolved/security.md`: as regras de segurança VIGENTES
|
|
581
|
+
deste schema (mesma régua do DOCREV; ao vivo: `bau_code_guidelines` ou
|
|
582
|
+
`bau_conventions().security_guidelines`). Mínimo que vale sempre:
|
|
583
|
+
- Valor em SQL dinâmico vai por USING, sem exceção por tipo (ORA-INJ-01);
|
|
584
|
+
identificador, por DBMS_ASSERT na função da posição. NOOP não valida nada.
|
|
585
|
+
- Nada de segredo em literal (ORA-CRY-01); nada de MD5/SHA-1 para senha (ORA-CRY-02).
|
|
586
|
+
- WHEN OTHERS em autorização/autenticação nega, nunca permite (ORA-EXC-02).
|
|
587
|
+
Violação vira achado DOCREV (p133 / `bau_code_findings`). Hoje é alerta (WARN);
|
|
588
|
+
nada bloqueia a liberação.
|
|
589
|
+
|
|
554
590
|
## Regras de edicao (cicatrizes de campo -- todas ja custaram um deploy)
|
|
555
591
|
|
|
556
592
|
- **Fonte VIVO antes de tudo:** nunca edite a partir do script versionado sem
|
|
@@ -595,6 +631,7 @@ estar em UTC e o BAUCTX em -3h).
|
|
|
595
631
|
- Confiar em VALID como prova de deploy aplicado.
|
|
596
632
|
- Editar entre marcadores BAU:HEADER.
|
|
597
633
|
- Deployar corpo de package com QUALQUER coisa depois do `/` no arquivo.
|
|
634
|
+
- Concatenar valor em SQL dinâmico (use USING).
|
|
598
635
|
""" + LESSONS_CROSSREF_MD
|
|
599
636
|
|
|
600
637
|
|
|
@@ -644,12 +681,58 @@ Atualize com `baucli project skills{f' --schema {schema}' if schema else ''}`.
|
|
|
644
681
|
"""
|
|
645
682
|
|
|
646
683
|
|
|
684
|
+
def build_security_md(guidelines_payload: dict | None, schema: str | None,
|
|
685
|
+
generated_at: str) -> str:
|
|
686
|
+
"""resolved/security.md: a regua de seguranca VIGENTE do schema (so a
|
|
687
|
+
categoria SECURITY). Sem payload (falha best-effort no chamador), o
|
|
688
|
+
arquivo existe e diz que a regua nao veio -- nunca fica uma copia velha."""
|
|
689
|
+
sec = security_guidelines(guidelines_payload)
|
|
690
|
+
upd = f"baucli project skills{f' --schema {schema}' if schema else ''}"
|
|
691
|
+
if not (guidelines_payload or {}).get("guidelines"):
|
|
692
|
+
body = ("_A régua de segurança não veio do BAU nesta geração "
|
|
693
|
+
"(falha ou payload vazio em `GET /code-guidelines`)._\n\n"
|
|
694
|
+
"Consulte ao vivo com `bau_code_guidelines` (categoria "
|
|
695
|
+
"SECURITY) ou `bau_conventions().security_guidelines` antes "
|
|
696
|
+
f"de editar, e re-rode `{upd}`.\n")
|
|
697
|
+
elif not sec:
|
|
698
|
+
body = ("_Nenhuma diretriz de segurança ativa para este schema "
|
|
699
|
+
"(todas mutadas pelo tenant?)._\n\nConfira ao vivo com "
|
|
700
|
+
"`bau_code_guidelines`.\n")
|
|
701
|
+
else:
|
|
702
|
+
lines = []
|
|
703
|
+
for g in sec:
|
|
704
|
+
scope = g.get("scope") or "GLOBAL"
|
|
705
|
+
tag = "" if scope == "GLOBAL" else f" _(regra da casa: {scope})_"
|
|
706
|
+
lines.append(f"- **`{g.get('code')}`** — {g.get('guideline')}"
|
|
707
|
+
f"{_ref_tag(g)}{tag}")
|
|
708
|
+
body = "\n".join(lines) + "\n"
|
|
709
|
+
return f"""{GENERATED_BANNER}
|
|
710
|
+
# Regua de seguranca (OWASP Top 10:2025) -- {schema or '(nivel tenant)'}
|
|
711
|
+
|
|
712
|
+
As diretrizes de categoria SECURITY VIGENTES para este schema (seeds da
|
|
713
|
+
plataforma + regras da casa, mutes/overrides do tenant ja resolvidos). Mesma
|
|
714
|
+
regua do DOCREV: violacao vira achado em `bau_code_findings`. Ao vivo:
|
|
715
|
+
`bau_code_guidelines` ou `bau_conventions().security_guidelines`.
|
|
716
|
+
|
|
717
|
+
Total: **{len(sec)}** diretriz(es). Gerado em {generated_at}.
|
|
718
|
+
|
|
719
|
+
{body}
|
|
720
|
+
---
|
|
721
|
+
Atualize com `{upd}`.
|
|
722
|
+
"""
|
|
723
|
+
|
|
724
|
+
|
|
647
725
|
def write_edit_safety_skill(root: Path, payload: dict, schema: str | None,
|
|
648
|
-
generated_at: str, force: bool = False
|
|
726
|
+
generated_at: str, force: bool = False,
|
|
727
|
+
guidelines_payload: dict | None = None) -> dict:
|
|
649
728
|
"""Grava o skill em <root>/.claude/skills/bau-edit-safety/.
|
|
650
729
|
|
|
651
730
|
Mesmo contrato do write_skill: SKILL.md (metodo) so quando ausente ou com
|
|
652
731
|
force=True; resolved/ (dado) sempre reescrito.
|
|
732
|
+
|
|
733
|
+
guidelines_payload: o mesmo `GET /code-guidelines` que o write_skill
|
|
734
|
+
recebe; dele sai resolved/security.md (so a categoria SECURITY). None
|
|
735
|
+
grava o arquivo avisando que a regua nao veio, sem quebrar o skill.
|
|
653
736
|
"""
|
|
654
737
|
base = root / EDIT_SAFETY_SKILL_DIR
|
|
655
738
|
resolved = base / "resolved"
|
|
@@ -674,6 +757,11 @@ def write_edit_safety_skill(root: Path, payload: dict, schema: str | None,
|
|
|
674
757
|
encoding="utf-8")
|
|
675
758
|
result["resolved_written"].append(rt)
|
|
676
759
|
|
|
760
|
+
sec = resolved / "security.md"
|
|
761
|
+
sec.write_text(build_security_md(guidelines_payload, schema, generated_at),
|
|
762
|
+
encoding="utf-8")
|
|
763
|
+
result["resolved_written"].append(sec)
|
|
764
|
+
|
|
677
765
|
return result
|
|
678
766
|
|
|
679
767
|
|
|
@@ -48,6 +48,105 @@ def _nome_do_item(obj) -> str:
|
|
|
48
48
|
return f"{nome} ({tipo})" if tipo else str(nome)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def _prefix_tokens(csv: str) -> tuple[list[str], list[str]]:
|
|
52
|
+
"""Tokens de um CSV de prefixos (BAU_SCHEMAS.EXCLUDE_PREFIXES /
|
|
53
|
+
INCLUDE_PREFIXES): (únicos, na ordem em que aparecem; repetidos)."""
|
|
54
|
+
seen: list[str] = []
|
|
55
|
+
dups: list[str] = []
|
|
56
|
+
for tok in (csv or "").split(","):
|
|
57
|
+
tok = tok.strip()
|
|
58
|
+
if not tok:
|
|
59
|
+
continue
|
|
60
|
+
if tok.upper() in {t.upper() for t in seen}:
|
|
61
|
+
if tok.upper() not in {t.upper() for t in dups}:
|
|
62
|
+
dups.append(tok)
|
|
63
|
+
else:
|
|
64
|
+
seen.append(tok)
|
|
65
|
+
return seen, dups
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _em_colunas(tokens: list[str], ncol: int = 3, indent: str = " ") -> list[str]:
|
|
69
|
+
"""Tokens em N colunas de largura igual, uma linha por fileira: 20 prefixos
|
|
70
|
+
numa linha só quebravam no meio do terminal de 80 colunas."""
|
|
71
|
+
if not tokens:
|
|
72
|
+
return []
|
|
73
|
+
w = max(len(t) for t in tokens)
|
|
74
|
+
linhas = []
|
|
75
|
+
for i in range(0, len(tokens), ncol):
|
|
76
|
+
fila = tokens[i:i + ncol]
|
|
77
|
+
linhas.append("[dim]" + indent
|
|
78
|
+
+ " ".join(t.ljust(w) for t in fila).rstrip() + "[/dim]")
|
|
79
|
+
return linhas
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def prefix_filter_lines(schema: str, exclude_csv: str, include_csv: str) -> list[str]:
|
|
83
|
+
"""Mensagem do filtro de prefixos que o servidor empurra para o sync.
|
|
84
|
+
|
|
85
|
+
A versão antiga despejava o CSV cru numa linha só: com 27 tokens (caso
|
|
86
|
+
COMPIERE) ele quebrava no meio, repetia prefixo e escondia dois fatos que
|
|
87
|
+
custam uma rodada de suporte: de onde o filtro vem (cadastro do schema no
|
|
88
|
+
App 601, não do bau.toml) e que ele é SÓ de PREFIXO -- tanto aqui
|
|
89
|
+
(`_prefix_clause`, LIKE 'TOK%') quanto no servidor
|
|
90
|
+
(FNC_BAU_NAME_MATCHES_PREFIX_CSV, instr = 1). Um token como `_BKP` não
|
|
91
|
+
casa nome nenhum; quem quer tirar sufixo ou trecho usa regra de objeto.
|
|
92
|
+
"""
|
|
93
|
+
out: list[str] = []
|
|
94
|
+
excl, excl_dups = _prefix_tokens(exclude_csv)
|
|
95
|
+
incl, incl_dups = _prefix_tokens(include_csv)
|
|
96
|
+
if not excl and not incl:
|
|
97
|
+
return out
|
|
98
|
+
origem = f" [dim]origem: cadastro do schema {schema} no BAU (App 601 p28)[/dim]"
|
|
99
|
+
inertes = [t for t in excl + incl if not t[0].isalnum()]
|
|
100
|
+
excl_ok = [t for t in excl if t[0].isalnum()]
|
|
101
|
+
incl_ok = [t for t in incl if t[0].isalnum()]
|
|
102
|
+
if excl_ok:
|
|
103
|
+
out.append(f" [dim]Filtro de nomes: {len(excl_ok)} prefixo(s) excluído(s)[/dim]")
|
|
104
|
+
out.extend(_em_colunas(sorted(excl_ok, key=str.upper)))
|
|
105
|
+
if incl_ok:
|
|
106
|
+
out.append(f" [dim]Filtro de nomes: APENAS {len(incl_ok)} prefixo(s) "
|
|
107
|
+
f"(allow-list)[/dim]")
|
|
108
|
+
out.extend(_em_colunas(sorted(incl_ok, key=str.upper)))
|
|
109
|
+
out.append(origem)
|
|
110
|
+
dups = excl_dups + incl_dups
|
|
111
|
+
if dups:
|
|
112
|
+
out.append(" [dim] repetido(s) no cadastro, contado(s) uma vez: "
|
|
113
|
+
+ ", ".join(dups) + "[/dim]")
|
|
114
|
+
if inertes:
|
|
115
|
+
out.append(" [yellow] ⚠ sem efeito: " + " ".join(inertes)
|
|
116
|
+
+ " — o filtro é só de PREFIXO (nome que COMEÇA pelo token); "
|
|
117
|
+
"para sufixo ou trecho do nome use regra de objeto "
|
|
118
|
+
"(App 601 > Regras de Objetos).[/yellow]")
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _fora_pelo_filtro(extractor, schema: str, kind: str) -> int:
|
|
123
|
+
"""Quantos objetos/tabelas o filtro de prefixos tirou do escopo: conta sem
|
|
124
|
+
o filtro e subtrai o `count_*` filtrado. -1 = não dá para saber (engine sem
|
|
125
|
+
count ou falha no dicionário); o chamador omite o parêntese."""
|
|
126
|
+
if not (getattr(extractor, "exclude_prefixes", "") or
|
|
127
|
+
getattr(extractor, "include_prefixes", "")):
|
|
128
|
+
return -1
|
|
129
|
+
fn = getattr(extractor, f"count_{kind}", None)
|
|
130
|
+
if fn is None:
|
|
131
|
+
return -1
|
|
132
|
+
ex, inc = extractor.exclude_prefixes, extractor.include_prefixes
|
|
133
|
+
try:
|
|
134
|
+
com = fn(schema)
|
|
135
|
+
extractor.exclude_prefixes = extractor.include_prefixes = ""
|
|
136
|
+
try:
|
|
137
|
+
sem = fn(schema)
|
|
138
|
+
finally:
|
|
139
|
+
extractor.exclude_prefixes, extractor.include_prefixes = ex, inc
|
|
140
|
+
return max(sem - com, 0)
|
|
141
|
+
except Exception:
|
|
142
|
+
extractor.exclude_prefixes, extractor.include_prefixes = ex, inc
|
|
143
|
+
return -1
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _fora_txt(n: int) -> str:
|
|
147
|
+
return f" [dim](+{n} fora pelo filtro de prefixos)[/dim]" if n > 0 else ""
|
|
148
|
+
|
|
149
|
+
|
|
51
150
|
def _somar_resultados(acc: dict, novo: dict) -> dict:
|
|
52
151
|
"""Junta os contadores de dois retornos do /sync/batch."""
|
|
53
152
|
for k, v in (novo or {}).items():
|
|
@@ -1039,12 +1138,9 @@ def sync_database(db_name: str, db_config: dict, api: BauApi,
|
|
|
1039
1138
|
db_config["include_prefixes"] = _incl
|
|
1040
1139
|
extractor.exclude_prefixes = _excl
|
|
1041
1140
|
extractor.include_prefixes = _incl
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
if extractor.include_prefixes:
|
|
1046
|
-
console.print(f" [dim]APENAS prefixos (BAU): "
|
|
1047
|
-
f"{extractor.include_prefixes}[/dim]")
|
|
1141
|
+
for _line in prefix_filter_lines(schema, extractor.exclude_prefixes,
|
|
1142
|
+
extractor.include_prefixes):
|
|
1143
|
+
console.print(_line)
|
|
1048
1144
|
|
|
1049
1145
|
# === Step 2: Extract ===
|
|
1050
1146
|
objects = []
|
|
@@ -1102,7 +1198,12 @@ def sync_database(db_name: str, db_config: dict, api: BauApi,
|
|
|
1102
1198
|
else len(eff_table_name) if isinstance(eff_table_name, list)
|
|
1103
1199
|
else extractor.count_tables(schema))
|
|
1104
1200
|
if cnt_obj >= 0 and cnt_tbl >= 0:
|
|
1105
|
-
|
|
1201
|
+
_fora = -1
|
|
1202
|
+
if not object_name and not table_name and not isinstance(eff_table_name, list):
|
|
1203
|
+
_fora = _fora_pelo_filtro(extractor, schema, "objects")
|
|
1204
|
+
_ft = _fora_pelo_filtro(extractor, schema, "tables")
|
|
1205
|
+
_fora = (max(_fora, 0) + max(_ft, 0)) if (_fora >= 0 or _ft >= 0) else -1
|
|
1206
|
+
console.print(f" Scope: [cyan]{cnt_obj}[/cyan] objects + [cyan]{cnt_tbl}[/cyan] tables/views{_fora_txt(_fora)}")
|
|
1106
1207
|
except Exception:
|
|
1107
1208
|
pass
|
|
1108
1209
|
|
|
@@ -1226,10 +1327,17 @@ def sync_database(db_name: str, db_config: dict, api: BauApi,
|
|
|
1226
1327
|
else len(eff_table_name) if isinstance(eff_table_name, list)
|
|
1227
1328
|
else extractor.count_tables(schema))
|
|
1228
1329
|
parts = []
|
|
1229
|
-
|
|
1230
|
-
if
|
|
1330
|
+
_fora = 0
|
|
1331
|
+
if cnt_obj >= 0:
|
|
1332
|
+
parts.append(f"[cyan]{cnt_obj}[/cyan] objects")
|
|
1333
|
+
if not object_name and not isinstance(eff_object_name, list):
|
|
1334
|
+
_fora += max(_fora_pelo_filtro(extractor, schema, "objects"), 0)
|
|
1335
|
+
if cnt_tbl >= 0:
|
|
1336
|
+
parts.append(f"[cyan]{cnt_tbl}[/cyan] tables/views")
|
|
1337
|
+
if not table_name and not isinstance(eff_table_name, list):
|
|
1338
|
+
_fora += max(_fora_pelo_filtro(extractor, schema, "tables"), 0)
|
|
1231
1339
|
if parts:
|
|
1232
|
-
console.print(f" Scope: {' + '.join(parts)}")
|
|
1340
|
+
console.print(f" Scope: {' + '.join(parts)}{_fora_txt(_fora)}")
|
|
1233
1341
|
except Exception:
|
|
1234
1342
|
pass
|
|
1235
1343
|
|
|
@@ -100,6 +100,7 @@ tests/test_docmig_support_objects.py
|
|
|
100
100
|
tests/test_docrev_dismiss.py
|
|
101
101
|
tests/test_doctab_split.py
|
|
102
102
|
tests/test_documentation_list_db_label.py
|
|
103
|
+
tests/test_mcp_conventions.py
|
|
103
104
|
tests/test_mcp_doc_changeset.py
|
|
104
105
|
tests/test_mcp_doc_tags.py
|
|
105
106
|
tests/test_mcp_doctab_text.py
|
|
@@ -115,9 +116,11 @@ tests/test_scope_scan_owner.py
|
|
|
115
116
|
tests/test_scope_scan_pkg_func_from.py
|
|
116
117
|
tests/test_skills_apex_domain.py
|
|
117
118
|
tests/test_skills_lessons.py
|
|
119
|
+
tests/test_skills_security.py
|
|
118
120
|
tests/test_skills_size_budget.py
|
|
119
121
|
tests/test_spec_sem_beg.py
|
|
120
122
|
tests/test_sync_413.py
|
|
121
123
|
tests/test_sync_charset.py
|
|
124
|
+
tests/test_sync_prefix_msg.py
|
|
122
125
|
tests/test_table_status.py
|
|
123
126
|
tests/test_uikit_scope.py
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""bau_conventions acrescenta a regua de seguranca (categoria SECURITY do
|
|
2
|
+
GET code-guidelines) ao dict do servidor -- best-effort: erro na regua nao
|
|
3
|
+
derruba a tool."""
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from baucli import mcp_server as M
|
|
10
|
+
from baucli.api import BauApiError
|
|
11
|
+
|
|
12
|
+
PAYLOAD = {"guidelines": [
|
|
13
|
+
{"code": "ORA-SET-01", "category": "PERFORMANCE", "guideline": "x"},
|
|
14
|
+
{"code": "ORA-INJ-01", "category": "SECURITY",
|
|
15
|
+
"guideline": "y [OWASP A05:2025 Injeção]",
|
|
16
|
+
"reference": "OWASP A05:2025 Injeção", "scope": "GLOBAL"},
|
|
17
|
+
{"code": "ORA-ACL-01", "category": "SECURITY", "guideline": "z",
|
|
18
|
+
"reference": None},
|
|
19
|
+
]}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _Api:
|
|
23
|
+
def __init__(self, gl_exc=None):
|
|
24
|
+
self.gl_exc = gl_exc
|
|
25
|
+
self.calls = []
|
|
26
|
+
|
|
27
|
+
def conventions(self, schema):
|
|
28
|
+
self.calls.append(("conventions", schema))
|
|
29
|
+
return {"schema": schema, "pk_style": "surrogate:ID_<TABLE>"}
|
|
30
|
+
|
|
31
|
+
def code_guidelines(self, schema=None):
|
|
32
|
+
self.calls.append(("code_guidelines", schema))
|
|
33
|
+
if self.gl_exc:
|
|
34
|
+
raise self.gl_exc
|
|
35
|
+
return PAYLOAD
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _Ctx:
|
|
39
|
+
def __init__(self, api):
|
|
40
|
+
self.api = api
|
|
41
|
+
|
|
42
|
+
def schema(self, override=""):
|
|
43
|
+
return (override or "S").upper()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def use(monkeypatch):
|
|
48
|
+
def _use(api):
|
|
49
|
+
monkeypatch.setattr(M, "_ctx", _Ctx(api))
|
|
50
|
+
return api
|
|
51
|
+
return _use
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _call(name, **args):
|
|
55
|
+
res = asyncio.run(M.build_server().call_tool(name, args))
|
|
56
|
+
if isinstance(res, tuple):
|
|
57
|
+
res = res[1] if isinstance(res[1], dict) else res[0]
|
|
58
|
+
if isinstance(res, dict):
|
|
59
|
+
return res.get("result", res)
|
|
60
|
+
return json.loads(res[0].text)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_bau_conventions_acrescenta_security(use):
|
|
64
|
+
api = use(_Api())
|
|
65
|
+
out = _call("bau_conventions")
|
|
66
|
+
assert out["schema"] == "S"
|
|
67
|
+
assert out["pk_style"] == "surrogate:ID_<TABLE>"
|
|
68
|
+
assert [g["code"] for g in out["security_guidelines"]] == ["ORA-ACL-01", "ORA-INJ-01"]
|
|
69
|
+
assert "security_guidelines_error" not in out
|
|
70
|
+
assert ("code_guidelines", "S") in api.calls
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_bau_conventions_sobrevive_erro_guidelines(use):
|
|
74
|
+
use(_Api(gl_exc=BauApiError("boom", 500)))
|
|
75
|
+
out = _call("bau_conventions")
|
|
76
|
+
assert out["schema"] == "S"
|
|
77
|
+
assert out["pk_style"] == "surrogate:ID_<TABLE>"
|
|
78
|
+
assert out["security_guidelines"] == []
|
|
79
|
+
assert "boom" in out["security_guidelines_error"]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_bau_conventions_payload_com_error(use, monkeypatch):
|
|
83
|
+
api = use(_Api())
|
|
84
|
+
monkeypatch.setattr(api, "code_guidelines", lambda schema=None: {"error": "sem acesso"})
|
|
85
|
+
out = _call("bau_conventions")
|
|
86
|
+
assert out["security_guidelines"] == []
|
|
87
|
+
assert "sem acesso" in out["security_guidelines_error"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Regua de seguranca OWASP Top 10:2025 nas skills geradas: security_guidelines
|
|
2
|
+
filtra a categoria SECURITY do GET code-guidelines, guidelines.md mostra a
|
|
3
|
+
referencia OWASP e a skill bau-edit-safety ganha resolved/security.md."""
|
|
4
|
+
from baucli import skills
|
|
5
|
+
from baucli.skills import (EDIT_SAFETY_SKILL_MD, _fmt_guidelines,
|
|
6
|
+
security_guidelines, write_edit_safety_skill)
|
|
7
|
+
|
|
8
|
+
PAYLOAD = {"guidelines": [
|
|
9
|
+
{"code": "ORA-SET-01", "category": "PERFORMANCE", "guideline": "x"},
|
|
10
|
+
{"code": "ORA-INJ-01", "category": "SECURITY",
|
|
11
|
+
"guideline": "y [OWASP A05:2025 Injeção]",
|
|
12
|
+
"reference": "OWASP A05:2025 Injeção", "scope": "GLOBAL"},
|
|
13
|
+
{"code": "ORA-ACL-01", "category": "SECURITY", "guideline": "z",
|
|
14
|
+
"reference": None},
|
|
15
|
+
]}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_security_guidelines_filtra_e_ordena():
|
|
19
|
+
out = security_guidelines(PAYLOAD)
|
|
20
|
+
assert [g["code"] for g in out] == ["ORA-ACL-01", "ORA-INJ-01"]
|
|
21
|
+
assert set(out[0]) == {"code", "guideline", "reference", "scope"}
|
|
22
|
+
assert out[1]["reference"] == "OWASP A05:2025 Injeção"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_security_guidelines_payload_vazio():
|
|
26
|
+
assert security_guidelines({}) == []
|
|
27
|
+
assert security_guidelines(None) == []
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_fmt_guidelines_mostra_referencia():
|
|
31
|
+
md = _fmt_guidelines(PAYLOAD["guidelines"])
|
|
32
|
+
inj = next(ln for ln in md.splitlines() if "ORA-INJ-01" in ln)
|
|
33
|
+
acl = next(ln for ln in md.splitlines() if "ORA-ACL-01" in ln)
|
|
34
|
+
assert "OWASP A05:2025 Injeção" in inj
|
|
35
|
+
assert "None" not in acl
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_edit_safety_md_tem_secao_seguranca():
|
|
39
|
+
md = EDIT_SAFETY_SKILL_MD
|
|
40
|
+
assert "## Segurança (OWASP Top 10:2025)" in md
|
|
41
|
+
assert "resolved/security.md" in md
|
|
42
|
+
assert "bau_code_guidelines" in md
|
|
43
|
+
assert "ORA-INJ-01" in md
|
|
44
|
+
assert "USING" in md
|
|
45
|
+
assert md.index("## Segurança (OWASP Top 10:2025)") < md.index("## Regras de edicao")
|
|
46
|
+
nunca = md[md.index("## NUNCA"):]
|
|
47
|
+
assert "Concatenar valor em SQL dinâmico (use USING)." in nunca
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_write_edit_safety_grava_security_md(tmp_path):
|
|
51
|
+
res = write_edit_safety_skill(tmp_path, {"routines": []}, "S",
|
|
52
|
+
"2026-10-22 10:00",
|
|
53
|
+
guidelines_payload=PAYLOAD)
|
|
54
|
+
sec = tmp_path / skills.EDIT_SAFETY_SKILL_DIR / "resolved" / "security.md"
|
|
55
|
+
assert sec in res["resolved_written"]
|
|
56
|
+
txt = sec.read_text(encoding="utf-8")
|
|
57
|
+
assert "ORA-INJ-01" in txt and "ORA-ACL-01" in txt
|
|
58
|
+
assert "ORA-SET-01" not in txt
|
|
59
|
+
assert "OWASP A05:2025 Injeção" in txt
|
|
60
|
+
assert "None" not in txt
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_write_edit_safety_sem_payload_nao_quebra(tmp_path):
|
|
64
|
+
write_edit_safety_skill(tmp_path, {"routines": []}, "S", "2026-10-22 10:00")
|
|
65
|
+
sec = tmp_path / skills.EDIT_SAFETY_SKILL_DIR / "resolved" / "security.md"
|
|
66
|
+
assert sec.exists()
|
|
67
|
+
txt = sec.read_text(encoding="utf-8")
|
|
68
|
+
assert "não veio" in txt
|
|
69
|
+
assert "bau_code_guidelines" in txt
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_fmt_guidelines_acrescenta_referencia_ausente_do_texto():
|
|
73
|
+
md = _fmt_guidelines([{"code": "ORA-CRY-01", "category": "SECURITY",
|
|
74
|
+
"guideline": "Nada de segredo em literal.",
|
|
75
|
+
"reference": "OWASP A04:2025 Falhas criptográficas"}])
|
|
76
|
+
assert "OWASP A04:2025 Falhas criptográficas" in md
|
|
77
|
+
# nao duplica quando o texto ja cita a referencia
|
|
78
|
+
md2 = _fmt_guidelines(PAYLOAD["guidelines"])
|
|
79
|
+
assert md2.count("OWASP A05:2025 Injeção") == 1
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Mensagem do filtro de prefixos do sync (BAU_SCHEMAS.EXCLUDE/INCLUDE_PREFIXES)."""
|
|
2
|
+
from baucli.sync import _fora_pelo_filtro, _prefix_tokens, prefix_filter_lines
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_sem_filtro_nao_imprime_nada():
|
|
6
|
+
assert prefix_filter_lines("X", "", "") == []
|
|
7
|
+
assert prefix_filter_lines("X", None, None) == []
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_tokens_dedup_ignora_caixa_e_espacos():
|
|
11
|
+
toks, dups = _prefix_tokens(" AD_, c_ ,AD_,,C_")
|
|
12
|
+
assert toks == ["AD_", "c_"]
|
|
13
|
+
assert dups == ["AD_", "C_"] # C_ repete c_ (caixa ignorada); um por token
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_lista_ordenada_com_contagem_e_origem():
|
|
17
|
+
out = "\n".join(prefix_filter_lines("COMPIERE", "M_,AD_,C_", ""))
|
|
18
|
+
assert "3 prefixo(s) excluído(s)" in out
|
|
19
|
+
assert "schema COMPIERE" in out and "App 601" in out
|
|
20
|
+
assert "AD_ C_ M_" in out # 3 por linha, colunas alinhadas
|
|
21
|
+
assert "⚠" not in out
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_token_de_sufixo_vira_aviso_e_sai_da_lista():
|
|
25
|
+
out = prefix_filter_lines("S", "BKP_,_BKP,_OLD,BKP_", "")
|
|
26
|
+
txt = "\n".join(out)
|
|
27
|
+
assert "1 prefixo(s) excluído(s)" in txt # so BKP_ conta
|
|
28
|
+
assert "sem efeito: _BKP _OLD" in txt
|
|
29
|
+
assert "regra de objeto" in txt
|
|
30
|
+
assert "repetido(s) no cadastro" in txt and "BKP_" in txt
|
|
31
|
+
assert not any(" BKP_ " in l and "_BKP" in l for l in out)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_allow_list_tem_rotulo_proprio():
|
|
35
|
+
txt = "\n".join(prefix_filter_lines("S", "", "NAC_,BEG_"))
|
|
36
|
+
assert "APENAS 2 prefixo(s)" in txt and "BEG_ NAC_" in txt
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_lista_em_tres_colunas_alinhadas():
|
|
40
|
+
from baucli.sync import _em_colunas
|
|
41
|
+
linhas = _em_colunas(["AD_", "A_", "BKP_", "B_", "C_", "FACT_", "GL_"])
|
|
42
|
+
assert len(linhas) == 3 # 3 + 3 + 1
|
|
43
|
+
assert linhas[0] == "[dim] AD_ A_ BKP_[/dim]"
|
|
44
|
+
assert linhas[2] == "[dim] GL_[/dim]" # ultima fileira sem espaco sobrando
|
|
45
|
+
assert _em_colunas([]) == []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _Ext:
|
|
49
|
+
def __init__(self, com, sem):
|
|
50
|
+
self.exclude_prefixes = "AD_"
|
|
51
|
+
self.include_prefixes = ""
|
|
52
|
+
self._com, self._sem = com, sem
|
|
53
|
+
self.calls = []
|
|
54
|
+
|
|
55
|
+
def count_tables(self, schema):
|
|
56
|
+
self.calls.append((self.exclude_prefixes, self.include_prefixes))
|
|
57
|
+
return self._com if self.exclude_prefixes else self._sem
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_fora_pelo_filtro_conta_sem_o_filtro_e_restaura():
|
|
61
|
+
e = _Ext(com=3272, sem=3894)
|
|
62
|
+
assert _fora_pelo_filtro(e, "COMPIERE", "tables") == 622
|
|
63
|
+
assert e.exclude_prefixes == "AD_"
|
|
64
|
+
assert e.calls == [("AD_", ""), ("", "")]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_fora_pelo_filtro_sem_filtro_ou_sem_count_devolve_menos_um():
|
|
68
|
+
e = _Ext(1, 1)
|
|
69
|
+
e.exclude_prefixes = ""
|
|
70
|
+
assert _fora_pelo_filtro(e, "S", "tables") == -1
|
|
71
|
+
e.exclude_prefixes = "AD_"
|
|
72
|
+
assert _fora_pelo_filtro(e, "S", "objects") == -1 # sem count_objects
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|