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:
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user