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.
Files changed (128) hide show
  1. {baucli-1.0.152 → baucli-1.0.154}/PKG-INFO +1 -1
  2. {baucli-1.0.152 → baucli-1.0.154}/baucli/__init__.py +1 -1
  3. {baucli-1.0.152 → baucli-1.0.154}/baucli/cli.py +2 -1
  4. {baucli-1.0.152 → baucli-1.0.154}/baucli/mcp_server.py +19 -2
  5. {baucli-1.0.152 → baucli-1.0.154}/baucli/skills.py +90 -2
  6. {baucli-1.0.152 → baucli-1.0.154}/baucli/sync.py +118 -10
  7. {baucli-1.0.152 → baucli-1.0.154}/baucli.egg-info/PKG-INFO +1 -1
  8. {baucli-1.0.152 → baucli-1.0.154}/baucli.egg-info/SOURCES.txt +3 -0
  9. baucli-1.0.154/tests/test_mcp_conventions.py +87 -0
  10. baucli-1.0.154/tests/test_skills_security.py +79 -0
  11. baucli-1.0.154/tests/test_sync_prefix_msg.py +72 -0
  12. {baucli-1.0.152 → baucli-1.0.154}/MANIFEST.in +0 -0
  13. {baucli-1.0.152 → baucli-1.0.154}/README.md +0 -0
  14. {baucli-1.0.152 → baucli-1.0.154}/baucli/apex_rebuild.py +0 -0
  15. {baucli-1.0.152 → baucli-1.0.154}/baucli/apexlang.py +0 -0
  16. {baucli-1.0.152 → baucli-1.0.154}/baucli/apexlang_profile.json +0 -0
  17. {baucli-1.0.152 → baucli-1.0.154}/baucli/apexlint.py +0 -0
  18. {baucli-1.0.152 → baucli-1.0.154}/baucli/api.py +0 -0
  19. {baucli-1.0.152 → baucli-1.0.154}/baucli/canon.py +0 -0
  20. {baucli-1.0.152 → baucli-1.0.154}/baucli/config.py +0 -0
  21. {baucli-1.0.152 → baucli-1.0.154}/baucli/db/__init__.py +0 -0
  22. {baucli-1.0.152 → baucli-1.0.154}/baucli/db/mssql.py +0 -0
  23. {baucli-1.0.152 → baucli-1.0.154}/baucli/db/mysql.py +0 -0
  24. {baucli-1.0.152 → baucli-1.0.154}/baucli/db/oracle.py +0 -0
  25. {baucli-1.0.152 → baucli-1.0.154}/baucli/db/postgresql.py +0 -0
  26. {baucli-1.0.152 → baucli-1.0.154}/baucli/db_check_pdf.py +0 -0
  27. {baucli-1.0.152 → baucli-1.0.154}/baucli/db_check_report.py +0 -0
  28. {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm.py +0 -0
  29. {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm_sql/mssql/crm_data_mssql.sql +0 -0
  30. {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm_sql/mssql/crm_demo_mssql.sql +0 -0
  31. {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm_sql/mysql/crm_data_mysql.sql +0 -0
  32. {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm_sql/mysql/crm_demo_mysql.sql +0 -0
  33. {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm_sql/oracle/crm_demo_oracle.sql +0 -0
  34. {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm_sql/postgresql/crm_data_postgresql.sql +0 -0
  35. {baucli-1.0.152 → baucli-1.0.154}/baucli/democrm_sql/postgresql/crm_demo_postgresql.sql +0 -0
  36. {baucli-1.0.152 → baucli-1.0.154}/baucli/diffenvs.py +0 -0
  37. {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig.py +0 -0
  38. {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig_baseline.py +0 -0
  39. {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig_build.py +0 -0
  40. {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig_install.py +0 -0
  41. {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig_shell.py +0 -0
  42. {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig_spec_schema.py +0 -0
  43. {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig_spec_tools.py +0 -0
  44. {baucli-1.0.152 → baucli-1.0.154}/baucli/docmig_spec_workspace.py +0 -0
  45. {baucli-1.0.152 → baucli-1.0.154}/baucli/git.py +0 -0
  46. {baucli-1.0.152 → baucli-1.0.154}/baucli/legacy/__init__.py +0 -0
  47. {baucli-1.0.152 → baucli-1.0.154}/baucli/legacy/delphi.py +0 -0
  48. {baucli-1.0.152 → baucli-1.0.154}/baucli/legacy/delphi_canon.py +0 -0
  49. {baucli-1.0.152 → baucli-1.0.154}/baucli/legacy/delphi_evidence.py +0 -0
  50. {baucli-1.0.152 → baucli-1.0.154}/baucli/legacy/magic.py +0 -0
  51. {baucli-1.0.152 → baucli-1.0.154}/baucli/project.py +0 -0
  52. {baucli-1.0.152 → baucli-1.0.154}/baucli/rag_embed.py +0 -0
  53. {baucli-1.0.152 → baucli-1.0.154}/baucli/sample_scan.py +0 -0
  54. {baucli-1.0.152 → baucli-1.0.154}/baucli/scope_scan.py +0 -0
  55. {baucli-1.0.152 → baucli-1.0.154}/baucli/screenshot.py +0 -0
  56. {baucli-1.0.152 → baucli-1.0.154}/baucli/seed_check.py +0 -0
  57. {baucli-1.0.152 → baucli-1.0.154}/baucli/stress.py +0 -0
  58. {baucli-1.0.152 → baucli-1.0.154}/baucli/templates/app_shell/.apex/apexlang.json +0 -0
  59. {baucli-1.0.152 → baucli-1.0.154}/baucli/templates/app_shell/pages/login.apx.tpl +0 -0
  60. {baucli-1.0.152 → baucli-1.0.154}/baucli/templates/app_shell/shared-components/lists/navigation-bar.apx +0 -0
  61. {baucli-1.0.152 → baucli-1.0.154}/baucli/templates/app_shell/shared-components/themes/universal-theme/theme.apx +0 -0
  62. {baucli-1.0.152 → baucli-1.0.154}/baucli/templates/logo-bau.jpg +0 -0
  63. {baucli-1.0.152 → baucli-1.0.154}/baucli/uikit.py +0 -0
  64. {baucli-1.0.152 → baucli-1.0.154}/baucli/unwrap.py +0 -0
  65. {baucli-1.0.152 → baucli-1.0.154}/baucli.egg-info/dependency_links.txt +0 -0
  66. {baucli-1.0.152 → baucli-1.0.154}/baucli.egg-info/entry_points.txt +0 -0
  67. {baucli-1.0.152 → baucli-1.0.154}/baucli.egg-info/requires.txt +0 -0
  68. {baucli-1.0.152 → baucli-1.0.154}/baucli.egg-info/top_level.txt +0 -0
  69. {baucli-1.0.152 → baucli-1.0.154}/pyproject.toml +0 -0
  70. {baucli-1.0.152 → baucli-1.0.154}/setup.cfg +0 -0
  71. {baucli-1.0.152 → baucli-1.0.154}/setup.py +0 -0
  72. {baucli-1.0.152 → baucli-1.0.154}/tests/test_apexlint_binario.py +0 -0
  73. {baucli-1.0.152 → baucli-1.0.154}/tests/test_apexlint_dialogo.py +0 -0
  74. {baucli-1.0.152 → baucli-1.0.154}/tests/test_apexlint_dominio.py +0 -0
  75. {baucli-1.0.152 → baucli-1.0.154}/tests/test_api_request_db.py +0 -0
  76. {baucli-1.0.152 → baucli-1.0.154}/tests/test_canon_anchored_markers.py +0 -0
  77. {baucli-1.0.152 → baucli-1.0.154}/tests/test_canon_multiengine.py +0 -0
  78. {baucli-1.0.152 → baucli-1.0.154}/tests/test_db_check_pdf.py +0 -0
  79. {baucli-1.0.152 → baucli-1.0.154}/tests/test_db_check_report.py +0 -0
  80. {baucli-1.0.152 → baucli-1.0.154}/tests/test_db_tenant_guard.py +0 -0
  81. {baucli-1.0.152 → baucli-1.0.154}/tests/test_delphi_canon.py +0 -0
  82. {baucli-1.0.152 → baucli-1.0.154}/tests/test_delphi_datasnap.py +0 -0
  83. {baucli-1.0.152 → baucli-1.0.154}/tests/test_delphi_evidence.py +0 -0
  84. {baucli-1.0.152 → baucli-1.0.154}/tests/test_delphi_fmx.py +0 -0
  85. {baucli-1.0.152 → baucli-1.0.154}/tests/test_delta_clock.py +0 -0
  86. {baucli-1.0.152 → baucli-1.0.154}/tests/test_deploy_alias.py +0 -0
  87. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_baseline.py +0 -0
  88. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_build.py +0 -0
  89. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_build_extend.py +0 -0
  90. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_build_new.py +0 -0
  91. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_expressividade.py +0 -0
  92. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_install.py +0 -0
  93. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_install_extend.py +0 -0
  94. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_install_shell.py +0 -0
  95. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_project.py +0 -0
  96. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_project_new.py +0 -0
  97. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_shell.py +0 -0
  98. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_spec_analista.py +0 -0
  99. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_spec_schema.py +0 -0
  100. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_spec_schema_11.py +0 -0
  101. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_spec_schema_app.py +0 -0
  102. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_spec_use_baseline.py +0 -0
  103. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_sqlcl_padrao.py +0 -0
  104. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docmig_support_objects.py +0 -0
  105. {baucli-1.0.152 → baucli-1.0.154}/tests/test_docrev_dismiss.py +0 -0
  106. {baucli-1.0.152 → baucli-1.0.154}/tests/test_doctab_split.py +0 -0
  107. {baucli-1.0.152 → baucli-1.0.154}/tests/test_documentation_list_db_label.py +0 -0
  108. {baucli-1.0.152 → baucli-1.0.154}/tests/test_mcp_doc_changeset.py +0 -0
  109. {baucli-1.0.152 → baucli-1.0.154}/tests/test_mcp_doc_tags.py +0 -0
  110. {baucli-1.0.152 → baucli-1.0.154}/tests/test_mcp_doctab_text.py +0 -0
  111. {baucli-1.0.152 → baucli-1.0.154}/tests/test_mcp_schema_context.py +0 -0
  112. {baucli-1.0.152 → baucli-1.0.154}/tests/test_packaging_deps.py +0 -0
  113. {baucli-1.0.152 → baucli-1.0.154}/tests/test_post_apply_uikit.py +0 -0
  114. {baucli-1.0.152 → baucli-1.0.154}/tests/test_profiles_add_prompt.py +0 -0
  115. {baucli-1.0.152 → baucli-1.0.154}/tests/test_project_guarda_chuva.py +0 -0
  116. {baucli-1.0.152 → baucli-1.0.154}/tests/test_scope_scan_call_wrapper.py +0 -0
  117. {baucli-1.0.152 → baucli-1.0.154}/tests/test_scope_scan_datasnap_reach.py +0 -0
  118. {baucli-1.0.152 → baucli-1.0.154}/tests/test_scope_scan_from_depth.py +0 -0
  119. {baucli-1.0.152 → baucli-1.0.154}/tests/test_scope_scan_owner.py +0 -0
  120. {baucli-1.0.152 → baucli-1.0.154}/tests/test_scope_scan_pkg_func_from.py +0 -0
  121. {baucli-1.0.152 → baucli-1.0.154}/tests/test_skills_apex_domain.py +0 -0
  122. {baucli-1.0.152 → baucli-1.0.154}/tests/test_skills_lessons.py +0 -0
  123. {baucli-1.0.152 → baucli-1.0.154}/tests/test_skills_size_budget.py +0 -0
  124. {baucli-1.0.152 → baucli-1.0.154}/tests/test_spec_sem_beg.py +0 -0
  125. {baucli-1.0.152 → baucli-1.0.154}/tests/test_sync_413.py +0 -0
  126. {baucli-1.0.152 → baucli-1.0.154}/tests/test_sync_charset.py +0 -0
  127. {baucli-1.0.152 → baucli-1.0.154}/tests/test_table_status.py +0 -0
  128. {baucli-1.0.152 → baucli-1.0.154}/tests/test_uikit_scope.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: baucli
3
- Version: 1.0.152
3
+ Version: 1.0.154
4
4
  Summary: Extracts verified business-logic and schema context from Oracle, PostgreSQL, MySQL and SQL Server
5
5
  Home-page: https://www.begcloud.com
6
6
  Author: BEG Support
@@ -3,6 +3,6 @@
3
3
  import warnings
4
4
  warnings.filterwarnings("ignore", message="urllib3 v2 only supports OpenSSL")
5
5
 
6
- __version__ = "1.0.152"
6
+ __version__ = "1.0.154"
7
7
  __product__ = "BAUCLI"
8
8
  __author__ = "BEG Support"
@@ -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
- return ctx().api.conventions(s)
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')}{tag}")
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) -> dict:
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
- if extractor.exclude_prefixes:
1043
- console.print(f" [dim]prefixos excluídos (BAU): "
1044
- f"{extractor.exclude_prefixes}[/dim]")
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
- console.print(f" Scope: [cyan]{cnt_obj}[/cyan] objects + [cyan]{cnt_tbl}[/cyan] tables/views")
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
- if cnt_obj >= 0: parts.append(f"[cyan]{cnt_obj}[/cyan] objects")
1230
- if cnt_tbl >= 0: parts.append(f"[cyan]{cnt_tbl}[/cyan] tables/views")
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: baucli
3
- Version: 1.0.152
3
+ Version: 1.0.154
4
4
  Summary: Extracts verified business-logic and schema context from Oracle, PostgreSQL, MySQL and SQL Server
5
5
  Home-page: https://www.begcloud.com
6
6
  Author: BEG Support
@@ -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