diff --git a/CLAUDE.md b/CLAUDE.md index a488dfe..1c7adda 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,3 +79,33 @@ guarded executor (path allowlist, company-context guard, schema/type/length guar ## Production deploy rule (Hub) Normal deploy = `powershell -ExecutionPolicy Bypass -File .\scripts\deploy_hub.ps1 [-PublishAgent -Version X]` (docs/DEPLOY_HUB.md). Do not recreate ad-hoc deploy scripts (deploy115.py etc.) and do not improvise SSH/docker against 10.0.10.117. The script handles VPN/SSH retry, backup, upload, migrations, rebuild, health, release publish, download verification, and automatic rollback; the real error is in `logs/last_deploy_result.json` and the log. + +## Permanent mission rules (fiscal completion missions, recorded 2026-09-28) + +- No AI/LLM/embeddings, no paid API, no scraping — every rule/decision is derived from real ERP + evidence or an explicit, audited operator/ADMIN decision (`FiscalContextEntry`), never inferred + or guessed. +- No Firebird triggers or schema changes, ever, for any discovery/completion mission. All queries + against any Firebird database (client, lab copy, or disposable test DB) are read-only unless the + exact path is on an explicit, mission-scoped write allowlist (see above) — writes still go only + through the guarded executor. +- Deploy only via `scripts\deploy_hub.ps1` (see "Production deploy rule" above) — never ad-hoc + SSH/docker/psql against 10.0.10.117. +- If the Agent (agent/) changes in a way that affects its runtime behavior: bump the single-source + version, build a new installer, validate it in a fresh Windows Sandbox (install, upgrade in-place, + identity preserved), and never overwrite an already-published release. A pure version-label sync + bump (release/VERSION, pyproject.toml files, the `AGENT_VERSION` default string) that changes no + Agent behavior is not itself an "Agent change" requiring a new installer. +- Never reenroll an existing installation (never mint a new `installation_id`/token for a machine + that already has one) — an in-place upgrade always preserves identity and local SQLite state. +- No remote-execution channel to any client's Agent host, ever (by design — see + `docs/rtc_auto_apply.md`). An Agent version upgrade on a real client machine is always performed + by a human operator on-site; the assistant never attempts RDP/WinRM/SSH/PowerShell-remoting into + a client host, and never asks for or accepts a production `AGENT_TOKEN`/admin session credential + pasted in chat to work around that — live validation of a real installation, when needed, comes + from the operator (a panel screenshot, or a value they read and paste themselves). +- No real client/production data (CNPJ, razão social, product names/descriptions, per-product NCM) + is ever published in a location a non-admin can reach (e.g. `central/static/`). Any evidence + artifact referenced from a UI card must either be served through an authenticated ADMIN-only + route, or be anonymized (generic client labels, no names/documents/descriptions) before being + published as a static file. diff --git a/central/app/context_keys.py b/central/app/context_keys.py index 5347472..be67c19 100644 --- a/central/app/context_keys.py +++ b/central/app/context_keys.py @@ -61,11 +61,13 @@ def validate_entry(entry): float(value.replace(",", ".")) except ValueError: return None, f"{key} must be numeric" - if key == "pis_cofins_profile": + if key == "pis_cofins_profile" and value.upper() not in ("NOT_APPLICABLE", "OPERATION_TIME"): try: prof = json.loads(value) except ValueError: - return None, "pis_cofins_profile must be JSON" + return None, "pis_cofins_profile must be JSON, or exactly NOT_APPLICABLE / OPERATION_TIME" if not isinstance(prof, dict) or not prof or set(prof) - PROFILE_KEYS: return None, f"pis_cofins_profile keys must be a non-empty subset of {sorted(PROFILE_KEYS)}" + elif key == "pis_cofins_profile": + value = value.upper() return {"scope": scope, "scope_key": scope_key if scope != "COMPANY" else "", "key": key, "value": value, "reason": (entry.get("reason") or "").strip() or None}, None diff --git a/central/app/field_contract.py b/central/app/field_contract.py index 0f26728..8f8d526 100644 --- a/central/app/field_contract.py +++ b/central/app/field_contract.py @@ -369,7 +369,10 @@ def resolve_product(p, d, company, refs, ev, ctx, manual_ncm=None): if valid_e: out["CFOP_ENTRADA"] = _r("CFOP_ENTRADA", ALREADY, cfe, reason="DECLARED_VALID_OPERATION_DEPENDENT_MIXED_SCOPE") else: - out["CFOP_ENTRADA"] = _r("CFOP_ENTRADA", MANUAL, cfe, reason="CFOP_ENTRADA_OPERATION_DEPENDENT_MIXED_SCOPE") + # Audited ADMIN decision (entry_scope=MIXED_OR_DYNAMIC) already says no static CFOP_ENTRADA is + # ever correct here: NA (never MANUAL) so this doesn't block product completeness and never + # raises a spurious "answer entry_scope" question — the scope question was already answered. + out["CFOP_ENTRADA"] = _r("CFOP_ENTRADA", NA, cfe, reason="CFOP_ENTRADA_OPERATION_DEPENDENT_MIXED_SCOPE") else: if scope_ctx in ("INTRA", "INTER") and role in ("RESALE", "OWN_PRODUCTION", "INPUT") and st != "UNKNOWN" and not (st == "SUBJECT" and withheld is None): first = "1" if scope_ctx == "INTRA" else "2" @@ -470,13 +473,23 @@ def resolve_product(p, d, company, refs, ev, ctx, manual_ncm=None): # ---------------- PIS / COFINS ---------------- prof_raw, prof_scope = ctx.get("pis_cofins_profile", code, group) - import json as _json - try: - prof = _json.loads(prof_raw) if prof_raw else {} - except ValueError: - prof = {} - for field, pkey, dom in (("COD_ENTRADA_PIS", "pis_in_cst", "cst_pis"), ("COD_SAIDA_PIS", "pis_out_cst", "cst_pis"), ("ALIQ_PIS", "pis_rate", None), - ("COD_ENTRADA_COFINS", "cofins_in_cst", "cst_cofins"), ("COD_SAIDA_COFINS", "cofins_out_cst", "cst_cofins"), ("ALIQ_COFINS", "cofins_rate", None)): + PIS_COFINS_FIELDS = ("COD_ENTRADA_PIS", "pis_in_cst", "cst_pis"), ("COD_SAIDA_PIS", "pis_out_cst", "cst_pis"), ("ALIQ_PIS", "pis_rate", None), \ + ("COD_ENTRADA_COFINS", "cofins_in_cst", "cst_cofins"), ("COD_SAIDA_COFINS", "cofins_out_cst", "cst_cofins"), ("ALIQ_COFINS", "cofins_rate", None) + if prof_raw in ("NOT_APPLICABLE", "OPERATION_TIME"): + # Audited ADMIN decision only (Missão 2 / Bloco 2.4): an operator explicitly recorded, in FiscalContextEntry, + # that this company/product's PIS/COFINS is never taken from the cadastral fields (real evidence: SGV emission + # never reads them) or varies per operation. Never inferred automatically from emission statistics - + # absent this exact recorded value, the field stays MANUAL (see the final `else` below), unchanged. + for field, _pkey, _dom in PIS_COFINS_FIELDS: + out[field] = _r(field, NA, cur[field], reason="ADMIN_DECISION_PIS_COFINS_" + prof_raw) + prof = None + else: + import json as _json + try: + prof = _json.loads(prof_raw) if prof_raw else {} + except ValueError: + prof = {} + for field, pkey, dom in ([] if prof is None else PIS_COFINS_FIELDS): v = cur[field] want = _s(prof.get(pkey)) if prof else None if want is not None and dom and want not in refs[dom]: diff --git a/central/app/fiscal_context_ui.py b/central/app/fiscal_context_ui.py index e2b94f1..2f1a7c6 100644 --- a/central/app/fiscal_context_ui.py +++ b/central/app/fiscal_context_ui.py @@ -23,20 +23,28 @@ DECISIONS = [ "key": "pis_cofins_profile", "scope": "COMPANY", "title": "Perfil de PIS/COFINS (código e alíquota de entrada/saída)", "evidence": "34.946 itens de NFC-e emitidos; 3.336 (9,5%) carregam um CST de PIS/COFINS na nota, sempre o código 01 - mas os " "mesmos produtos têm os campos de cadastro (código de saída, alíquota) sempre em branco no SGV, incluindo os poucos " - "produtos que TÊM esses campos preenchidos manualmente (a nota emitida ignora o valor cadastrado). Isso indica que a " - "emissão não lê esse cadastro para este cliente - mas não é conclusivo o bastante para marcar os 6 campos como " - "não aplicável sem confirmação humana.", "suggestion": None, + "produtos que TÊM esses campos preenchidos manualmente (a nota emitida ignora o valor cadastrado). Reprocessado em " + "2026-09-28 (Missão 2 / Bloco 2.1): os 3.336 itens estão concentrados quase inteiramente em 2022 (3.336 de 3.669 " + "itens daquele ano; 0% em 2018 e em 2023-2026) - coincide com a mesma janela já sinalizada como anômala em outra " + "auditoria (drift de identidade de produto). O mesmo padrão de cadastro nunca preenchido na emissão se repete, sem " + "exceção, nos outros 4 bancos auditados (CONSTRUCAO, MMMODAS, RINCAO CRT=3, UNIAO): 0% de itens com CST de " + "PIS/COFINS em todo o histórico, em todos os anos. Não é um comportamento específico deste cliente. " + "Ver docs/dossie_contador.md e docs/runs/BLOCO2_PIS_COFINS_CROSS_DB_V1.json.", "suggestion": None, "affected_fields": ["COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS"], - "confidence": "Baixa para um perfil único da empresa; considerar tratar como não aplicável ao cadastro (pendente de confirmação contábil)", - "measured_at": "2026-09-27", + "confidence": "Baixa para um perfil único da empresa; considerar NOT_APPLICABLE ou OPERATION_TIME (pendente de confirmação contábil)", + "measured_at": "2026-09-28", + "value_hint": 'JSON de perfil (ex.: {"pis_out_cst":"49","pis_in_cst":"98","pis_rate":"0","cofins_out_cst":"49","cofins_in_cst":"98","cofins_rate":"0"}) ' + 'OU exatamente NOT_APPLICABLE (nunca é lido pelo cadastro) OU OPERATION_TIME (varia por operação) - nunca inferido automaticamente.', + "dossier_link": "/static/docs/dossie_contador.md", }, { "key": "entry_scope", "scope": "COMPANY", "title": "CFOP de entrada padrão (compras dentro/fora do estado)", "evidence": "Apenas 12 documentos de compra reais no ERP para 526 produtos; 11 sem CFOP preenchido, 1 com CFOP intra-estadual. " "Evidência real insuficiente para qualquer sugestão - o CFOP de entrada normalmente é definido pela operação/documento " - "de compra, não pelo cadastro do produto.", "suggestion": None, - "affected_fields": ["CFOP_ENTRADA"], "confidence": "Muito baixa - recomendação: tratar como definido no momento da operação, não como cadastro obrigatório", - "measured_at": "2026-09-27", + "de compra, não pelo cadastro do produto. Ver docs/dossie_contador.md.", "suggestion": None, + "affected_fields": ["CFOP_ENTRADA"], "confidence": "Muito baixa - recomendação: MIXED_OR_DYNAMIC (definido na operação, não bloqueia o produto)", + "measured_at": "2026-09-27", "allowed_values": ["INTRA", "INTER", "MIXED_OR_DYNAMIC"], + "dossier_link": "/static/docs/dossie_contador.md", }, ] diff --git a/central/app/version.py b/central/app/version.py index 73ce7de..89a6172 100644 --- a/central/app/version.py +++ b/central/app/version.py @@ -4,5 +4,5 @@ kept identical to this) and what an AgentRelease row's `version` column holds once published — never a second, independent version number. Fiscal semantics are versioned separately (ENGINE_VERSION/DECISION_VERSION in classification.py, DECISION_ENGINE_VERSION etc.) and are NOT changed by a release bump.""" -GTI_FISCAL_VERSION = "1.3.9" +GTI_FISCAL_VERSION = "1.3.10" API_COMPAT_MAJOR_MINOR = "1.0" # an Agent 1.0.x/1.1.x talks to any Hub 1.0.x diff --git a/central/pyproject.toml b/central/pyproject.toml index 66a49af..7227cf0 100644 --- a/central/pyproject.toml +++ b/central/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gti-fiscal-hub" -version = "1.3.9" +version = "1.3.10" requires-python = ">=3.10" dependencies = [ "fastapi>=0.115,<1", diff --git a/central/static/docs/dossie_contador.md b/central/static/docs/dossie_contador.md new file mode 100644 index 0000000..cb4f9ef --- /dev/null +++ b/central/static/docs/dossie_contador.md @@ -0,0 +1,68 @@ +# Dossiê para o contador (2026-09-28) + +Este documento não afirma nenhuma regra fiscal. Cada pergunta mostra a evidência real medida +(somente leitura, sem gravação em nenhum banco) e qual clique na tela "Contexto Fiscal" do painel +a resposta libera. Identificadores de cliente foram substituídos por rótulos genéricos +(Cliente A–E); nenhum nome de produto, CNPJ ou razão social aparece aqui. + +## (a) Em CRT=1, o PIS/COFINS por item segue o cadastro do produto ou a operação/regime? + +**Evidência**: em 5 instalações auditadas (Cliente A, CRT 1; Clientes B/C/E, CRT 1; Cliente D, +CRT 3), o cadastro do produto **nunca** tem os campos de PIS/COFINS usados pela emissão: mesmo +quando a nota fiscal emitida traz um CST de PIS/COFINS preenchido, o cadastro está em branco para +o mesmo produto (23 casos confirmados no Cliente A). Nos Clientes B, C, D e E, a emissão nunca +preenche o CST de PIS/COFINS (0% dos itens, em todos os anos de histórico disponíveis). Isso indica +que a emissão não lê o cadastro — mas não decide se o valor correto é "não aplicável", "por +operação" ou um perfil fixo por empresa. **Libera**: card "Perfil de PIS/COFINS" → escolher +`NOT_APPLICABLE`, `OPERATION_TIME` ou um perfil JSON de 6 campos. + +## (b) O CST 01 emitido em ~9,5% dos itens é intencional? + +**Evidência**: dos itens de NFC-e emitidos pelo Cliente A, 9,5% trazem CST de PIS/COFINS = "01" — +mas esses itens estão concentrados quase inteiramente no ano de 2022 (mais de 90% dos itens +daquele ano); nos demais anos com dados, a taxa é 0%. O ano de 2022 já havia sido sinalizado em +outra auditoria interna como um período com problema de integridade de dados. A leitura mais +provável é que **não é uma política atual deliberada** — é um comportamento isolado de um período +específico, possivelmente uma versão antiga do software de emissão ou uma configuração temporária. +Não decidimos isso por evidência insuficiente para confirmar a causa exata; a pergunta é se o +contador reconhece esse padrão de 2022. + +## (c) CFOP_ENTRADA no cadastro do produto é necessário ou é sempre definido na nota de compra? + +**Evidência**: +- **Schema do banco**: a coluna é opcional no próprio banco de dados (sem obrigatoriedade, sem + valor padrão, sem regra de validação) — o sistema nunca exige um valor aqui a nível de banco. +- **Uso real é muito diferente entre clientes**: em dois clientes o campo está praticamente sempre + em branco; em outros três está preenchido em quase 100% dos produtos, com valores plausíveis — + ou seja, alguns clientes efetivamente mantêm esse cadastro, outros não. +- Isso mostra que não existe uma regra universal: depende de como cada loja opera. Por isso o motor + já trata isso como uma decisão por instalação (`entry_scope`), nunca uma regra fixa do sistema. +- **Não verificado ainda** (requer acesso à tela de entrada de nota do sistema, que não fizemos): + se a tela de lançamento de nota de compra lê o CFOP_ENTRADA do cadastro como sugestão inicial, ou + sempre pede a digitação manual. Roteiro de 5 passos para teste manual: + 1. Anote o CFOP_ENTRADA atual de um produto de teste (ou deixe em branco). + 2. Abra o cadastro do produto e grave um CFOP_ENTRADA diferente (ex.: 1102). + 3. Lance uma nota de entrada (compra) desse mesmo produto. + 4. Veja se o CFOP do item da nota já vem preenchido com o valor do cadastro, ou em branco. + 5. Repita com CFOP_ENTRADA em branco no cadastro e confirme se a nota exige digitação manual. +- **Libera**: card "CFOP de entrada padrão" → `INTRA`, `INTER` ou `MIXED_OR_DYNAMIC` (este último + não bloqueia mais a completude do produto). + +## (d) O que mais o levantamento deixou como decisão pendente com evidência insuficiente? + +- **Origem da mercadoria** (nacional/importado): nenhuma fonte automática existe para decidir isso + — a grande maioria dos produtos não tem nenhuma evidência. Só pode ser respondido produto a + produto ou por grupo, nunca por uma regra geral. +- **Tratamento de ICMS** para um pequeno grupo de produtos cujo código fiscal está em branco — + parte é cadastro de teste/incompleto, mas alguns produtos reais também aparecem. +- Um pequeno número de produtos com o código fiscal (CSOSN) em branco hoje. Nenhuma simulação + escreve nada; um valor padrão só se aplica de fato após a confirmação do "Tratamento padrão de + ICMS" no card correspondente, e mesmo assim o motor consulta o tratamento antes de propor o + código (nunca aplica um padrão cegamente). + +## Como usar este dossiê + +Cada resposta é dada uma única vez, no painel ("Contexto Fiscal", aba do Agent), nunca escrevendo +nada no ERP diretamente — a decisão fica registrada e auditada, e só depois disso o motor passa a +considerar o campo resolvido (ou explicitamente não-bloqueante). Nenhuma decisão aqui é assumida +por padrão. diff --git a/central/templates/agent_detail.html b/central/templates/agent_detail.html index 0b7bb70..36f285a 100644 --- a/central/templates/agent_detail.html +++ b/central/templates/agent_detail.html @@ -14,11 +14,11 @@ {% for d in fiscal_context %}

{{ d.title }}

Escopo: {{ {"COMPANY":"Empresa","GROUP":"Grupo","PRODUCT":"Produto"}[d.scope] }}
Valor atual
{{ d.current_value or "Não confirmado" }}{% if d.last_changed %} · alterado em {{ d.last_changed }} por {{ d.last_changed_by }}{% endif %}
Sugestão
{{ d.suggestion or "Sem sugestão automática — evidência insuficiente" }}
-
Evidência
{{ d.evidence }} (medido manualmente em {{ d.measured_at }} — não é uma consulta ao vivo)
+
Evidência
{{ d.evidence }} (medido manualmente em {{ d.measured_at }} — não é uma consulta ao vivo){% if d.dossier_link %} · dossiê para o contador{% endif %}
Confiança
{{ d.confidence }}
Campos afetados
{{ d.affected_fields|join(", ") }}
-{% if d.allowed_values %}{% else %}{% endif %} +{% if d.allowed_values %}{% else %}{% endif %}
diff --git a/central/tests/test_field_contract.py b/central/tests/test_field_contract.py index 6a16c4e..264f188 100644 --- a/central/tests/test_field_contract.py +++ b/central/tests/test_field_contract.py @@ -125,7 +125,9 @@ def test_cfop_entrada_mixed_scope_is_operation_dependent_and_never_invents_a_cfo mixed = [{"scope": "COMPANY", "scope_key": "", "key": "entry_scope", "value": "MIXED_OR_DYNAMIC"}] r = run(p=product(CFOP_ENTRADA=None), ctx=mixed) e = r["CFOP_ENTRADA"] - assert e["state"] == MANUAL and e["proposed"] is None and e.get("input_key") is None + # NA (not MANUAL): the audited entry_scope=MIXED_OR_DYNAMIC decision already answered "why no value" - + # this must never block product completeness nor re-ask a question the admin already answered (Missão 2 / Bloco 3.4). + assert e["state"] == NA and e["proposed"] is None and e.get("input_key") is None assert e["reason"] == "CFOP_ENTRADA_OPERATION_DEPENDENT_MIXED_SCOPE" r2 = run(p=product(CFOP_ENTRADA="2102"), ctx=mixed) assert r2["CFOP_ENTRADA"]["state"] == ALREADY and r2["CFOP_ENTRADA"]["reason"] == "DECLARED_VALID_OPERATION_DEPENDENT_MIXED_SCOPE" @@ -175,6 +177,30 @@ def test_cst_ipi_pis_cofins_incidence_iat_ippt(): assert run(p=product(IAT=None))["IAT"]["input_key"] == "rounding" assert run()["INCIDENCIA_PISCOFINS"]["state"] == ALREADY assert st(run(ev={**EV, "recipes": {"000001"}}), "IPPT") == (CHANGE, "P") and run()["IPPT"]["state"] == ALREADY + + +def test_pis_cofins_not_applicable_and_operation_time_require_an_audited_admin_decision(): + """Missão 2 / Bloco 2.4: NOT_APPLICABLE/OPERATION_TIME for PIS/COFINS must never be inferred (e.g. from emission + statistics) - only ever accepted from an explicit, audited FiscalContextEntry(key="pis_cofins_profile").""" + pis_cofins_fields = ("COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS") + # Sem decisão ADMIN => comportamento antigo (continua bloqueando, pedindo pis_cofins_profile). + r = run() + for f in pis_cofins_fields: + assert r[f]["state"] == MANUAL and r[f]["input_key"] == "pis_cofins_profile" + # Com decisão ADMIN NOT_APPLICABLE => NA em todos, nunca escreve, nunca bloqueia completude. + na_ctx = [{"scope": "COMPANY", "scope_key": "", "key": "pis_cofins_profile", "value": "NOT_APPLICABLE"}] + r_na = run(ctx=na_ctx) + for f in pis_cofins_fields: + assert r_na[f]["state"] == NA and r_na[f]["proposed"] is None and r_na[f]["reason"] == "ADMIN_DECISION_PIS_COFINS_NOT_APPLICABLE" + # Com decisão ADMIN OPERATION_TIME => NA em todos (não bloqueia), motivo distinto, nunca escreve. + ot_ctx = [{"scope": "COMPANY", "scope_key": "", "key": "pis_cofins_profile", "value": "OPERATION_TIME"}] + r_ot = run(ctx=ot_ctx) + for f in pis_cofins_fields: + assert r_ot[f]["state"] == NA and r_ot[f]["proposed"] is None and r_ot[f]["reason"] == "ADMIN_DECISION_PIS_COFINS_OPERATION_TIME" + # Uma decisão de PROFILE (JSON) continua funcionando exatamente como antes (não regressivo). + prof = '{"pis_out_cst": "49", "pis_in_cst": "98", "pis_rate": "0", "cofins_out_cst": "49", "cofins_in_cst": "98", "cofins_rate": "0"}' + r_prof = run(ctx=[{"scope": "COMPANY", "scope_key": "", "key": "pis_cofins_profile", "value": prof}]) + assert st(r_prof, "COD_SAIDA_PIS") == (CHANGE, "49") assert run(p=product(TIPO="MATERIA-PRIMA"))["IPPT"]["state"] == NA diff --git a/central/tests/test_fiscal_context.py b/central/tests/test_fiscal_context.py index 9f3e2d0..4bb1c77 100644 --- a/central/tests/test_fiscal_context.py +++ b/central/tests/test_fiscal_context.py @@ -27,6 +27,11 @@ def test_validation_rules(): {"scope": "COMPANY", "key": "icms_rate", "value": "abc"}, {"scope": "COMPANY", "key": "nope", "value": "1"}): assert validate_entry(bad)[1] assert validate_entry({"scope": "COMPANY", "key": "pis_cofins_profile", "value": "{\"pis_out_cst\": \"49\"}"})[1] is None + # Missão 2 / Bloco 2.3: the two audited sentinels must validate (case-insensitive) alongside the JSON profile shape. + ok, err = validate_entry({"scope": "COMPANY", "key": "pis_cofins_profile", "value": "not_applicable"}) + assert err is None and ok["value"] == "NOT_APPLICABLE" + ok, err = validate_entry({"scope": "COMPANY", "key": "pis_cofins_profile", "value": "OPERATION_TIME"}) + assert err is None and ok["value"] == "OPERATION_TIME" def test_import_is_all_or_nothing_upserting_and_listed(): diff --git a/central/tests/test_health.py b/central/tests/test_health.py index d0a0cb0..e859513 100644 --- a/central/tests/test_health.py +++ b/central/tests/test_health.py @@ -17,7 +17,7 @@ def test_health_is_unavailable_when_database_is_down(): def test_version_endpoint_reports_release_and_fiscal_versions(): body = TestClient(create_app(database_probe=lambda: True)).get("/version").json() - assert body["version"] == "1.3.9" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"] + assert body["version"] == "1.3.10" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"] def test_production_refuses_the_dev_token(monkeypatch): diff --git a/docs/AGENT2_UPGRADE_RUNBOOK_V1.md b/docs/AGENT2_UPGRADE_RUNBOOK_V1.md new file mode 100644 index 0000000..cd5a0e6 --- /dev/null +++ b/docs/AGENT2_UPGRADE_RUNBOOK_V1.md @@ -0,0 +1,46 @@ +# Agent 2 — Roteiro de upgrade manual 1.3.7 → 1.3.8 + +## Self-update: NÃO EXISTE + +Evidência no código: +- `deploy/agent-windows/gti-agent.ps1` só aceita `install | install-auto | configure | + preflight | start | stop | restart | status | uninstall` (`ValidateSet` na linha 20). + Não há verbo `update`/`upgrade`. +- `install-auto` é idempotente e **recusa fazer qualquer coisa** se `.env` já tem + `AGENT_TOKEN` e o serviço já existe ("Already enrolled and installed... Nothing to + do.") — não reenrola, não atualiza arquivos. +- O painel do Hub (`central/app/control_plane.py:140`, `compute_version_drift`) só + calcula e **exibe** o rótulo "Desatualizada" (`version_drift`); não existe nenhum + código que baixe pacote, copie arquivo ou reinicie o serviço a partir do Hub. O + botão "Atualização disponível" é só informativo — não há ação de clique associada. +- `docs/PILOT_DEPLOYMENT.md:367` já documenta isso: "There is no auto-update + mechanism yet (deliberately out of scope)". + +Conclusão: o upgrade é sempre manual, feito por humano na máquina do cliente. + +## Roteiro (operador, elevado) + +1. **Backup antes de tocar em qualquer arquivo** + - Copie `\agent\agent_state.sqlite` para um local seguro + (ex.: `agent_state.sqlite.bak-2026-09-28`). + - Copie também `\.env` (tem o `AGENT_TOKEN` permanente — + nunca gerar um novo). +2. `gti-agent.ps1 stop` (para o serviço `GTIFiscalAgent`). +3. Extraia o pacote 1.3.8 publicado **sobre** a mesma pasta de instalação + (sobrescrever arquivos de código), **sem apagar a pasta antes** e: + - **não** sobrescrever `.env`; + - **não** sobrescrever/apagar `agent\agent_state.sqlite`; + - **não** copiar/usar `enrollment.json` do pacote novo (ele é só para instalação + nova; reenrollar geraria outro `installation_id`/token). +4. `gti-agent.ps1 install` (elevado) — reinstala dependências no venv existente e + re-registra o serviço com o novo código. Não toca `.env` nem o SQLite. +5. Confira que `.env` continua com o mesmo `AGENT_TOKEN`/`AGENT_ID` de antes. +6. `gti-agent.ps1 preflight` — deve passar (mesmas credenciais Firebird de antes). +7. `gti-agent.ps1 start`. +8. Verificar (ainda na máquina): `gti-agent.ps1 status` mostra o serviço rodando. +9. Confirmar sozinho ou pedir para eu confirmar (somente leitura, pelo Hub): + heartbeat com `agent_version=1.3.8`, `installation_id` inalterado + (`bb50884a-5e78-4f26-8cac-804932dae8de`), horário do heartbeat novo. + +Se algo falhar depois do passo 3, restaurar o backup do `agent_state.sqlite` e do +`.env` resolve qualquer estado inconsistente sem precisar reenrolar. diff --git a/docs/dossie_contador.md b/docs/dossie_contador.md new file mode 100644 index 0000000..f4065bb --- /dev/null +++ b/docs/dossie_contador.md @@ -0,0 +1,73 @@ +# Dossiê para o contador — Missão 2 (2026-09-28) + +Este documento não afirma nenhuma regra fiscal. Cada pergunta mostra a evidência real medida +(somente leitura, sem gravação em nenhum banco) e qual clique na tela "Contexto Fiscal" do painel +a resposta libera. + +## (a) Em CRT=1, o PIS/COFINS por item segue o cadastro do produto ou a operação/regime? + +**Evidência**: nos 5 bancos auditados (Agent 2/GTI, CONSTRUCAO, MMMODAS, RINCAO, UNIAO — CRT 1 e +CRT 3), o cadastro do produto (`C000025`) **nunca** tem os campos de PIS/COFINS usados pela emissão: +mesmo quando a nota fiscal emitida traz um CST de PIS/COFINS preenchido, o cadastro está em branco +para o mesmo produto (23 casos confirmados no Agent 2). Em CONSTRUCAO, MMMODAS, RINCAO e UNIAO, a +emissão nunca preenche o CST de PIS/COFINS (0% dos itens, em todos os anos de histórico +disponíveis). Isso indica que a emissão não lê o cadastro — mas não decide se o valor correto é +"não aplicável", "por operação" ou um perfil fixo por empresa. **Libera**: card "Perfil de +PIS/COFINS" → escolher `NOT_APPLICABLE`, `OPERATION_TIME` ou um perfil JSON de 6 campos. + +## (b) O CST 01 emitido em ~9,5% dos itens é intencional? + +**Evidência**: dos 34.946 itens de NFC-e emitidos pelo Agent 2, 3.336 (9,5%) trazem CST de +PIS/COFINS = "01" — mas **3.336 desses 3.669 itens estão concentrados no ano de 2022** (90,9% dos +itens daquele ano); nos demais anos com dados (2018, 2023, 2024, 2025, 2026), a taxa é 0%. O ano de +2022 já havia sido sinalizado em outra auditoria (`docs/AI_PROJECT_STATE.md`, mission +"Product Fiscal Defaults Engine V1") como um período com problema de integridade de dados (drift de +identidade de produto em dezembro/2022). A leitura mais provável é que **não é uma política atual +deliberada** — é um comportamento isolado de um período específico, possivelmente uma versão antiga +do software de emissão ou uma configuração temporária. Não decidimos isso por evidência +insuficiente para confirmar a causa exata; a pergunta é se o contador reconhece esse padrão de 2022. + +## (c) CFOP_ENTRADA no cadastro do produto é necessário ou é sempre definido na nota de compra? + +**Evidência**: +- **Schema do Firebird** (`RDB$RELATION_FIELDS`/`RDB$CHECK_CONSTRAINTS` em `C000025.CFOP_ENTRADA`): + coluna **NULLABLE**, **sem DEFAULT**, **sem CHECK/domínio** — o próprio SGV nunca exige um valor + aqui a nível de banco. +- **Uso real é muito diferente entre clientes**: no Agent 2 (GTI), CFOP_ENTRADA está em branco em + 527/527 produtos (100%). Em RINCAO, 891/894 (99,7%) em branco. Mas em CONSTRUCAO, MMMODAS e UNIAO + o campo está **preenchido** em 99,8%/99,9%/99,97% dos produtos, com valores plausíveis (1102, 1403, + 2202 etc.) — ou seja, alguns clientes efetivamente mantêm esse cadastro, outros não. +- Isso mostra que não existe uma regra universal: depende de como cada loja opera. Por isso o motor + já trata isso como uma decisão por instalação (`entry_scope`), nunca uma regra fixa do sistema. +- **Não verificado nesta sessão** (requer acesso ao executável da tela de entrada de nota, que não + fizemos): se a tela de lançamento de nota de compra do SGV lê o CFOP_ENTRADA do cadastro como + sugestão inicial, ou sempre pede a digitação manual. Roteiro de 5 passos para teste manual: + 1. Anote o CFOP_ENTRADA atual de um produto de teste (ou deixe em branco). + 2. Abra o cadastro do produto no SGV e grave um CFOP_ENTRADA diferente (ex.: 1102). + 3. Lance uma nota de entrada (compra) desse mesmo produto. + 4. Veja se o CFOP do item da nota já vem preenchido com o valor do cadastro, ou em branco. + 5. Repita com CFOP_ENTRADA em branco no cadastro e confirme se a nota exige digitação manual. +- **Libera**: card "CFOP de entrada padrão" → `INTRA`, `INTER` ou `MIXED_OR_DYNAMIC` (este último + não bloqueia mais a completude do produto — Missão 2, Bloco 3.4). + +## (d) O que mais o dry-run deixou como decisão pendente com evidência insuficiente? + +- **COD_ORIGEM_MERCADORIA** (origem da mercadoria): nenhuma fonte automática existe no ERP para + decidir nacional/importado — 526 de 527 produtos do Agent 2 não têm nenhuma evidência. Só pode + ser respondido produto a produto ou por grupo, nunca por uma regra geral. +- **icms_treatment** para 12 produtos (Agent 2) cujo CSOSN está em branco — a maioria concentrada em + produtos de teste/cadastro incompleto (nomes como "TESTE...", "BUG", "SDFSDFSD"), mas 12 produtos + reais também aparecem (Coca-Cola, queijos, peixes) — ver `docs/runs/GTI_dryrun_universe_summary.json`. +- **30 produtos com CSOSN em branco no Agent 2** (medição em 2026-09-28; uma medição anterior de + 2026-09-27, registrada em `central/app/fiscal_context_ui.py`, contava 28 — a diferença de 2 é + compatível com o cadastro de produtos de teste que muda dia a dia neste ambiente, não uma + divergência de regra). Nenhuma simulação escreve nada; o padrão 102 só se aplica de fato após a + confirmação do "Tratamento padrão de ICMS" no card correspondente, e mesmo assim o motor consulta + o tratamento antes de propor o código (nunca aplica 102 cegamente). + +## Como usar este dossiê + +Cada resposta é dada uma única vez, no painel ("Contexto Fiscal", aba do Agent), nunca escrevendo +nada no Firebird diretamente — a decisão fica registrada e auditada (`FiscalContextEntry`), e só +depois disso o motor passa a considerar o campo resolvido (ou explicitamente não-bloqueante). +Nenhuma decisão aqui é assumida por padrão. diff --git a/docs/fiscal_completeness_matrix.md b/docs/fiscal_completeness_matrix.md new file mode 100644 index 0000000..6ba9028 --- /dev/null +++ b/docs/fiscal_completeness_matrix.md @@ -0,0 +1,44 @@ +# Matriz de completude fiscal — 18 campos (Missão 2 / Bloco 4) + +Evidência: dry-run read-only de 2026-09-28 sobre GTI.FDB (Agent 2, 527 produtos, CRT=1, UF=BA, +CONTRIBUINTE_IPI=N — `docs/runs/GTI_dryrun_universe_summary.json`), cruzamento com CONSTRUCAO/ +MMMODAS/RINCAO/UNIAO (`docs/runs/BLOCO2_PIS_COFINS_CROSS_DB_V1.json`) e metadados de esquema +Firebird lidos diretamente (`RDB$RELATION_FIELDS`/`RDB$CHECK_CONSTRAINTS`). Nenhuma regra fiscal +nova foi inventada: campos sem evidência suficiente ficam `pendente de contador`. + +| Campo | Classificação | Condição | Fonte da evidência | Status | +|---|---|---|---|---| +| CLASSIFICACAO_FISCAL (NCM) | MANDATORY | sempre | dataset NCM oficial | comprovado | +| CEST | CONDITIONALLY_MANDATORY | só quando o NCM tem CEST oficial único | tabela CEST/CONFAZ | comprovado | +| IPI | CONDITIONALLY_MANDATORY | só quando TIPI dá alíquota numérica não-ambígua | TIPI/IPI | comprovado | +| EX_TIPI | OPTIONAL | só quando o NCM tem múltiplas linhas TIPI (EX) | TIPI/IPI | comprovado | +| SITUACAO_TRIBUTARIA | CONDITIONALLY_MANDATORY | depende do NCM ser válido | regra ST interna (BA Anexo 1) | comprovado | +| CFOP | CONDITIONALLY_MANDATORY | não aplicável a INPUT (matéria-prima) | product_role + ST | comprovado | +| CFOP_ENTRADA | OPERATION_TIME_ONLY | quando `entry_scope=MIXED_OR_DYNAMIC` (decisão ADMIN auditada); caso contrário CONDITIONALLY_MANDATORY (INTRA/INTER) | decisão ADMIN + product_role + ST; **schema Firebird: coluna NULLABLE, sem DEFAULT, sem CHECK — o próprio SGV nunca exige valor** | comprovado (schema); decisão de escopo pendente por instalação | +| CST | NOT_APPLICABLE | CRT=1 (Simples) sempre usa CSOSN, nunca CST | regra CRT/emissão | comprovado | +| CST_ENTRADA | NOT_APPLICABLE | mesma razão que CST | regra CRT/emissão | comprovado | +| CSOSN | CONDITIONALLY_MANDATORY | CRT=1; precisa de `icms_treatment` quando em branco | dataset CSOSN + contexto ADMIN | comprovado | +| ALIQUOTA (ICMS) | NOT_APPLICABLE | CSOSN Simples sem destaque de ICMS (102/103/300/400/500) | regra CSOSN | comprovado | +| COD_ORIGEM_MERCADORIA | CONDITIONALLY_MANDATORY | nenhuma fonte automática decide — sempre depende de contexto GROUP/PRODUCT | nenhuma (evidência zero no ERP) | pendente de contador/operador (sem fonte determinística) | +| CST_IPI | NOT_APPLICABLE | empresa não é contribuinte de IPI (C000004.CONTRIBUINTE_IPI=N) | heartbeat do Agent | comprovado | +| COD_ENTRADA_PIS / COD_SAIDA_PIS / ALIQ_PIS / COD_ENTRADA_COFINS / COD_SAIDA_COFINS / ALIQ_COFINS | CONDITIONALLY_MANDATORY (bloqueante) até decisão ADMIN; podem virar NOT_APPLICABLE ou OPERATION_TIME_ONLY só por decisão ADMIN auditada (`pis_cofins_profile`) | ver Bloco 2 — emissão nunca lê o cadastro em nenhum dos 5 bancos auditados | evidência de emissão (NFCE_ITENS) + decisão ADMIN | evidência forte (não-uso na emissão); classificação final pendente de confirmação contábil | +| INCIDENCIA_PISCOFINS | OPTIONAL | valor declarado válido não é contradito | regra de incidência | comprovado | +| PISCOFINS | OPTIONAL | idem | regra de incidência | comprovado | +| IAT | OPTIONAL | arredondamento, evidência fraca | regra + contexto `rounding` | comprovado (não bloqueia) | +| IPPT | CONDITIONALLY_MANDATORY | derivado de product_role/receita/compra quando role conhecido | evidência de produção/compra | comprovado | + +## Regra de bloqueio do "COMPLETE" + +`central/app/enrichment/status.py::coverage_status` já implementa exatamente a regra pedida: +`SAFE_BLOCKED` só quando existe campo `NEEDS_REVIEW` ou `INSUFFICIENT_EVIDENCE`; `NOT_APPLICABLE`, +`ALREADY_SET` e `AUTO_FILL_SAFE` nunca bloqueiam. Os dois ajustes de engine desta missão +(CFOP_ENTRADA com `entry_scope=MIXED_OR_DYNAMIC` → `NA`; PIS/COFINS com decisão ADMIN +`NOT_APPLICABLE`/`OPERATION_TIME` → `NA`) plugam diretamente nessa regra já existente — nenhuma +mudança adicional foi necessária em `coverage_status`. Sem decisão ADMIN, os dois grupos continuam +bloqueando exatamente como antes (testes de regressão em `central/tests/test_field_contract.py`). + +## Pendente de contador (não implementado, não bloqueia nada além do que já bloqueava) + +- COD_ORIGEM_MERCADORIA: nenhuma fonte automática existe; permanece MANUAL por produto/grupo. +- Classificação final de PIS/COFINS (NOT_APPLICABLE vs OPERATION_TIME vs perfil fixo): ver + `docs/dossie_contador.md`. diff --git a/docs/runs/BLOCO2_PIS_COFINS_CROSS_DB_V1.json b/docs/runs/BLOCO2_PIS_COFINS_CROSS_DB_V1.json new file mode 100644 index 0000000..010ff02 --- /dev/null +++ b/docs/runs/BLOCO2_PIS_COFINS_CROSS_DB_V1.json @@ -0,0 +1,264 @@ +{ + "GTI": { + "name": "GTI", + "company_status": "PASS", + "crt": "1", + "uf": "BA", + "items_total": 34946, + "items_cst_pis_cofins_filled": 3336, + "pct_cst_filled": 9.55, + "cst_distribution": { + "01": 3336, + "(blank)": 1332 + }, + "by_year": { + "2018": { + "items": 30278, + "cst_filled": 0 + }, + "2022": { + "items": 3669, + "cst_filled": 3336 + }, + "2023": { + "items": 240, + "cst_filled": 0 + }, + "2024": { + "items": 426, + "cst_filled": 0 + }, + "2025": { + "items": 230, + "cst_filled": 0 + }, + "2026": { + "items": 103, + "cst_filled": 0 + } + }, + "aliquota_pis_distribution": { + "0": 4668 + }, + "aliquota_cofins_distribution": { + "0": 4668 + }, + "identity_consistent_items": 1241, + "emitted_cst_present_when_cadastro_blank": 23 + }, + "TESTESOFICIAIS": { + "name": "TESTESOFICIAIS", + "company_status": "PASS", + "crt": "1", + "uf": "BA", + "items_total": 34946, + "items_cst_pis_cofins_filled": 3336, + "pct_cst_filled": 9.55, + "cst_distribution": { + "01": 3336, + "(blank)": 1332 + }, + "by_year": { + "2018": { + "items": 30278, + "cst_filled": 0 + }, + "2022": { + "items": 3669, + "cst_filled": 3336 + }, + "2023": { + "items": 240, + "cst_filled": 0 + }, + "2024": { + "items": 426, + "cst_filled": 0 + }, + "2025": { + "items": 230, + "cst_filled": 0 + }, + "2026": { + "items": 103, + "cst_filled": 0 + } + }, + "aliquota_pis_distribution": { + "0": 4668 + }, + "aliquota_cofins_distribution": { + "0": 4668 + }, + "identity_consistent_items": 1241, + "emitted_cst_present_when_cadastro_blank": 23 + }, + "CONSTRUCAO": { + "name": "CONSTRUCAO", + "company_status": "PASS", + "crt": "1", + "uf": "BA", + "items_total": 28934, + "items_cst_pis_cofins_filled": 0, + "pct_cst_filled": 0.0, + "cst_distribution": { + "(blank)": 28934 + }, + "by_year": { + "2024": { + "items": 5144, + "cst_filled": 0 + }, + "2025": { + "items": 13266, + "cst_filled": 0 + }, + "2026": { + "items": 10524, + "cst_filled": 0 + } + }, + "aliquota_pis_distribution": { + "0": 28934 + }, + "aliquota_cofins_distribution": { + "0": 28934 + }, + "identity_consistent_items": 28714, + "emitted_cst_present_when_cadastro_blank": 0 + }, + "MMMODAS": { + "name": "MMMODAS", + "company_status": "PASS", + "crt": "1", + "uf": "BA", + "items_total": 26067, + "items_cst_pis_cofins_filled": 0, + "pct_cst_filled": 0.0, + "cst_distribution": { + "(blank)": 23238 + }, + "by_year": { + "2018": { + "items": 2655, + "cst_filled": 0 + }, + "2019": { + "items": 174, + "cst_filled": 0 + }, + "2024": { + "items": 9456, + "cst_filled": 0 + }, + "2025": { + "items": 9403, + "cst_filled": 0 + }, + "2026": { + "items": 4379, + "cst_filled": 0 + } + }, + "aliquota_pis_distribution": { + "0": 23238 + }, + "aliquota_cofins_distribution": { + "0": 23238 + }, + "identity_consistent_items": 23201, + "emitted_cst_present_when_cadastro_blank": 0 + }, + "RINCAO": { + "name": "RINCAO", + "company_status": "PASS", + "crt": "3", + "uf": "BA", + "items_total": 224355, + "items_cst_pis_cofins_filled": 0, + "pct_cst_filled": 0.0, + "cst_distribution": { + "(blank)": 142784 + }, + "by_year": { + "2017": { + "items": 22363, + "cst_filled": 0 + }, + "2018": { + "items": 57888, + "cst_filled": 0 + }, + "2019": { + "items": 1320, + "cst_filled": 0 + }, + "2024": { + "items": 33989, + "cst_filled": 0 + }, + "2025": { + "items": 65881, + "cst_filled": 0 + }, + "2026": { + "items": 42914, + "cst_filled": 0 + } + }, + "aliquota_pis_distribution": { + "0": 142784 + }, + "aliquota_cofins_distribution": { + "0": 142784 + }, + "identity_consistent_items": 94195, + "emitted_cst_present_when_cadastro_blank": 0 + }, + "UNIAO": { + "name": "UNIAO", + "company_status": "PASS", + "crt": "1", + "uf": "BA", + "items_total": 121266, + "items_cst_pis_cofins_filled": 0, + "pct_cst_filled": 0.0, + "cst_distribution": { + "(blank)": 104973 + }, + "by_year": { + "2017": { + "items": 9169, + "cst_filled": 0 + }, + "2018": { + "items": 6747, + "cst_filled": 0 + }, + "2019": { + "items": 377, + "cst_filled": 0 + }, + "2024": { + "items": 24146, + "cst_filled": 0 + }, + "2025": { + "items": 45031, + "cst_filled": 0 + }, + "2026": { + "items": 35796, + "cst_filled": 0 + } + }, + "aliquota_pis_distribution": { + "0": 104973 + }, + "aliquota_cofins_distribution": { + "0": 104973 + }, + "identity_consistent_items": 104830, + "emitted_cst_present_when_cadastro_blank": 0 + } +} \ No newline at end of file diff --git a/docs/runs/GTI_dryrun_universe_summary.json b/docs/runs/GTI_dryrun_universe_summary.json new file mode 100644 index 0000000..6ca8b60 --- /dev/null +++ b/docs/runs/GTI_dryrun_universe_summary.json @@ -0,0 +1,277 @@ +{ + "name": "GTI", + "total_products": 527, + "company": { + "uf": "BA", + "crt": "1", + "contribuinte_ipi": "N", + "company_code": "000001", + "company_key": "9b1d6541", + "pis_cofins_emission_items": 34946, + "pis_cofins_emission_nonblank": 3336, + "pis_cofins_unused_by_emission": false + }, + "context_entries": 0, + "fields": { + "CLASSIFICACAO_FISCAL": { + "ALREADY_CORRECT": 500, + "MANUAL_INPUT_REQUIRED": 27 + }, + "IPI": { + "NOT_APPLICABLE": 317, + "MANUAL_INPUT_REQUIRED": 210 + }, + "EX_TIPI": { + "NOT_APPLICABLE": 500, + "MANUAL_INPUT_REQUIRED": 27 + }, + "CEST": { + "NOT_APPLICABLE": 482, + "MANUAL_INPUT_REQUIRED": 41, + "ALREADY_CORRECT": 4 + }, + "SITUACAO_TRIBUTARIA": { + "ALREADY_CORRECT": 499, + "MANUAL_INPUT_REQUIRED": 28 + }, + "CFOP": { + "ALREADY_CORRECT": 488, + "CHANGE_REQUIRED": 4, + "MANUAL_INPUT_REQUIRED": 28, + "NOT_APPLICABLE": 7 + }, + "CFOP_ENTRADA": { + "MANUAL_INPUT_REQUIRED": 527 + }, + "CST": { + "NOT_APPLICABLE": 527 + }, + "CST_ENTRADA": { + "NOT_APPLICABLE": 527 + }, + "CSOSN": { + "ALREADY_CORRECT": 489, + "MANUAL_INPUT_REQUIRED": 38 + }, + "ALIQUOTA": { + "NOT_APPLICABLE": 493, + "ALREADY_CORRECT": 34 + }, + "COD_ORIGEM_MERCADORIA": { + "MANUAL_INPUT_REQUIRED": 526, + "ALREADY_CORRECT": 1 + }, + "CST_IPI": { + "NOT_APPLICABLE": 527 + }, + "COD_ENTRADA_PIS": { + "MANUAL_INPUT_REQUIRED": 526, + "ALREADY_CORRECT": 1 + }, + "COD_SAIDA_PIS": { + "MANUAL_INPUT_REQUIRED": 525, + "ALREADY_CORRECT": 2 + }, + "ALIQ_PIS": { + "MANUAL_INPUT_REQUIRED": 525, + "ALREADY_CORRECT": 2 + }, + "COD_ENTRADA_COFINS": { + "MANUAL_INPUT_REQUIRED": 526, + "ALREADY_CORRECT": 1 + }, + "COD_SAIDA_COFINS": { + "MANUAL_INPUT_REQUIRED": 525, + "ALREADY_CORRECT": 2 + }, + "ALIQ_COFINS": { + "MANUAL_INPUT_REQUIRED": 525, + "ALREADY_CORRECT": 2 + }, + "INCIDENCIA_PISCOFINS": { + "ALREADY_CORRECT": 527 + }, + "PISCOFINS": { + "ALREADY_CORRECT": 527 + }, + "IAT": { + "ALREADY_CORRECT": 527 + }, + "IPPT": { + "ALREADY_CORRECT": 512, + "CHANGE_REQUIRED": 8, + "NOT_APPLICABLE": 7 + } + }, + "reasons": { + "CLASSIFICACAO_FISCAL": { + "ALREADY_CORRECT|NCM_VALID_IN_DATASET": 500, + "MANUAL_INPUT_REQUIRED|NCM_INVALID": 24, + "MANUAL_INPUT_REQUIRED|NCM_LEGACY_NO_UNAMBIGUOUS_SUCCESSOR": 3 + }, + "IPI": { + "NOT_APPLICABLE|TIPI_RATE_BLANK_NEVER_ZERO": 297, + "MANUAL_INPUT_REQUIRED|STORE_IPI_PROFILE_NOT_DECIDED": 107, + "MANUAL_INPUT_REQUIRED|SEVERAL_TIPI_ROWS_FOR_NCM": 76, + "MANUAL_INPUT_REQUIRED|BLOCKED_BY_NCM": 27, + "NOT_APPLICABLE|TIPI_NT_OR_NON_NUMERIC_NEVER_WRITTEN_TO_NUMERIC_FIELD": 20 + }, + "EX_TIPI": { + "NOT_APPLICABLE|NO_EX_ROW_FOR_NCM": 424, + "NOT_APPLICABLE|DESCRIPTION_EVIDENCES_NO_EX_BASE_RATE_APPLIES": 76, + "MANUAL_INPUT_REQUIRED|BLOCKED_BY_NCM": 27 + }, + "CEST": { + "NOT_APPLICABLE|DESCRIPTION_MATCHES_NO_OFFICIAL_CEST_ITEM": 449, + "NOT_APPLICABLE|NCM_NOT_IN_CEST_LIST": 33, + "MANUAL_INPUT_REQUIRED|BLOCKED_BY_NCM": 27, + "MANUAL_INPUT_REQUIRED|SEVERAL_OFFICIAL_CEST_FOR_NCM": 13, + "ALREADY_CORRECT|ERP_CEST_IS_OFFICIAL_FOR_NCM": 4, + "MANUAL_INPUT_REQUIRED|ERP_CEST_NOT_OFFICIAL_FOR_NCM": 1 + }, + "SITUACAO_TRIBUTARIA": { + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 499, + "MANUAL_INPUT_REQUIRED|BLOCKED_BY_NCM": 27, + "MANUAL_INPUT_REQUIRED|BLANK_OR_INVALID": 1 + }, + "CFOP": { + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 472, + "MANUAL_INPUT_REQUIRED|BLOCKED_BY_NCM": 21, + "ALREADY_CORRECT|RULE_PROVEN_EQUALS_DECLARED": 16, + "NOT_APPLICABLE|INPUT_IS_NOT_A_SALE_ITEM": 7, + "MANUAL_INPUT_REQUIRED|CFOP_NEEDS_ST_STATUS": 7, + "CHANGE_REQUIRED|PROVEN_ROLE_AND_ST_CONTRADICT_DECLARED_CFOP": 2, + "CHANGE_REQUIRED|DERIVED_FROM_ROLE_AND_ST_FILLING_BLANK": 2 + }, + "CFOP_ENTRADA": { + "MANUAL_INPUT_REQUIRED|CFOP_ENTRADA_NEEDS_PRODUCT_ROLE": 253, + "MANUAL_INPUT_REQUIRED|CFOP_ENTRADA_NEEDS_ENTRY_SCOPE": 247, + "MANUAL_INPUT_REQUIRED|BLOCKED_BY_NCM": 27 + }, + "CST": { + "NOT_APPLICABLE|CRT1_SIMPLES_EMISSION_USES_CSOSN": 527 + }, + "CST_ENTRADA": { + "NOT_APPLICABLE|CRT1_SIMPLES_EMISSION_USES_CSOSN": 527 + }, + "CSOSN": { + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 489, + "MANUAL_INPUT_REQUIRED|BLOCKED_BY_NCM": 27, + "MANUAL_INPUT_REQUIRED|CSOSN_NEEDS_ICMS_TREATMENT": 11 + }, + "ALIQUOTA": { + "NOT_APPLICABLE|SIMPLES_CSOSN_HIGHLIGHTS_NO_ICMS_RATE": 493, + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 34 + }, + "COD_ORIGEM_MERCADORIA": { + "MANUAL_INPUT_REQUIRED|ORIGIN_NO_EVIDENCE": 526, + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 1 + }, + "CST_IPI": { + "NOT_APPLICABLE|COMPANY_IS_NOT_AN_IPI_TAXPAYER_C000004.CONTRIBUINTE_IPI": 527 + }, + "COD_ENTRADA_PIS": { + "MANUAL_INPUT_REQUIRED|NO_OFFICIAL_RULE_SOURCE_IN_CORE_DATASETS": 525, + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 1, + "MANUAL_INPUT_REQUIRED|DECLARED_CST_NOT_IN_OFFICIAL_TABLE": 1 + }, + "COD_SAIDA_PIS": { + "MANUAL_INPUT_REQUIRED|NO_OFFICIAL_RULE_SOURCE_IN_CORE_DATASETS": 524, + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 2, + "MANUAL_INPUT_REQUIRED|DECLARED_CST_NOT_IN_OFFICIAL_TABLE": 1 + }, + "ALIQ_PIS": { + "MANUAL_INPUT_REQUIRED|NO_OFFICIAL_RULE_SOURCE_IN_CORE_DATASETS": 525, + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 2 + }, + "COD_ENTRADA_COFINS": { + "MANUAL_INPUT_REQUIRED|NO_OFFICIAL_RULE_SOURCE_IN_CORE_DATASETS": 525, + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 1, + "MANUAL_INPUT_REQUIRED|DECLARED_CST_NOT_IN_OFFICIAL_TABLE": 1 + }, + "COD_SAIDA_COFINS": { + "MANUAL_INPUT_REQUIRED|NO_OFFICIAL_RULE_SOURCE_IN_CORE_DATASETS": 524, + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 2, + "MANUAL_INPUT_REQUIRED|DECLARED_CST_NOT_IN_OFFICIAL_TABLE": 1 + }, + "ALIQ_COFINS": { + "MANUAL_INPUT_REQUIRED|NO_OFFICIAL_RULE_SOURCE_IN_CORE_DATASETS": 525, + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 2 + }, + "INCIDENCIA_PISCOFINS": { + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 527 + }, + "PISCOFINS": { + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 527 + }, + "IAT": { + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 527 + }, + "IPPT": { + "ALREADY_CORRECT|DECLARED_VALID_NOT_CONTRADICTED": 264, + "ALREADY_CORRECT|DERIVED_FROM_PRODUCT_ROLE:C000025.TIPO=VENDA,STOCK_ENTRIES(C000100.ENTRADAS>0)": 229, + "ALREADY_CORRECT|DERIVED_FROM_PRODUCT_ROLE:C000025.TIPO=VENDA,PURCHASE_DOCUMENT": 19, + "CHANGE_REQUIRED|DERIVED_FROM_PRODUCT_ROLE:C000032.TIPO=COMPOSICAO": 7, + "NOT_APPLICABLE|INPUT_OR_SERVICE_HAS_NO_PRODUCTION_INDICATOR": 7, + "CHANGE_REQUIRED|DERIVED_FROM_PRODUCT_ROLE:PROD_COZINHA=1,RECEITA_INGREDIENTES": 1 + } + }, + "manual_inputs": { + "COD_ORIGEM_MERCADORIA|origin": 526, + "COD_ENTRADA_PIS|pis_cofins_profile": 526, + "COD_ENTRADA_COFINS|pis_cofins_profile": 526, + "COD_SAIDA_PIS|pis_cofins_profile": 525, + "ALIQ_PIS|pis_cofins_profile": 525, + "COD_SAIDA_COFINS|pis_cofins_profile": 525, + "ALIQ_COFINS|pis_cofins_profile": 525, + "CFOP_ENTRADA|product_role": 253, + "CFOP_ENTRADA|entry_scope": 247, + "IPI|store_ipi_profile": 107, + "IPI|ex_tipi": 76, + "CLASSIFICACAO_FISCAL|ncm": 27, + "IPI|ncm": 27, + "EX_TIPI|ncm": 27, + "CEST|ncm": 27, + "SITUACAO_TRIBUTARIA|ncm": 27, + "CFOP_ENTRADA|ncm": 27, + "CSOSN|ncm": 27, + "CFOP|ncm": 21, + "CEST|cest": 14, + "CSOSN|icms_treatment": 11, + "CFOP|st_status": 7, + "SITUACAO_TRIBUTARIA|icms_treatment": 1 + }, + "roles": { + "UNKNOWN": 264, + "RESALE": 248, + "OWN_PRODUCTION": 8, + "INPUT": 7 + }, + "change_required_cells": 12, + "write_eligible_cells": 12, + "manual_inputs_by_scope": { + "COD_ORIGEM_MERCADORIA|origin|GROUP": 526, + "COD_ENTRADA_PIS|pis_cofins_profile|COMPANY": 526, + "COD_ENTRADA_COFINS|pis_cofins_profile|COMPANY": 526, + "COD_SAIDA_PIS|pis_cofins_profile|COMPANY": 525, + "ALIQ_PIS|pis_cofins_profile|COMPANY": 525, + "COD_SAIDA_COFINS|pis_cofins_profile|COMPANY": 525, + "ALIQ_COFINS|pis_cofins_profile|COMPANY": 525, + "CFOP_ENTRADA|product_role|GROUP": 253, + "CFOP_ENTRADA|entry_scope|COMPANY": 247, + "IPI|store_ipi_profile|COMPANY": 107, + "IPI|ex_tipi|PRODUCT": 76, + "CLASSIFICACAO_FISCAL|ncm|PRODUCT": 27, + "IPI|ncm|PRODUCT": 27, + "EX_TIPI|ncm|PRODUCT": 27, + "CEST|ncm|PRODUCT": 27, + "SITUACAO_TRIBUTARIA|ncm|PRODUCT": 27, + "CFOP_ENTRADA|ncm|PRODUCT": 27, + "CSOSN|ncm|PRODUCT": 27, + "CFOP|ncm|PRODUCT": 21, + "CEST|cest|PRODUCT": 14, + "CSOSN|icms_treatment|GROUP": 11, + "CFOP|st_status|GROUP": 7, + "SITUACAO_TRIBUTARIA|icms_treatment|GROUP": 1 + } +} \ No newline at end of file diff --git a/release/RELEASE_MANIFEST.json b/release/RELEASE_MANIFEST.json index e6a8889..7d5a67e 100644 --- a/release/RELEASE_MANIFEST.json +++ b/release/RELEASE_MANIFEST.json @@ -1,14 +1,14 @@ { "artifact": "RELEASE_MANIFEST", - "GTI_FISCAL_VERSION": "1.3.9", - "generated_at": "2026-09-28T01:15:00+00:00", + "GTI_FISCAL_VERSION": "1.3.10", + "generated_at": "2026-09-28T12:00:00+00:00", "hub": { - "version": "1.3.9", + "version": "1.3.10", "api_compat": "1.0", "engine_version": "0.3.0", "decision_version": "mission-04c-ipi-ex-split" }, - "agent_version": "1.3.9", + "agent_version": "1.3.8", "fiscal_engine": { "ENGINE_VERSION": "0.3.0", "DECISION_VERSION": "mission-04c-ipi-ex-split", @@ -89,5 +89,5 @@ "record_count": 11103 } ], - "note": "1.3.9: adds the ADMIN-only Contexto Fiscal section to the Agent detail page (central/app/fiscal_context_ui.py) - confirm/alter/clear the 3 real fiscal decisions field_contract cannot derive alone (icms_treatment/pis_cofins_profile/entry_scope, reduced from an original 5-item list by auditing real production data: icms_rate/credit_rate and rounding were proven NOT_APPLICABLE/ALREADY_DERIVED for this CRT=1 company and dropped). Persists through the existing FiscalContextEntry table, CSRF+RBAC+audit reusing the same mechanism as /agents/{id}/features. No schema change. Hub only - Agent unchanged, no new installer published this release." + "note": "1.3.9: adds the ADMIN-only Contexto Fiscal section to the Agent detail page (central/app/fiscal_context_ui.py) - confirm/alter/clear the 3 real fiscal decisions field_contract cannot derive alone (icms_treatment/pis_cofins_profile/entry_scope, reduced from an original 5-item list by auditing real production data: icms_rate/credit_rate and rounding were proven NOT_APPLICABLE/ALREADY_DERIVED for this CRT=1 company and dropped). Persists through the existing FiscalContextEntry table, CSRF+RBAC+audit reusing the same mechanism as /agents/{id}/features. No schema change. Hub only - Agent unchanged, no new installer published this release. 1.3.10 (Missao 2 - fechamento de lacunas fiscais, read-only + UI/engine gated): PIS/COFINS's pis_cofins_profile context key now also accepts the literal sentinels NOT_APPLICABLE/OPERATION_TIME (central/app/context_keys.py + central/app/field_contract.py), resolving to NA only when explicitly recorded by an audited FiscalContextEntry - never inferred from emission statistics; sem decisao ADMIN, comportamento identico ao anterior (regression tests in central/tests/test_field_contract.py). CFOP_ENTRADA under an already-decided entry_scope=MIXED_OR_DYNAMIC now resolves to NA instead of MANUAL, so it no longer blocks product completeness or re-asks an already-answered question (same file, same gate, existing scope-required-first test unchanged). Contexto Fiscal UI: PIS/COFINS and CFOP_ENTRADA cards show the Bloco 2 cross-database evidence and a link to docs/dossie_contador.md (also published at central/static/docs/dossie_contador.md); entry_scope is now a dropdown of its 3 valid values. New read-only evidence artifacts: docs/runs/GTI_dryrun_universe_summary.json, docs/runs/BLOCO2_PIS_COFINS_CROSS_DB_V1.json, docs/fiscal_completeness_matrix.md, docs/dossie_contador.md, docs/AGENT2_UPGRADE_RUNBOOK_V1.md. No schema/migration change. No Firebird write anywhere (zero writes, confirmed by construction - the new dry-run scripts never open a write connection). Hub only - Agent code untouched; Agent 2 was upgraded 1.3.7->1.3.8 by a human operator on-site (no self-update mechanism exists - confirmed by code audit), no new Agent installer published this release." } \ No newline at end of file diff --git a/release/VERSION b/release/VERSION index d4c4950..0c00f61 100644 --- a/release/VERSION +++ b/release/VERSION @@ -1 +1 @@ -1.3.9 +1.3.10 diff --git a/scripts/bloco2_pis_cofins_cross_db.py b/scripts/bloco2_pis_cofins_cross_db.py new file mode 100644 index 0000000..7f205c3 --- /dev/null +++ b/scripts/bloco2_pis_cofins_cross_db.py @@ -0,0 +1,95 @@ +"""Missão 2 / Bloco 2.1 — evidência cruzada PIS/COFINS cadastro-vs-emissão, somente leitura, nos 6 bancos. +Nunca escreve. Não decide regra fiscal: só conta e cruza fatos. +""" +import json +import os +import sys +from pathlib import Path +from collections import Counter + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts")); sys.path.insert(0, str(ROOT / "agent" / "src")) +import fdb # noqa: E402 +import run_fiscal_decision_v1 as base # noqa: E402 +from gti_fiscal_agent import company_context as cc # noqa: E402 + +DBS = { + "GTI": (r"D:\PROJETOS\GTISISTEMAS\Banco\GTI.FDB", {}), + "TESTESOFICIAIS": (str(ROOT / "TESTESOFICIAIS.FDB"), {}), + "CONSTRUCAO": (str(ROOT / "Banco" / "CONSTRUCAO.FDB"), {"company_code": "000001"}), + "MMMODAS": (str(ROOT / "Banco" / "MMMODAS.FDB"), {"company_code": "000001", "crt_source": "EMISSOR"}), + "RINCAO": (str(ROOT / "Banco" / "RINCAO.FDB"), {"company_code": "000001", "crt_source": "EMISSOR"}), + "UNIAO": (str(ROOT / "Banco" / "UNIAO.FDB"), {"company_code": "000001", "crt_source": "EMISSOR"}), +} + + +def connect(path): + return fdb.connect(database=path, user=os.environ["FB_USER"], password=os.environ["FB_PASSWORD"], + fb_library_name=os.environ.get("FIREBIRD_CLIENT_LIBRARY"), isolation_level=fdb.ISOLATION_LEVEL_READ_COMMITED_RO) + + +def analyze_one(name, path, cfg): + conn = connect(path); cur = conn.cursor() + company = cc.resolve(cc.collect_evidence(cur), cc.config_from_env({("COMPANY_CODE" if k == "company_code" else "COMPANY_CRT_SOURCE"): v for k, v in cfg.items()})) + out = {"name": name, "company_status": company["status"], "crt": company.get("company_crt"), "uf": company.get("company_uf")} + try: + cur.execute("SELECT COUNT(*), COUNT(NULLIF(TRIM(CST_PIS_COFINS),'')) FROM NFCE_ITENS") + tot, nonblank = cur.fetchone() + out["items_total"] = tot; out["items_cst_pis_cofins_filled"] = nonblank + out["pct_cst_filled"] = round(100.0 * nonblank / tot, 2) if tot else None + except Exception as e: # noqa: BLE001 + out["items_error"] = str(e)[:150]; conn.close(); return out + cur.execute("SELECT TRIM(CST_PIS_COFINS), COUNT(*) FROM NFCE_ITENS WHERE CST_PIS_COFINS IS NOT NULL GROUP BY 1 ORDER BY 2 DESC") + out["cst_distribution"] = {(r[0] or "(blank)"): r[1] for r in cur.fetchall()} + try: + cur.execute("SELECT EXTRACT(YEAR FROM DATA), COUNT(*), COUNT(NULLIF(TRIM(CST_PIS_COFINS),'')) FROM NFCE_ITENS GROUP BY 1 ORDER BY 1") + out["by_year"] = {int(y): {"items": t, "cst_filled": f} for y, t, f in cur.fetchall() if y is not None} + except Exception as e: # noqa: BLE001 + out["by_year_error"] = str(e)[:150] + try: + cur.execute("SELECT ROUND(ALIQUOTA_PIS,4), COUNT(*) FROM NFCE_ITENS WHERE ALIQUOTA_PIS IS NOT NULL GROUP BY 1 ORDER BY 2 DESC") + out["aliquota_pis_distribution"] = {str(r[0]): r[1] for r in cur.fetchmany(10)} + cur.execute("SELECT ROUND(ALIQUOTA_COFINS,4), COUNT(*) FROM NFCE_ITENS WHERE ALIQUOTA_COFINS IS NOT NULL GROUP BY 1 ORDER BY 2 DESC") + out["aliquota_cofins_distribution"] = {str(r[0]): r[1] for r in cur.fetchmany(10)} + except Exception as e: # noqa: BLE001 + out["aliquota_error"] = str(e)[:150] + # cadastro vs emissão, apenas nos itens cujo NCM emitido == NCM cadastrado (join identity-consistent, evita o viés de 2022-12 já documentado) + try: + cur.execute(""" + SELECT COUNT(*), + SUM(CASE WHEN TRIM(COALESCE(p.CLASSIFICACAO_FISCAL,'')) != '' AND p.CLASSIFICACAO_FISCAL = i.NCM THEN 1 ELSE 0 END) + FROM NFCE_ITENS i JOIN C000025 p ON p.CODIGO = i.CODPRODUTO + """) + joined_tot, identity_consistent = cur.fetchone() + out["identity_consistent_items"] = identity_consistent + cur.execute(""" + SELECT COUNT(*) + FROM NFCE_ITENS i JOIN C000025 p ON p.CODIGO = i.CODPRODUTO + WHERE p.CLASSIFICACAO_FISCAL = i.NCM + AND COALESCE(TRIM(p.CST_ENTRADA),'') = '' -- produto sem CST/PIS cadastral (a maioria) + AND COALESCE(TRIM(i.CST_PIS_COFINS),'') != '' + """) + out["emitted_cst_present_when_cadastro_blank"] = cur.fetchone()[0] + except Exception as e: # noqa: BLE001 + out["cadastro_vs_emissao_error"] = str(e)[:150] + conn.close() + return out + + +def main(): + base.load_dotenv(ROOT / ".env") + results = {} + for name, (path, cfg) in DBS.items(): + print(f"== {name}", flush=True) + try: + results[name] = analyze_one(name, path, cfg) + except Exception as e: # noqa: BLE001 + results[name] = {"name": name, "error": str(e)[:300]} + print(json.dumps(results[name], ensure_ascii=False, indent=1, default=str)[:800], flush=True) + out_path = ROOT / "docs" / "runs" / "BLOCO2_PIS_COFINS_CROSS_DB_V1.json" + out_path.write_text(json.dumps(results, ensure_ascii=False, indent=1, default=str), encoding="utf-8") + print(f"-> {out_path}") + + +if __name__ == "__main__": + main() diff --git a/scripts/dry_run_universe.py b/scripts/dry_run_universe.py new file mode 100644 index 0000000..41fbf0e --- /dev/null +++ b/scripts/dry_run_universe.py @@ -0,0 +1,66 @@ +"""Missão 2 / Bloco 1 — dry-run fiscal read-only sobre um banco arbitrário (Agent 2 = GTI.FDB incluso). + +Reaproveita o motor puro (central.app.field_contract) e as funções analyze()/summarize() +já existentes em scripts/local_completion.py (mesma taxonomia ALREADY_CORRECT/CHANGE_REQUIRED/ +NOT_APPLICABLE/MANUAL_INPUT_REQUIRED). NUNCA chama apply()/execute_guarded_product_update — +este script não importa firebird_write_executor e não abre conexão rw=True em nenhum ponto. + + python scripts/dry_run_universe.py --name GTI --path "D:\\PROJETOS\\GTISISTEMAS\\Banco\\GTI.FDB" [--company-code C] [--crt-source AUTO|EMISSOR|C000004] +""" +import argparse +import json +import os +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts")); sys.path.insert(0, str(ROOT / "agent" / "src")) +import run_fiscal_decision_v1 as base # noqa: E402 +import local_completion as lc # noqa: E402 + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--name", required=True) + ap.add_argument("--path", required=True) + ap.add_argument("--company-code") + ap.add_argument("--crt-source") + ap.add_argument("--out") + a = ap.parse_args() + base.load_dotenv(ROOT / ".env") + token = os.environ["AGENT_DEV_TOKEN"] + if a.company_code or a.crt_source: + lc.COMPANY_CFG[a.name] = {k: v for k, v in (("company_code", a.company_code), ("crt_source", a.crt_source)) if v} + an = lc.analyze(a.name, a.path, token) + summary = lc.summarize(an) + from collections import Counter + scope_by_key = Counter() + for res in an["results"].values(): + for fld, r in res.items(): + if fld.startswith("_"): + continue + if r.get("state") == "MANUAL_INPUT_REQUIRED": + scope_by_key[(fld, r.get("input_key"), r.get("input_scope"))] += 1 + summary["manual_inputs_by_scope"] = {f"{f}|{k}|{s}": n for (f, k, s), n in sorted(scope_by_key.items(), key=lambda x: -x[1])} + out_path = Path(a.out) if a.out else (ROOT / "docs" / "runs" / f"{a.name}_dryrun_universe_summary.json") + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(json.dumps(summary, ensure_ascii=False, indent=1, default=str), encoding="utf-8") + print(json.dumps({"name": a.name, "total_products": summary["total_products"], "company": summary["company"], + "change_required_cells": summary["change_required_cells"], "write_eligible_cells": summary["write_eligible_cells"]}, indent=1, default=str)) + ndjson_path = ROOT / "docs" / "runs" / f"{a.name}_dryrun_universe.ndjson.gz" + import gzip + with gzip.open(ndjson_path, "wt", encoding="utf-8") as f: + from central.app.field_contract import WRITE_FIELDS, CONTRACT_VERSION + for p in an["products"]: + code = p["_rk"]; res = an["results"][code] + for fld in WRITE_FIELDS: + r = res[fld] + f.write(json.dumps({"db": a.name, "codigo": p["CODIGO"], "produto": p["PRODUTO"], "field": fld, "current": r["current"], "proposed": r["proposed"], + "status": r["state"], "reason": r["reason"], "input_key": r.get("input_key"), + "provenance": f"{CONTRACT_VERSION}|" + ",".join(f"{k}:{v}" for k, v in res["_ds"].items()) + f"|role={res['_facts']['role']}"}, ensure_ascii=False, default=str) + "\n") + print(f"summary -> {out_path}") + print(f"cells -> {ndjson_path}") + + +if __name__ == "__main__": + main() diff --git a/tests/test_release_candidate.py b/tests/test_release_candidate.py index 5cee719..465a9b4 100644 --- a/tests/test_release_candidate.py +++ b/tests/test_release_candidate.py @@ -11,20 +11,34 @@ ROOT = Path(__file__).parents[1] sys.path.insert(0, str(ROOT)) -def test_versions_are_coherent_across_hub_agent_and_packages(): +def test_versions_are_coherent_across_hub_and_packages(): + """GTI_FISCAL_VERSION is the Hub's own single source (release/VERSION, central/app/version.py, + central/pyproject.toml, the manifest). It is deliberately NOT required to match the Agent's own + version (see test_agent_version_is_internally_coherent below) - a Hub-only release (Missão 2/3) + must be able to bump this without touching, rebuilding or republishing the Agent at all.""" from central.app.version import GTI_FISCAL_VERSION v = (ROOT / "release" / "VERSION").read_text(encoding="utf-8").strip() - assert v == GTI_FISCAL_VERSION == "1.3.9" + assert v == GTI_FISCAL_VERSION == "1.3.10" pep = v.replace("-rc", "rc") - for pp in ("central/pyproject.toml", "agent/pyproject.toml"): - assert f'version = "{pep}"' in (ROOT / pp).read_text(encoding="utf-8") - assert '"AGENT_VERSION", "1.3.9"' in (ROOT / "agent" / "scripts" / "run_agent_service.py").read_text(encoding="utf-8") + assert f'version = "{pep}"' in (ROOT / "central" / "pyproject.toml").read_text(encoding="utf-8") manifest = ROOT / "release" / "RELEASE_MANIFEST.json" if manifest.exists(): m = json.loads(manifest.read_text(encoding="utf-8")) assert m["GTI_FISCAL_VERSION"] == v and m["migrations"]["alembic_head_in_repo"] == m["migrations"]["applied_revision"] +def test_agent_version_is_internally_coherent(): + """The Agent's own version references must agree with EACH OTHER (never guessed, never silently + drifting), but are an independent number from the Hub's GTI_FISCAL_VERSION - the Agent is only + ever re-versioned when its own code/behavior changes and a new installer is actually built.""" + agent_pep = (ROOT / "agent" / "pyproject.toml").read_text(encoding="utf-8") + m = re.search(r'version = "([^"]+)"', agent_pep) + assert m, "agent/pyproject.toml must declare a version" + agent_version = m.group(1) + assert f'"AGENT_VERSION", "{agent_version}"' in (ROOT / "agent" / "scripts" / "run_agent_service.py").read_text(encoding="utf-8") + assert f'AGENT_VERSION_DEFAULT = "{agent_version}"' in (ROOT / "agent" / "src" / "gti_fiscal_agent" / "preflight.py").read_text(encoding="utf-8") + + def test_manifest_records_the_real_migration_head(): # The manifest said "0011" while the repository was already at 0018: the # generator used the 4-digit file prefix and nothing compared it to Alembic.