feat: audited-decision gate for PIS/COFINS and CFOP_ENTRADA NOT_APPLICABLE/OPERATION_TIME

Missao 2 (fechamento de lacunas fiscais, read-only + UI/engine gated):
- field_contract: pis_cofins_profile now also accepts the literal sentinels
  NOT_APPLICABLE/OPERATION_TIME, resolving to NA only from an audited
  FiscalContextEntry - never inferred from emission statistics. CFOP_ENTRADA
  under an already-decided entry_scope=MIXED_OR_DYNAMIC now resolves to NA
  instead of MANUAL, so it stops blocking product completeness and stops
  re-asking an already-answered question. Regression tests prove: no ADMIN
  decision => unchanged old behavior; decision present => gated bucket, no write.
- context_keys: validate_entry accepts the two sentinels for pis_cofins_profile.
- Contexto Fiscal UI: PIS/COFINS and CFOP_ENTRADA cards show the Bloco 2
  cross-database evidence (5 installations, CRT 1 and 3: emission never reads
  the PIS/COFINS cadastral fields anywhere) and a link to an ADMIN-facing,
  anonymized dossie-for-the-accountant; entry_scope is now a dropdown.
- release/tests: decoupled the Hub's GTI_FISCAL_VERSION from the Agent's own
  version (test_versions_are_coherent_across_hub_and_packages vs the new
  test_agent_version_is_internally_coherent) - a Hub-only release must not
  require touching, rebuilding or republishing the Agent.
- New read-only evidence: docs/runs/GTI_dryrun_universe_summary.json (527
  products, Agent 2's real database), docs/runs/BLOCO2_PIS_COFINS_CROSS_DB_V1.json
  (5 installations), docs/fiscal_completeness_matrix.md, docs/dossie_contador.md
  (internal, real names) + central/static/docs/dossie_contador.md (public,
  anonymized - no CNPJ/razao social/product names), docs/AGENT2_UPGRADE_RUNBOOK_V1.md.
- CLAUDE.md: recorded the permanent fiscal-mission rules (no AI/scraping, no
  Firebird schema changes, deploy only via deploy_hub.ps1, never reenroll,
  no remote access to client Agent hosts, no client data in unauthenticated
  static routes).

No schema/migration change. Zero Firebird writes (new scripts never open a
write connection). Hub only - Agent code untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-28 09:44:05 -03:00
co-authored by Claude Sonnet 5
parent 1621ea4e0b
commit e935ed61d5
21 changed files with 1066 additions and 35 deletions
+30
View File
@@ -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.
+4 -2
View File
@@ -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
+21 -8
View File
@@ -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]:
+16 -8
View File
@@ -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",
},
]
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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",
+68
View File
@@ -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.
+2 -2
View File
@@ -14,11 +14,11 @@
{% for d in fiscal_context %}<article class="panel" style="margin-top:12px"><div class="panel-title"><h3>{{ d.title }}</h3><span class="badge">Escopo: {{ {"COMPANY":"Empresa","GROUP":"Grupo","PRODUCT":"Produto"}[d.scope] }}</span></div>
<dl class="facts"><dt>Valor atual</dt><dd>{{ d.current_value or "Não confirmado" }}{% if d.last_changed %} <small class="muted">· alterado em {{ d.last_changed }} por {{ d.last_changed_by }}</small>{% endif %}</dd>
<dt>Sugestão</dt><dd>{{ d.suggestion or "Sem sugestão automática — evidência insuficiente" }}</dd>
<dt>Evidência</dt><dd>{{ d.evidence }} <small class="muted">(medido manualmente em {{ d.measured_at }} — não é uma consulta ao vivo)</small></dd>
<dt>Evidência</dt><dd>{{ d.evidence }} <small class="muted">(medido manualmente em {{ d.measured_at }} — não é uma consulta ao vivo){% if d.dossier_link %} · <a href="{{ d.dossier_link }}" target="_blank" rel="noopener">dossiê para o contador</a>{% endif %}</small></dd>
<dt>Confiança</dt><dd>{{ d.confidence }}</dd>
<dt>Campos afetados</dt><dd>{{ d.affected_fields|join(", ") }}</dd></dl>
<form method="post" action="/agents/{{ detail.agent_id }}/fiscal-context" class="filters compact-filters"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="key" value="{{ d.key }}">
{% if d.allowed_values %}<label>Valor<select name="value">{% for v in d.allowed_values %}<option value="{{ v }}" {% if d.current_value==v %}selected{% endif %}>{{ v }}</option>{% endfor %}</select></label>{% else %}<label>Valor<input name="value" value="{{ d.current_value or '' }}" placeholder="ex.: {01,01,1.65,01,01,7.6} ou INTRA"></label>{% endif %}
{% if d.allowed_values %}<label>Valor<select name="value">{% for v in d.allowed_values %}<option value="{{ v }}" {% if d.current_value==v %}selected{% endif %}>{{ v }}</option>{% endfor %}</select></label>{% else %}<label>Valor<input name="value" value="{{ d.current_value or '' }}" placeholder="{{ d.value_hint or 'ex.: {01,01,1.65,01,01,7.6} ou INTRA' }}"></label>{% endif %}
<label>Motivo<input name="reason" placeholder="opcional" autocomplete="off"></label>
<button class="secondary" type="submit" name="fc_action" value="confirm">Confirmar</button>
<button class="secondary" type="submit" name="fc_action" value="clear" onclick="return confirm('Limpar esta decisão? Os produtos voltam a depender só do que o ERP prova.')">Limpar</button></form>
+27 -1
View File
@@ -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
+5
View File
@@ -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():
+1 -1
View File
@@ -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):
+46
View File
@@ -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 `<pasta_instalação>\agent\agent_state.sqlite` para um local seguro
(ex.: `agent_state.sqlite.bak-2026-09-28`).
- Copie também `<pasta_instalação>\.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.
+73
View File
@@ -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.
+44
View File
@@ -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`.
@@ -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
}
}
+277
View File
@@ -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
}
}
+5 -5
View File
@@ -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."
}
+1 -1
View File
@@ -1 +1 @@
1.3.9
1.3.10
+95
View File
@@ -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()
+66
View File
@@ -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()
+19 -5
View File
@@ -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.