chore: establish project baseline

This commit is contained in:
OpenCode
2026-09-21 15:51:47 -03:00
commit 9dcc263217
459 changed files with 447890 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
"""Read-only historical fiscal analysis over TESTESOFICIAIS.FDB: correlates emitted
NFC-e items (NFCE_ITENS) and sale/purchase item tables with C000025 product defaults.
No DML/DDL. Output: docs/runs/erp_fiscal_history_analysis.json
"""
import json, os, sys
from collections import Counter, defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src"))
from gti_fiscal_agent.firebird_reader import _connect # noqa: E402
for l in (ROOT / ".env").read_text(encoding="utf-8").splitlines():
if "=" in l and not l.startswith("#"):
k, _, v = l.partition("="); os.environ.setdefault(k.strip(), v.strip())
def s(v):
return None if v is None else str(v).strip()
def digits(v):
return "".join(c for c in str(v or "") if c.isdigit())
def main():
conn = _connect(os.environ); cur = conn.cursor()
cur.execute("select CODIGO, CFOP, CST, CSOSN, GRUPO_ICMS, CEST, CLASSIFICACAO_FISCAL, COD_SAIDA_PIS, INCIDENCIA_PISCOFINS, ALIQUOTA from C000025")
prod = {s(r[0]): dict(zip(("CFOP", "CST", "CSOSN", "GRUPO_ICMS", "CEST", "NCM", "PIS", "INC", "ALIQ"), map(s, r[1:]))) for r in cur.fetchall()}
res = {"products": len(prod)}
cur.execute("select min(DATA), max(DATA), count(*) from NFCE_ITENS"); mn, mx, n = cur.fetchone()
res["nfce_itens"] = {"rows": n, "date_min": str(mn), "date_max": str(mx)}
cur.execute("select count(*), min(DATA), max(DATA) from NFCE"); r = cur.fetchone(); res["nfce_docs"] = {"rows": r[0], "min": str(r[1]), "max": str(r[2])}
cur.execute("select CODPRODUTO, CFOP, CST, GRUPO_ICMS, CSOSN, NCM, CEST, CST_PIS_COFINS, ALIQUOTA, VALOR from NFCE_ITENS")
rows = cur.fetchall()
joined = 0; agree = defaultdict(Counter); per_prod = defaultdict(lambda: defaultdict(Counter)); orphan = 0
for cod, cfop, cst, gicms, csosn, ncm, cest, pispc, aliq, val in rows:
p = prod.get(s(cod))
if p is None:
orphan += 1; continue
joined += 1
item = {"CFOP": s(cfop), "CST": s(cst), "GRUPO_ICMS": s(gicms), "CSOSN": s(csosn), "NCM": s(ncm), "CEST": s(cest), "PIS": s(pispc)}
for f, iv in item.items():
if iv is None:
agree[f]["ITEM_NULL"] += 1; continue
pv = p.get(f if f != "PIS" else "PIS")
if f == "CEST":
agree[f]["MATCH" if digits(iv) == digits(pv) else "MISMATCH"] += 1
elif f == "PIS":
agree[f]["MATCH" if iv.lstrip("0") == (pv or "").lstrip("0") else "MISMATCH"] += 1
else:
agree[f]["MATCH" if iv == pv else "MISMATCH"] += 1
per_prod[s(cod)][f][iv] += 1
res["nfce_join"] = {"items_joined_to_C000025": joined, "orphan_items": orphan,
"agreement_item_vs_product_default": {f: dict(c) for f, c in agree.items()}}
res["nfce_products_seen"] = len(per_prod)
res["variability"] = {f: sum(1 for pp in per_prod.values() if len(pp[f]) > 1) for f in ("CFOP", "CST", "GRUPO_ICMS", "CSOSN", "CEST", "NCM")}
res["nfce_item_value_distributions"] = {}
for f, idx in (("CFOP", 1), ("CST", 2), ("GRUPO_ICMS", 3), ("CSOSN", 4), ("CST_PIS_COFINS", 7)):
res["nfce_item_value_distributions"][f] = Counter(s(r[idx]) for r in rows).most_common(8)
# Sale/purchase item tables (C000032 sales, C000062 purchase items) vs product defaults
for table, extra in (("C000032", "CST, CFOP"), ("C000062", "CST, CFOP")):
cur.execute(f"select CODPRODUTO, {extra} from {table}")
c = Counter(); cfopc = Counter(); cstc = Counter()
for cod, cst, cfop in cur.fetchall():
p = prod.get(s(cod))
if p is None: c["ORPHAN"] += 1; continue
c["JOINED"] += 1
if s(cst): cstc["MATCH" if s(cst) == p["CST"] else "MISMATCH"] += 1
if s(cfop): cfopc["MATCH" if s(cfop) == p["CFOP"] else "MISMATCH"] += 1
res[table] = {"joined": dict(c), "cst_vs_product": dict(cstc), "cfop_vs_product": dict(cfopc)}
# Natureza de operação (C000030) and operation-context sources
cur.execute("select TIPO, count(*) from C000030 group by TIPO"); res["C000030_tipo"] = dict(cur.fetchall())
cur.execute("select substring(CFOP from 1 for 1), count(*) from C000030 group by 1 order by 1"); res["C000030_cfop_first_digit"] = dict(cur.fetchall())
cur.execute("select CFOP from C000030 where CFOP in ('5102','5405','5656','5929','6102','1102')"); res["C000030_has_cfops"] = [s(r[0]) for r in cur.fetchall()]
cur.execute("select IND_CFOP_ENTRADA, IND_CFOP_SAIDA, IND_CFOP_PERDA from C000001"); res["C000001_cfop_defaults"] = [s(x) for x in cur.fetchone()]
cur.execute("select OPTANTE_SIMPLES, OPTANTE_SUPER_SIMPLES, CRT, ICMS_CSOSN, SUBSTITUTO_TRIBUTARIO, CONTRIBUINTE_IPI, UF from C000004"); res["C000004"] = [s(x) for x in cur.fetchone()]
cur.execute("select EMITENTE_CRT from EMISSOR"); res["EMISSOR_CRT"] = [s(x) for x in cur.fetchone()]
cur.execute("select CFOP, count(*) from C000062 group by CFOP"); res["C000062_cfop"] = {s(a): b for a, b in cur.fetchall()}
cur.execute("select CST, count(*) from C000062 group by CST"); res["C000062_cst"] = {s(a): b for a, b in cur.fetchall()}
cur.execute("select count(*), sum(case when MARGEM_AGREGADA <> 0 then 1 else 0 end), sum(case when ICMS_SUB <> 0 then 1 else 0 end) from C000062"); res["C000062_mva_usage"] = list(cur.fetchone())
cur.execute("select count(*) from PRODUTOS_MARGEM_AGREGADA"); res["PRODUTOS_MARGEM_AGREGADA_rows"] = cur.fetchone()[0]
cur.execute("select count(*) from C000025 where MARGEM_AGREGADA is not null"); res["C000025_MARGEM_AGREGADA_nonnull"] = cur.fetchone()[0]
# CEST history for the 15 CEST-candidate products (from the decision matrix)
matrix = json.loads((ROOT / "docs" / "FISCAL_DECISION_MATRIX_V1.json").read_text(encoding="utf-8"))
cand = [m for m in matrix["products"] if m["fields"]["CEST"]["status"] == "RULE_CANDIDATE"]
cest_hist = []
for m in cand:
code = s(m["CODIGO"])
hist = Counter(digits(k) for k, c in per_prod.get(code, {}).get("CEST", {}).items() for _ in range(c) if digits(k))
cest_hist.append({"CODIGO": code, "DESCRIPTION": m["DESCRIPTION"], "NCM": m["NCM"], "erp_current_cest": m["fields"]["CEST"]["current_value"], "historical_cest": dict(hist)})
res["cest_candidates_history"] = cest_hist
conn.close()
# Shadow validation: decision matrix engine value vs historical NFC-e item value
val = {}
for dom, hist_field in (("NCM", "NCM"), ("CEST", "CEST"), ("CFOP", "CFOP"), ("CST_ICMS", "CST"), ("CSOSN", "CSOSN"), ("PIS_CST", "PIS")):
val[dom] = Counter()
by_code = {s(m["CODIGO"]): m["fields"] for m in matrix["products"]}
conn = _connect(os.environ); cur = conn.cursor()
cur.execute("select first 5000 CODPRODUTO, NCM, CEST, CFOP, CST, CSOSN, CST_PIS_COFINS from NFCE_ITENS where NCM is not null")
n_sim = 0
for cod, ncm, cest, cfop, cst, csosn, pispc in cur.fetchall():
f = by_code.get(s(cod))
if f is None: continue
n_sim += 1
hist = {"NCM": s(ncm), "CEST": s(cest), "CFOP": s(cfop), "CST_ICMS": s(cst), "CSOSN": s(csosn), "PIS_CST": s(pispc)}
for dom, hv in hist.items():
rec = f[dom]
ev = rec["proposed_value"] if rec["status"] == "RULE_RESOLVED" else None
if rec["status"] == "RULE_RESOLVED" and dom == "NCM":
ev = rec["current_value"]
if ev is None:
val[dom]["UNRESOLVED"] += 1
elif digits(ev) == digits(hv) if dom in ("NCM", "CEST") else str(ev) == str(hv):
val[dom]["MATCH"] += 1
else:
val[dom]["MISMATCH"] += 1
res["shadow_validation"] = {"items": n_sim, **{d: dict(c) for d, c in val.items()}}
conn.close()
print(json.dumps(res, ensure_ascii=False, indent=1, default=str))
main()
@@ -0,0 +1,61 @@
"""Read-only extras: (1) history agreement restricted to identity-consistent joins (item NCM == product NCM),
(2) CEST historical support for the CEST-candidate products against the Hub's official candidates."""
import json, os, sys, urllib.request
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src"))
from gti_fiscal_agent.firebird_reader import _connect
for l in (ROOT / ".env").read_text(encoding="utf-8").splitlines():
if "=" in l and not l.startswith("#"):
k, _, v = l.partition("="); os.environ.setdefault(k.strip(), v.strip())
s = lambda v: None if v is None else str(v).strip()
dg = lambda v: "".join(c for c in str(v or "") if c.isdigit())
c = _connect(os.environ).cursor()
c.execute("select CODIGO, PRODUTO, CLASSIFICACAO_FISCAL, CFOP, CST, CSOSN, CEST, COD_SAIDA_PIS from C000025")
P = {s(r[0]): dict(zip(("D", "NCM", "CFOP", "CST", "CSOSN", "CEST", "PIS"), map(s, r[1:]))) for r in c.fetchall()}
c.execute("select CODPRODUTO, NCM, CFOP, CST, CSOSN, CEST, CST_PIS_COFINS, GRUPO_ICMS from NFCE_ITENS where NCM is not null")
consistent = Counter(); inconsistent = 0; agree = {f: Counter() for f in ("CFOP", "CST", "CSOSN", "CEST", "PIS")}
hist_cest = {}
for cod, ncm, cfop, cst, csosn, cest, pis, gi in c.fetchall():
p = P.get(s(cod))
if not p: continue
if s(ncm) != p["NCM"]:
inconsistent += 1
else:
consistent["items"] += 1
for f, iv in (("CFOP", s(cfop)), ("CST", s(cst)), ("CSOSN", s(csosn)), ("CEST", s(cest)), ("PIS", s(pis))):
pv = p[f]
if iv is None: continue
eq = dg(iv) == dg(pv) if f == "CEST" else (iv.lstrip("0") == (pv or "").lstrip("0") if f == "PIS" else iv == pv)
agree[f]["MATCH" if eq else "MISMATCH"] += 1
if dg(cest):
hist_cest.setdefault(s(cod), Counter())[dg(cest)] += 1
out = {"ncm_identity_consistent_items": consistent["items"], "ncm_identity_inconsistent_items": inconsistent,
"agreement_consistent_joins_only": {f: dict(v) for f, v in agree.items()}}
# also all-time (any NCM) CEST history per product
c.execute("select CODPRODUTO, CEST from NFCE_ITENS where CEST is not null and trim(CEST) <> ''")
allc = {}
for cod, cest in c.fetchall():
allc.setdefault(s(cod), Counter())[dg(cest)] += 1
tok = [l.split("=", 1)[1].strip() for l in open(ROOT / ".env", encoding="utf-8") if l.startswith("AGENT_DEV_TOKEN=")][0]
matrix = json.loads((ROOT / "docs" / "FISCAL_DECISION_MATRIX_V1.json").read_text(encoding="utf-8"))
cands = [m for m in matrix["products"] if m["fields"]["CEST"]["status"] == "RULE_CANDIDATE"]
req = urllib.request.Request("http://localhost:8000/v1/classify/batch", data=json.dumps({"products": [{"current_ncm": m["NCM"], "description": m["DESCRIPTION"] or ""} for m in cands]}).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + tok})
decs = json.load(urllib.request.urlopen(req, timeout=600))["results"]
rows = []
for m, d in zip(cands, decs):
code = s(m["CODIGO"]); off = {dg(x["code"]): x for x in d["classification"].get("cest_candidates", [])}
h = dict(allc.get(code, {}))
supported = [k for k in h if k in off]
mva_ceps = {dg(x.get("cest")) for x in d["classification"].get("ba_st_mva_candidates", [])}
rows.append({"CODIGO": code, "DESCRIPTION": m["DESCRIPTION"], "NCM": m["NCM"], "official_candidates": len(off), "historical_cest": h,
"historical_in_official_candidates": supported, "historical_in_ba_st_mva": [k for k in h if k in mva_ceps],
"product_ncm_matches_item_ncm": None,
"verdict": "CEST_HISTORICALLY_SUPPORTED" if len(h) == 1 and len(supported) == 1 else ("HISTORY_MULTIPLE_OR_UNSUPPORTED" if h else "NO_HISTORY")})
out["cest_candidates"] = rows
out["cest_historically_supported"] = sum(1 for r in rows if r["verdict"] == "CEST_HISTORICALLY_SUPPORTED")
print(json.dumps(out, ensure_ascii=True, indent=1))
+85
View File
@@ -0,0 +1,85 @@
"""READ-ONLY company-context audit + resolver retest over every .FDB in a directory (default ./Banco).
Writes docs/COMPANY_CONTEXT_RESOLUTION_V1.json (evidence) and docs/MULTI_DATABASE_COMPANY_CONTEXT_V1.json (outcomes).
The full CNPJ is never written: masked value + short SHA-256 fingerprint only.
"""
import json
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src"))
import fdb # noqa: E402
from gti_fiscal_agent import company_context as cc # noqa: E402
for line in (ROOT / ".env").read_text(encoding="utf-8").splitlines():
if "=" in line and not line.startswith("#"):
k, _, v = line.partition("=")
os.environ.setdefault(k.strip(), v.strip())
def evidence_for(path):
conn = fdb.connect(database=str(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)
try:
return cc.collect_evidence(conn.cursor())
finally:
conn.close()
def table(ev):
rows = []
emissor_cnpjs = {cc.digits(e["cnpj"]) for e in ev["emissor"]}
docs_by_cnpj = {}
for d in ev["nfce_keys"]:
docs_by_cnpj[cc.digits(d["cnpj"])] = docs_by_cnpj.get(cc.digits(d["cnpj"]), 0) + d["n"]
for i, c in enumerate(ev["c000004"]):
cn = cc.digits(c.get("cnpj"))
em = next((e for e in ev["emissor"] if cc.digits(e["cnpj"]) == cn), None)
n_docs = docs_by_cnpj.get(cn, 0); n_fil = ev["nf_by_filial"].get(c.get("code"), 0)
active = bool(em) and n_docs > 0
rows.append({"C000004_CODE": c.get("code"), "CNPJ": cc.mask_cnpj(cn), "CNPJ_FINGERPRINT": cc.cnpj_fingerprint(cn), "UF_C000004": c.get("uf"), "CRT_C000004": c.get("crt"), "OPTANTE_SIMPLES": c.get("optante"),
"EMISSOR_ID": (ev["emissor"].index(em) + 1) if em else None, "UF_EMISSOR": em.get("uf") if em else None, "CRT_EMISSOR": em.get("crt") if em else None,
"DOCUMENT_COUNT": {"nfce_keys_with_this_cnpj": n_docs, "nf_C000061_with_this_CODFILIAL": n_fil}, "IS_ACTIVE_CANDIDATE": active,
"EVIDENCE": [x for x in (("EMISSOR.EMITENTE_CNPJ equals this CNPJ" if em else "no EMISSOR row with this CNPJ"), (f"{n_docs} emitted NFC-e keys carry this CNPJ" if n_docs else "no emitted NFC-e key carries this CNPJ"),
(f"{n_fil} NF-e rows use CODFILIAL={c.get('code')}" if n_fil else None)) if x]})
return rows
def main():
directory = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / "Banco"
evid, outcomes = {}, {}
for p in sorted(directory.glob("*.FDB")):
ev = evidence_for(p)
rows = table(ev)
no_cfg = cc.resolve(ev, {})
crt_cfg = cc.resolve(ev, {"crt_source": "EMISSOR"})
multi = len(ev["c000004"]) > 1
neg = {}
if multi: # negative tests on real data: wrong company / unknown CNPJ must not resolve silently
other = next((c for c in ev["c000004"] if not next((e for e in ev["emissor"] if cc.digits(e["cnpj"]) == cc.digits(c["cnpj"])), None)), None)
neg["configured_non_emitting_company"] = cc.resolve(ev, {"company_code": other["code"], "crt_source": "EMISSOR"})["classification"] if other else None
neg["configured_unknown_cnpj"] = cc.resolve(ev, {"company_cnpj": "00000000000000"})["classification"]
crt_authority = {"EMISSOR_values": sorted({e.get("crt") for e in ev["emissor"] if e.get("crt")}), "C000004_selected_row": no_cfg.get("crt_candidates") or {"C000004": no_cfg.get("company_crt")}}
evid[p.stem] = {"c000004_rows": len(ev["c000004"]), "emissor_rows": len(ev["emissor"]), "companies": rows, "nfce_key_groups": [{"cUF": d["cuf"], "cnpj": cc.mask_cnpj(d["cnpj"]), "model": "65", "n": d["n"]} for d in ev["nfce_keys"]],
"nf_by_filial": ev["nf_by_filial"]}
outcomes[p.stem] = {"without_config": cc.preflight_report(no_cfg), "with_COMPANY_CRT_SOURCE=EMISSOR": cc.preflight_report(crt_cfg), "negative_tests": neg,
"crt_source_status": no_cfg.get("crt_status") or ("CRT_SOURCE_CONFLICT" if "CRT_SOURCE_CONFLICT" in " ".join(no_cfg["reasons"]) else "CRT_SOURCE_UNKNOWN"),
"uf_source_status": no_cfg.get("uf_status") or "UF_SOURCE_UNKNOWN", "resolution_class": no_cfg["classification"]}
classes = {}
for k, v in outcomes.items():
classes.setdefault(v["resolution_class"], []).append(k)
authority = {
"CRT": {"emission_authority": "EMISSOR.EMITENTE_CRT", "evidence": ["GtiVendas_NFCe.exe (NFC-e) and Envio NF-e.exe (NF-e) bind QrEmissor.EMITENTE_CRT/EMITENTE_UF/EMITENTE_CNPJ and never reference C000004.CRT or OPTANTE_SIMPLES",
"SGV_5.0.6.124.exe (ERP) binds C000004.CRT (qrFilialCRT/QrSPEDFilialCRT) and OPTANTE_SIMPLES for the filial registry/SPED and has no EMITENTE_* binding",
"emitted XML is not stored in the database (C000061.NFE_XML and XML_MES.XML hold file paths), so the CRT tag of issued documents could not be read directly"],
"status": "CRT_SOURCE_CONFLICT where the two values differ; the resolver refuses to choose without COMPANY_CRT_SOURCE"},
"UF": {"status": "UF_SOURCE_CONFIRMED", "evidence": "EMISSOR.EMITENTE_UF, C000004.UF and the cUF of 100% of emitted NFC-e keys agree in all 4 databases"},
"COMPANY": {"status": "confirmed", "evidence": "EMISSOR.EMITENTE_CNPJ equals exactly one C000004 CNPJ in every database and 100% of emitted NFC-e keys carry that CNPJ; other C000004 rows (RINCAO) have no documents"}}
(ROOT / "docs" / "COMPANY_CONTEXT_RESOLUTION_V1.json").write_text(json.dumps({"artifact": "COMPANY_CONTEXT_RESOLUTION_V1", "firebird_writes": 0, "authority": authority, "databases": evid}, ensure_ascii=False, indent=1), encoding="utf-8")
(ROOT / "docs" / "MULTI_DATABASE_COMPANY_CONTEXT_V1.json").write_text(json.dumps({"artifact": "MULTI_DATABASE_COMPANY_CONTEXT_V1", "firebird_writes": 0, "resolution_classes": classes, "databases": outcomes}, ensure_ascii=False, indent=1), encoding="utf-8")
print(json.dumps({k: (v["resolution_class"], v["with_COMPANY_CRT_SOURCE=EMISSOR"]["COMPANY_CONTEXT"], v["with_COMPANY_CRT_SOURCE=EMISSOR"]["company_crt"], v["negative_tests"]) for k, v in outcomes.items()}, ensure_ascii=False))
print(classes)
main()
+40
View File
@@ -0,0 +1,40 @@
"""Read-only audit of fiscal-field semantics in C000025 (types, value distributions, comments)."""
import json, os, sys
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src"))
from gti_fiscal_agent.firebird_reader import _connect # noqa: E402
FIELDS = ["CLASSIFICACAO_FISCAL","NCM","IPI","EX_TIPI","CEST","CFOP_ENTRADA","CFOP","CST_ENTRADA","CST","ALIQUOTA","CSOSN",
"COD_ENTRADA_PIS","COD_SAIDA_PIS","ALIQ_PIS","COD_ENTRADA_COFINS","COD_SAIDA_COFINS","ALIQ_COFINS","CST_IPI","ORIGEM","IAT","INCIDENCIA_PISCOFINS","IPPT"]
def load_env():
for line in (ROOT/".env").read_text(encoding="utf-8").splitlines():
if "=" in line and not line.startswith("#"):
k, _, v = line.partition("="); os.environ.setdefault(k.strip(), v.strip())
def main():
load_env()
conn = _connect(os.environ); cur = conn.cursor()
cur.execute("""SELECT rf.RDB$FIELD_NAME, f.RDB$FIELD_TYPE, f.RDB$FIELD_LENGTH, f.RDB$FIELD_PRECISION, f.RDB$FIELD_SCALE, f.RDB$CHARACTER_LENGTH, rf.RDB$DESCRIPTION, rf.RDB$DEFAULT_SOURCE
FROM RDB$RELATION_FIELDS rf JOIN RDB$FIELDS f ON rf.RDB$FIELD_SOURCE=f.RDB$FIELD_NAME WHERE rf.RDB$RELATION_NAME='C000025'""")
meta = {}
for r in cur.fetchall():
name = r[0].strip()
if name in FIELDS:
meta[name] = {"type_code": r[1], "len": r[5] or r[2], "precision": r[3], "scale": r[4], "comment": str(r[6]) if r[6] else None, "default": str(r[7]) if r[7] else None}
out = {}
for f in FIELDS:
if f not in meta:
out[f] = {"exists": False}; continue
cur.execute(f"SELECT {f}, COUNT(*) FROM C000025 GROUP BY {f}")
rows = sorted(((str(v).strip() if v is not None else None, c) for v, c in cur.fetchall()), key=lambda x: -x[1])
out[f] = {"exists": True, **meta[f], "distinct": len(rows), "top": rows[:12]}
# relations that may hold context (company/tax config), names only
cur.execute("SELECT RDB$RELATION_NAME FROM RDB$RELATIONS WHERE RDB$SYSTEM_FLAG=0 AND (RDB$RELATION_NAME LIKE '%EMPRESA%' OR RDB$RELATION_NAME LIKE '%CFOP%' OR RDB$RELATION_NAME LIKE '%TRIB%' OR RDB$RELATION_NAME LIKE '%ICMS%' OR RDB$RELATION_NAME LIKE '%CST%' OR RDB$RELATION_NAME LIKE '%NATUREZA%')")
out["_related_tables"] = [r[0].strip() for r in cur.fetchall()]
conn.close()
print(json.dumps(out, ensure_ascii=False, indent=1, default=str))
main()
+129
View File
@@ -0,0 +1,129 @@
"""READ-ONLY audit of any SGV Firebird database (restaurante / mercado / oficina ...).
python audit_firebird_database.py <FDB> [--json out.json]
Credentials come from the process environment (FB_USER / FB_PASSWORD, optional FIREBIRD_CLIENT_LIBRARY) or the
local ignored .env — never from the command line, never printed. The connection uses read-only isolation and the
script executes SELECT statements only. It never enables writes for the audited database.
"""
import argparse
import json
import os
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src")); sys.path.insert(0, str(ROOT / "src"))
FISCAL_FIELDS = ["CLASSIFICACAO_FISCAL", "NCM", "CFOP", "CFOP_ENTRADA", "CST", "CST_ENTRADA", "ALIQUOTA", "CEST", "CSOSN", "IPI", "EX_TIPI", "CST_IPI",
"COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS", "ORIGEM", "COD_ORIGEM_MERCADORIA",
"IAT", "INCIDENCIA_PISCOFINS", "IPPT", "GRUPO_ICMS", "SITUACAO_TRIBUTARIA", "MARGEM_AGREGADA"]
# Firebird RDB$FIELD_TYPE -> family
_FAMILY = {7: "int", 8: "int", 16: "int", 10: "float", 11: "float", 27: "float", 12: "date", 13: "time", 35: "timestamp", 14: "text", 37: "text", 261: "blob"}
# expected type family per field (from docs/ERP_FISCAL_FIELD_MAP_V1.json); text fields may also be char
EXPECTED_FAMILY = {"CLASSIFICACAO_FISCAL": "text", "NCM": "text", "CFOP": "text", "CFOP_ENTRADA": ("text", "int"), "CST": "text", "CST_ENTRADA": ("text", "int"), "ALIQUOTA": ("float", "int"),
"CEST": "text", "CSOSN": "text", "IPI": ("float", "int"), "EX_TIPI": "text", "CST_IPI": ("text", "int"), "COD_ENTRADA_PIS": "text", "COD_SAIDA_PIS": "text",
"ALIQ_PIS": "text", "COD_ENTRADA_COFINS": "text", "COD_SAIDA_COFINS": "text", "ALIQ_COFINS": "text", "ORIGEM": "text", "COD_ORIGEM_MERCADORIA": "int",
"IAT": "text", "INCIDENCIA_PISCOFINS": "text", "IPPT": "text", "GRUPO_ICMS": "text", "SITUACAO_TRIBUTARIA": "text", "MARGEM_AGREGADA": ("float", "int")}
def type_family(code):
return _FAMILY.get(code, f"other:{code}")
def ncm_bucket(value):
v = "" if value is None else str(value).strip()
if not v:
return "blank"
return "eight_digits" if re.fullmatch(r"\d{8}", v) else "other_format"
def compat(columns):
"""columns: {FIELD: type_code}. Returns per-field compatibility against the expected families."""
out = {}
for f, exp in EXPECTED_FAMILY.items():
if f not in columns:
out[f] = "MISSING"
continue
fam = type_family(columns[f]); exp = (exp,) if isinstance(exp, str) else exp
out[f] = "OK" if fam in exp else f"TYPE_DIFFERS({fam} not in {'/'.join(exp)})"
return out
def load_env():
env_file = ROOT / ".env"
if env_file.exists():
for line in env_file.read_text(encoding="utf-8").splitlines():
if "=" in line and not line.startswith("#"):
k, _, v = line.partition("="); os.environ.setdefault(k.strip(), v.strip())
def audit(path, env):
import fdb
from gti_discovery.fingerprint import build_fingerprint
from gti_fiscal_agent.firebird_reader import parse_company_context
conn = fdb.connect(database=path, user=env["FB_USER"], password=env["FB_PASSWORD"], fb_library_name=env.get("FIREBIRD_CLIENT_LIBRARY"),
isolation_level=fdb.ISOLATION_LEVEL_READ_COMMITED_RO)
try:
cur = conn.cursor()
res = {"database": Path(path).name, "mode": "READ_ONLY", "firebird_server_version": getattr(conn, "server_version", None), "ods": f"{getattr(conn, 'ods_version', '?')}.{getattr(conn, 'ods_minor_version', '?')}"}
cur.execute("""SELECT TRIM(rf.RDB$RELATION_NAME), TRIM(rf.RDB$FIELD_NAME), f.RDB$FIELD_TYPE, COALESCE(f.RDB$CHARACTER_LENGTH, f.RDB$FIELD_LENGTH), f.RDB$FIELD_SCALE
FROM RDB$RELATION_FIELDS rf JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE
JOIN RDB$RELATIONS r ON r.RDB$RELATION_NAME = rf.RDB$RELATION_NAME WHERE COALESCE(r.RDB$SYSTEM_FLAG, 0) = 0""")
meta = {}
for t, c, ty, ln, sc in cur.fetchall():
meta.setdefault(t, []).append({"name": c, "type": ty, "length": ln, "scale": sc})
res["schema_fingerprint"] = build_fingerprint({"tables": [{"name": t, "columns": cols} for t, cols in sorted(meta.items())]})
res["tables"] = len(meta)
# company context
if "C000004" in meta:
try:
cur.execute("SELECT UF, CRT FROM C000004"); res["company"] = {"status": "OK", **parse_company_context(cur.fetchall())}
except ValueError as e:
res["company"] = {"status": "UNAVAILABLE", "error": str(e)}
else:
res["company"] = {"status": "C000004_ABSENT"}
# products
res["C000025_present"] = "C000025" in meta
if res["C000025_present"]:
cols = {c["name"]: c["type"] for c in meta["C000025"]}
res["field_mapping_compatibility"] = compat(cols)
res["field_mapping_compatibility_summary"] = {k: sum(1 for v in res["field_mapping_compatibility"].values() if (v == k or v.startswith(k))) for k in ("OK", "MISSING", "TYPE_DIFFERS")}
cur.execute("SELECT COUNT(*) FROM C000025"); res["product_count"] = cur.fetchone()[0]
ncm_col = "CLASSIFICACAO_FISCAL" if "CLASSIFICACAO_FISCAL" in cols else None
if ncm_col:
cur.execute(f"SELECT {ncm_col}, COUNT(*) FROM C000025 GROUP BY {ncm_col}")
b = {"eight_digits": 0, "blank": 0, "other_format": 0}
for v, n in cur.fetchall():
b[ncm_bucket(v)] += n
res["ncm_coverage"] = {**b, "share_eight_digits": round(b["eight_digits"] / res["product_count"], 4) if res["product_count"] else None}
pop = {}
for f in FISCAL_FIELDS:
if f in cols:
cur.execute(f"SELECT COUNT(*) FROM C000025 WHERE {f} IS NOT NULL AND TRIM(CAST({f} AS VARCHAR(60))) <> ''")
pop[f] = cur.fetchone()[0]
res["fiscal_field_population"] = pop
return res
finally:
conn.close()
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("fdb"); ap.add_argument("--json")
a = ap.parse_args(argv)
load_env()
if not os.environ.get("FB_USER") or not os.environ.get("FB_PASSWORD"):
raise SystemExit("FB_USER/FB_PASSWORD must be set in the environment (never on the command line)")
if not Path(a.fdb).is_file():
raise SystemExit(f"database file not found: {a.fdb}")
res = audit(a.fdb, os.environ)
text = json.dumps(res, ensure_ascii=False, indent=1, default=str)
if a.json:
Path(a.json).write_text(text, encoding="utf-8")
print(text)
if __name__ == "__main__":
main()
+68
View File
@@ -0,0 +1,68 @@
"""Semantic audit of the local-completion writes (Local Completion V2). Offline: reads docs/runs/local_completion/* only.
Verdicts: SEMANTICALLY_PROVEN | NEEDS_REVIEW | INVALID_RULE. Emits docs/LOCAL_WRITE_SEMANTIC_AUDIT_V2.json (+ per-field tables) and rollback_plan.json {db: {field: [codes]}} for every write that is not PROVEN."""
import csv, gzip, json, pickle, re, sys
from collections import Counter, defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]; ST = ROOT / "docs" / "runs" / "local_completion"
DBS = ["CONSTRUCAO", "MMMODAS", "RINCAO", "UNIAO", "TESTESOFICIAIS"]
tipi = defaultdict(dict)
for line in (ST / "tipi_export.txt").read_text(encoding="utf-8").splitlines():
ncm, ex, rate, txt = line.split("|"); tipi[ncm][ex] = (float(rate) if rate else None, txt)
st_prefixes = {re.sub(r"\D", "", x) for x in (ST / "ba_st_ncm_export.txt").read_text(encoding="utf-8").split()}
ST_CODES = {"010", "030", "070", "060", "201", "202", "203", "500", "10", "30", "70", "60"}
audit = defaultdict(list); plan = defaultdict(lambda: defaultdict(list)); table = []
for n in DBS:
b = pickle.load(gzip.open(ST / f"{n}_before.pkl.gz", "rb")); cols = b["columns"]; rows = b["rows"]; g = lambda code, c: rows[code][cols.index(c)] # noqa: E731
dry = {}
for l in gzip.open(ST / f"{n}_dryrun.ndjson.gz", "rt", encoding="utf-8"):
r = json.loads(l); dry[(r["codigo"], r["field"])] = r
for l in gzip.open(ST / f"{n}_write_audit.ndjson.gz", "rt", encoding="utf-8"):
a = json.loads(l); code = a["product_code"]
for f, v in a["fields"].items():
d = dry[(code, f)]; ncm = re.sub(r"\D", "", str(g(code, "CLASSIFICACAO_FISCAL") or "")); rule = d["reason"]; ev = ""; verdict = "SEMANTICALLY_PROVEN"
if f == "COD_ORIGEM_MERCADORIA":
verdict, ev = "INVALID_RULE", f"CST digit is padding: CST={g(code,'CST')!r} while declared-origin products (1/2) also carry CST digit 0"
elif f == "IPI":
ncm_ok = dry[(code, "CLASSIFICACAO_FISCAL")]["status"] == "ALREADY_CORRECT" and len(ncm) == 8
base = tipi.get(ncm, {}).get("")
ex_prod = str(g(code, "EX_TIPI") or "").strip()
if not ncm_ok or base is None or base[0] is None or abs(base[0] - float(v["after"])) > 1e-9:
verdict, ev = "INVALID_RULE", "TIPI numeric base rate does not match"
elif g(code, "IPI") is not None:
verdict, ev = "NEEDS_REVIEW", "overwrote a non-null IPI"
elif ex_prod:
verdict, ev = "NEEDS_REVIEW", f"product declares EX_TIPI={ex_prod}"
else:
ev = f"NCM {ncm} valid; TIPI base row rate={base[0]} (text {base[1]!r}); EX rows for NCM: {sorted(k for k in tipi[ncm] if k)}; before NULL"
elif f == "CFOP":
cst, csosn = str(g(code, "CST") or "").strip(), str(g(code, "CSOSN") or "").strip()
st_decl = csosn in {"500", "201", "202", "203"} or cst in {"060", "010", "070", "030"}
if d["reason"] == "PROVEN_ROLE_AND_ST_CONTRADICT_DECLARED_CFOP" and v["before"] == "5405" and v["after"] == "5102":
verdict, ev = ("INVALID_RULE", f"ERP declares ICMS-ST already retained (CST={cst}, CSOSN={csosn}); CFOP 5102 contradicts the product's own CST/CSOSN, official table alone cannot override it") if st_decl else ("NEEDS_REVIEW", "ST not declared")
elif v["after"] == "5101":
ipp = dry.get((code, "IPPT"), {}).get("reason", "")
strong = "COMPOSICAO" in ipp or "RECEITA_INGREDIENTES" in ipp
verdict, ev = ("SEMANTICALLY_PROVEN", f"own production evidence: {ipp}") if strong else ("NEEDS_REVIEW", f"only kitchen-routing flag: {ipp}")
elif v["after"] == "5102" and v["before"] is None:
in_st = any(ncm.startswith(p) for p in st_prefixes if len(p) in (4, 6, 8))
verdict, ev = ("SEMANTICALLY_PROVEN", f"resale; CST={cst} CSOSN={csosn} not ST; NCM {ncm} not in BA ST prefixes") if (not st_decl and not in_st) else ("NEEDS_REVIEW", "possible ST")
elif f == "CEST":
cest = str(v["after"]); fmt = f"{cest[:2]}.{cest[2:5]}.{cest[5:]}"
verdict, ev = "SEMANTICALLY_PROVEN", f"official CEST dataset: NCM {ncm} linked exactly to {fmt} (single candidate); before blank"
elif f == "IPPT":
strong = "COMPOSICAO" in rule or "RECEITA_INGREDIENTES" in rule
verdict, ev = ("SEMANTICALLY_PROVEN", f"ERP evidence: {rule}") if strong else ("NEEDS_REVIEW", f"only kitchen-routing flag: {rule}")
else:
verdict, ev = "NEEDS_REVIEW", "no audit rule"
audit[(n, f)].append(verdict)
if verdict != "SEMANTICALLY_PROVEN":
plan[n][f].append(code)
if f in ("CFOP", "CEST", "IPPT"):
table.append({"database": n, "product": code, "field": f, "before": v["before"], "after": v["after"], "rule": rule, "evidence": ev, "verdict": verdict})
summary = {f"{n}|{f}": dict(Counter(v)) for (n, f), v in audit.items()}
(ROOT / "docs" / "LOCAL_WRITE_SEMANTIC_AUDIT_V2.json").write_text(json.dumps({"summary": summary, "small_field_writes": table}, ensure_ascii=False, indent=1), encoding="utf-8")
with open(ROOT / "docs" / "LOCAL_WRITE_SEMANTIC_AUDIT_V2_CFOP_CEST_IPPT.csv", "w", newline="", encoding="utf-8") as fh:
w = csv.DictWriter(fh, fieldnames=list(table[0])); w.writeheader(); w.writerows(table)
(ST / "rollback_plan.json").write_text(json.dumps(plan), encoding="utf-8")
for k, v in summary.items(): print(k, v)
+154
View File
@@ -0,0 +1,154 @@
"""READ-ONLY schema/capability/population audit of every .FDB in a directory (default: ./Banco).
python scripts/audit_multi_databases.py [DIR]
Uses read-only isolation and SELECT only; credentials from FB_USER/FB_PASSWORD (env or local ignored .env).
Writes docs/runs/multi_db/<DB>.json (raw) and docs/MULTI_DATABASE_SCHEMA_AUDIT_V1.json,
docs/MULTI_DATABASE_FIELD_COMPATIBILITY_V1.json. Never modifies a database.
"""
import json
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts")); sys.path.insert(0, str(ROOT / "agent" / "src")); sys.path.insert(0, str(ROOT / "src"))
import sgv_capabilities as cap # noqa: E402
REFERENCE = json.loads((ROOT / "docs" / "runs" / "audit_TESTESOFICIAIS.json").read_text(encoding="utf-8"))["schema_fingerprint"]
SIGNAL_TABLES = ["MESAS", "R000002", "RECEITA_INGREDIENTES", "DELIVERY_M", "C000051", "NFCE", "NFCE_ITENS", "C000032", "C000062", "C000088", "C000100", "C000030", "C000061"]
CONTEXT_TABLES = ["C000030", "C000032", "C000062", "C000088", "C000100", "NFCE_ITENS", "RECEITA_INGREDIENTES"]
def load_env():
f = ROOT / ".env"
if f.exists():
for line in f.read_text(encoding="utf-8").splitlines():
if "=" in line and not line.startswith("#"):
k, _, v = line.partition("=")
os.environ.setdefault(k.strip(), v.strip())
def audit_one(path):
import fdb
from gti_discovery.fingerprint import build_fingerprint
from gti_fiscal_agent.firebird_reader import parse_company_context
conn = fdb.connect(database=str(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)
try:
cur = conn.cursor()
res = {"file": path.name, "size_mb": round(path.stat().st_size / 1e6, 1), "mode": "READ_ONLY", "firebird_server_version": getattr(conn, "server_version", None),
"ods": f"{getattr(conn, 'ods_version', '?')}.{getattr(conn, 'ods_minor_version', '?')}"}
cur.execute("""SELECT TRIM(rf.RDB$RELATION_NAME), TRIM(rf.RDB$FIELD_NAME), f.RDB$FIELD_TYPE, COALESCE(f.RDB$CHARACTER_LENGTH, f.RDB$FIELD_LENGTH), f.RDB$FIELD_SCALE
FROM RDB$RELATION_FIELDS rf JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE JOIN RDB$RELATIONS r ON r.RDB$RELATION_NAME = rf.RDB$RELATION_NAME
WHERE COALESCE(r.RDB$SYSTEM_FLAG, 0) = 0""")
meta = {}
for t, c, ty, ln, sc in cur.fetchall():
meta.setdefault(t, []).append({"name": c, "type": ty, "length": ln, "scale": sc})
res["tables"] = len(meta)
res["schema_fingerprint"] = build_fingerprint({"tables": [{"name": t, "columns": cols} for t, cols in sorted(meta.items())]})
res["C000004_present"] = "C000004" in meta; res["C000025_present"] = "C000025" in meta
company = {"status": "C000004_ABSENT"}
if res["C000004_present"]:
names = {c["name"] for c in meta["C000004"]}
try:
cur.execute("SELECT UF, CRT FROM C000004"); company = {"status": "OK", **parse_company_context(cur.fetchall())}
except ValueError as e:
company = {"status": "UNAVAILABLE", "error": str(e)}
for col in ("OPTANTE_SIMPLES", "OPTANTE_SUPER_SIMPLES", "SUBSTITUTO_TRIBUTARIO", "CONTRIBUINTE_IPI", "ICMS_CSOSN"):
if col in names:
cur.execute(f"SELECT FIRST 1 {col} FROM C000004"); row = cur.fetchone(); company[col] = None if row is None or row[0] is None else str(row[0]).strip()
for col in ("FANTASIA", "NOME_FANTASIA", "RAZAO_SOCIAL", "EMPRESA", "NOME"): # short display name only, never CNPJ
if col in names:
cur.execute(f"SELECT FIRST 1 {col} FROM C000004"); row = cur.fetchone(); company["name"] = None if row is None or row[0] is None else str(row[0]).strip()[:40]; break
cur.execute("SELECT CODIGO, UF, CRT FROM C000004")
rows = [[None if v is None else str(v).strip() for v in r] for r in cur.fetchall()]
company["rows"] = len(rows); company["rows_summary"] = rows
if "FILIAL" in names:
cur.execute("SELECT FIRST 1 FILIAL FROM C000004 ORDER BY CODIGO"); r0 = cur.fetchone()
company["filial_name_first_row"] = None if r0 is None or r0[0] is None else str(r0[0]).strip()[:50]
if "EMISSOR" in meta and any(c["name"] == "EMITENTE_CRT" for c in meta["EMISSOR"]):
cur.execute("SELECT EMITENTE_CRT FROM EMISSOR"); company["emissor_crt"] = sorted({str(r[0]).strip() for r in cur.fetchall() if r[0] is not None})
res["company"] = company
counts = {}
for t in SIGNAL_TABLES:
if t in meta:
cur.execute(f'SELECT COUNT(*) FROM "{t}"'); counts[t] = cur.fetchone()[0]
else:
counts[t] = None
res["table_rows"] = counts
if res["C000025_present"]:
cols = {c["name"]: c["type"] for c in meta["C000025"]}
res["C000025_columns"] = len(cols)
full = {c["name"]: c for c in meta["C000025"]}
res["field_types"] = {f: [full[f]["type"], full[f]["length"], full[f]["scale"]] for f in cap.WRITE_TARGETS + [cap.ORIGIN_FIELD] if f in full}
res["capabilities"] = cap.detect_capabilities(cols)
res["field_compatibility"] = cap.field_compatibility(cols)
cur.execute("SELECT COUNT(*) FROM C000025"); res["product_count"] = cur.fetchone()[0]
dist, pop = {}, {}
for f in cap.WRITE_TARGETS + [cap.ORIGIN_FIELD]:
if f in cols:
cur.execute(f'SELECT "{f}", COUNT(*) FROM C000025 GROUP BY "{f}" ORDER BY 2 DESC')
rows = [(None if v is None else str(v).strip(), n) for v, n in cur.fetchall()]
pop[f] = sum(n for v, n in rows if v not in (None, ""))
dist[f] = {"distinct": len(rows), "top": rows[:8]}
res["fiscal_field_population"] = pop; res["fiscal_field_distribution"] = dist
for extra, sql in (("kitchen_products", "SELECT COUNT(*) FROM C000025 WHERE PROD_COZINHA = 1"), ("vehicle_products", "SELECT COUNT(*) FROM C000025 WHERE VEIC_TIPO IS NOT NULL AND TRIM(VEIC_TIPO) <> ''")):
col = "PROD_COZINHA" if "COZINHA" in sql else "VEIC_TIPO"
if col in cols:
cur.execute(sql); res[extra] = cur.fetchone()[0]
grade = [t for t in meta if "GRADE" in t]
res["grade_tables"] = {t: None for t in grade}
gi = 0
for t in grade:
cur.execute(f'SELECT COUNT(*) FROM "{t}"'); n = cur.fetchone()[0]; res["grade_tables"][t] = n; gi += n
sig = {"restaurant_tables_rows": (counts.get("MESAS") or 0) + (counts.get("R000002") or 0), "kitchen_products": res.get("kitchen_products", 0), "recipes": counts.get("RECEITA_INGREDIENTES") or 0,
"service_orders": counts.get("C000051") or 0, "vehicle_products": res.get("vehicle_products", 0), "grade_items": gi, "nfce_items": counts.get("NFCE_ITENS") or 0}
seg, score, ev = cap.infer_segment(sig, " ".join(str(company.get(k) or "") for k in ("name", "filial_name_first_row")))
res["segment_inferred"] = {"segment": seg, "score": score, "evidence": ev, "signals": sig, "note": "inferred from table population, not from the file name"}
return res
finally:
conn.close()
def main():
load_env()
directory = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / "Banco"
dbs = sorted(directory.glob("*.FDB")) + sorted(p for p in directory.glob("*.fdb") if p.suffix != ".FDB")
out_dir = ROOT / "docs" / "runs" / "multi_db"; out_dir.mkdir(parents=True, exist_ok=True)
audits = []
ref_types = audit_one(ROOT / "TESTESOFICIAIS.FDB").get("field_types", {}) # reference database, read-only
for p in dbs:
try:
a = audit_one(p)
a["type_variations_vs_reference"] = {f: {"reference": ref_types.get(f), "this": v} for f, v in a.get("field_types", {}).items() if ref_types.get(f) != v}
a["schema_class"] = cap.classify_schema(a["C000004_present"], a["C000025_present"], a["schema_fingerprint"], REFERENCE, a.get("field_compatibility", {}))
except Exception as e: # noqa: BLE001 — record, never guess
a = {"file": p.name, "size_mb": round(p.stat().st_size / 1e6, 1), "error": f"{type(e).__name__}: {str(e)[:200]}", "schema_class": "INCOMPATIBLE"}
(out_dir / f"{p.stem}.json").write_text(json.dumps(a, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
audits.append(a)
print(p.name, a.get("schema_class"), a.get("product_count"), a.get("company"), a.get("error", ""))
ok = [a for a in audits if "error" not in a]
schema_doc = {"artifact": "MULTI_DATABASE_SCHEMA_AUDIT_V1", "firebird_writes": 0, "directory": directory.name, "reference_fingerprint_TESTESOFICIAIS": REFERENCE, "databases_found": len(dbs), "databases_audited": len(ok),
"classification_counts": {k: sum(1 for a in audits if a["schema_class"] == k) for k in ("REFERENCE_SCHEMA_MATCH", "COMPATIBLE_WITH_VARIATIONS", "INCOMPATIBLE")},
"databases": [{k: a.get(k) for k in ("file", "size_mb", "firebird_server_version", "ods", "schema_fingerprint", "tables", "product_count", "company", "C000004_present", "C000025_present", "schema_class",
"capabilities", "segment_inferred", "table_rows", "error")} for a in audits]}
(ROOT / "docs" / "MULTI_DATABASE_SCHEMA_AUDIT_V1.json").write_text(json.dumps(schema_doc, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
fc = {}
for f in cap.WRITE_TARGETS + [cap.ORIGIN_FIELD]:
fc[f] = {"status_per_db": {a["file"]: a.get("field_compatibility", {}).get(f, "N/A") for a in audits},
"population_per_db": {a["file"]: a.get("fiscal_field_population", {}).get(f) for a in ok},
"distribution_top_per_db": {a["file"]: a.get("fiscal_field_distribution", {}).get(f, {}).get("top", [])[:5] for a in ok}}
all_present = [f for f in cap.WRITE_TARGETS if all(a.get("field_compatibility", {}).get(f) in ("PRESENT_OK", "PRESENT_TYPE_VARIATION") for a in ok)]
variations = {}
for a in ok:
for f, v in a.get("type_variations_vs_reference", {}).items():
variations.setdefault(f, {})[a["file"]] = v
for f, v in a.get("field_compatibility", {}).items():
if v not in ("PRESENT_OK",):
variations.setdefault(f, {})[a["file"]] = v
(ROOT / "docs" / "MULTI_DATABASE_FIELD_COMPATIBILITY_V1.json").write_text(json.dumps({"artifact": "MULTI_DATABASE_FIELD_COMPATIBILITY_V1", "write_targets_present_in_all": all_present, "write_targets_count": len(cap.WRITE_TARGETS),
"variations": variations, "fields": fc}, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
main()
+30
View File
@@ -0,0 +1,30 @@
"""Compare the ACTIVE BA_ST_MVA source PDF with a freshly downloaded RICMS-BA Anexo 1 and list the affected products. Read-only; changes no dataset."""
import gzip, json, pickle, re, sys
from pathlib import Path
from pypdf import PdfReader
ROOT = Path(__file__).resolve().parents[1]; sys.path.insert(0, str(ROOT))
from central.app.datasets.ba_st_mva import parse_page # noqa: E402
def load(p):
recs = []
for i, pg in enumerate(PdfReader(p).pages, 1):
f, _ = parse_page(pg.extract_text() or "", i); recs += f
return recs
def key(r): return (r["item"], r["cest"] or "")
old = load(ROOT / "sources/raw/bootstrap_v1/decreto_2012_13780_ricms_anexo_1_vigente_2026.pdf"); new = load(ROOT / "sources/raw/bootstrap_v1/decreto_2012_13780_ricms_anexo_1_vigente_2026_dl20260921.pdf")
sig = lambda r: (tuple(sorted(r["ncm_sh"] or [])), r["mva_original"], r["mva_adjusted"]) # noqa: E731
o = {key(r): r for r in old}; n = {key(r): r for r in new}
added = sorted(set(n) - set(o)); removed = sorted(set(o) - set(n)); changed = sorted(k for k in set(o) & set(n) if sig(o[k]) != sig(n[k]))
def prefixes(recs): return {re.sub(r"\D", "", x) for r in recs for x in (r["ncm_sh"] or []) if len(re.sub(r"\D", "", x)) in (4, 6, 8)}
po, pn = prefixes(old), prefixes(new); gone, come = po - pn, pn - po
affected = {}
for name in ["CONSTRUCAO", "MMMODAS", "RINCAO", "UNIAO", "TESTESOFICIAIS"]:
snap = pickle.load(gzip.open(ROOT / f"docs/runs/local_completion/{name}_before.pkl.gz", "rb")); cols = snap["columns"]; i = cols.index("CLASSIFICACAO_FISCAL")
hit = [c for c, r in snap["rows"].items() if any(re.sub(r"\D", "", r[i] or "").startswith(p) for p in (gone | come))]
affected[name] = {"products": len(hit), "examples": hit[:5]}
doc = {"old_records": len(old), "new_records": len(new), "items_added": [list(k) for k in added], "items_removed": [list(k) for k in removed], "items_changed": [{"key": list(k), "old": sig(o[k]), "new": sig(n[k])} for k in changed[:60]], "items_changed_count": len(changed),
"ncm_prefixes_no_longer_listed": sorted(gone)[:100], "ncm_prefixes_newly_listed": sorted(come)[:100], "affected_products_by_database": affected,
"note": "No dataset was modified; the Hub still serves the ACTIVE BA_ST_MVA (sha e07e9085...). The candidate PDF is versioned under sources/raw/bootstrap_v1/."}
(ROOT / "docs" / "BA_ST_MVA_REFRESH_DIFF_V1.json").write_text(json.dumps(doc, ensure_ascii=False, indent=1, default=list), encoding="utf-8")
print({k: (v if not isinstance(v, list) else len(v)) for k, v in doc.items() if k not in ("affected_products_by_database", "items_changed")}); print(affected)
+74
View File
@@ -0,0 +1,74 @@
"""Build the GTI Fiscal Agent Windows package (zip) and verify it contains no secrets.
python scripts/build_agent_package.py -> dist/GTIFiscalAgent-<version>.zip (+ .sha256)
The zip root is the install folder:
gti-agent.ps1 / gti-agent.cmd / requirements-agent.txt / agent.env.template / pilot-construcao.env.example / profiles/
agent/{src,scripts,service}, scripts/firebird_write_executor.py, central/app/{engine modules}, VERSION, MANIFEST.json
"""
import hashlib
import json
import re
import sys
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
VERSION = (ROOT / "release" / "VERSION").read_text(encoding="utf-8").strip()
NAME = f"GTIFiscalAgent-{VERSION}"
INCLUDE = [ # (source, destination inside the package)
*[(p, str(p.relative_to(ROOT)).replace("\\", "/")) for p in sorted((ROOT / "agent" / "src" / "gti_fiscal_agent").glob("*.py"))],
(ROOT / "agent" / "scripts" / "run_agent_service.py", "agent/scripts/run_agent_service.py"),
(ROOT / "agent" / "scripts" / "preflight.py", "agent/scripts/preflight.py"),
(ROOT / "agent" / "service" / "gti_agent_windows_service.py", "agent/service/gti_agent_windows_service.py"),
(ROOT / "scripts" / "firebird_write_executor.py", "scripts/firebird_write_executor.py"),
(ROOT / "central" / "__init__.py", "central/__init__.py"),
*[(ROOT / "central" / "app" / f, f"central/app/{f}") for f in ("__init__.py", "fiscal_decision.py", "dry_run_planner.py", "product_defaults.py", "icms_context.py")],
*[(p, p.name) for p in sorted((ROOT / "deploy" / "agent-windows").glob("*")) if p.is_file()],
*[(p, f"profiles/{p.name}") for p in sorted((ROOT / "deploy" / "agent-windows" / "profiles").glob("*")) if p.is_file()],
(ROOT / "release" / "VERSION", "VERSION"),
]
SECRET_PATTERNS = [
(re.compile(r"^[ ]*(AGENT_TOKEN|FB_PASSWORD|POSTGRES_PASSWORD)[ ]*=[ ]*\S+", re.M), "non-empty secret assignment"),
(re.compile(r"masterkey", re.I), "default Firebird password"),
(re.compile(r"[A-Za-z0-9_\-]{40,}"), "long token-like string"),
]
ALLOWED_LONG = re.compile(r"^[A-Za-z0-9_\-]{40,}$")
def scan(name, data):
if name.endswith((".ps1", ".cmd", ".py", ".txt", ".example", ".template", "VERSION")):
text = data.decode("utf-8", "replace")
for rx, why in SECRET_PATTERNS:
for m in rx.finditer(text):
token = m.group(0)
if why == "long token-like string" and (token.startswith(("_", "-")) or "_" in token and token.count("_") > 3 or token.isalpha()):
continue # identifiers / prose, not a secret
if why == "long token-like string" and not re.search(r"\d", token):
continue
if why == "long token-like string" and re.fullmatch(r"[0-9a-f]{64}", token):
continue # schema fingerprint (public SHA-256), not a secret
raise SystemExit(f"secret scan failed in {name}: {why}: {token[:6]}...")
def main():
dist = ROOT / "dist"
dist.mkdir(exist_ok=True)
out = dist / f"{NAME}.zip"
manifest = {"name": NAME, "version": VERSION, "files": {}}
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
for src, dst in INCLUDE:
data = Path(src).read_bytes()
assert not dst.endswith((".env", ".sqlite", ".log", ".FDB")), dst
scan(dst, data)
manifest["files"][dst] = hashlib.sha256(data).hexdigest()
z.writestr(f"{NAME}/{dst}", data)
z.writestr(f"{NAME}/MANIFEST.json", json.dumps(manifest, indent=1))
digest = hashlib.sha256(out.read_bytes()).hexdigest()
(dist / f"{NAME}.zip.sha256").write_text(f"{digest} {out.name}\n", encoding="utf-8")
print(json.dumps({"package": str(out.relative_to(ROOT)), "files": len(manifest["files"]), "sha256": digest}))
if __name__ == "__main__":
main()
@@ -0,0 +1,58 @@
"""docs/OPERATOR_COMPANY_QUESTIONS_V1.md: ONLY company-wide decisions (category C) in plain Portuguese, plus the residual counts. Read-only over the state files."""
import gzip, json, pickle, sys
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]; ST = ROOT / "docs/runs/local_completion"
DBS = ["CONSTRUCAO", "MMMODAS", "RINCAO", "UNIAO", "TESTESOFICIAIS"]
Q = json.loads((ROOT / "docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json").read_text(encoding="utf-8"))["questions"]
summ = {n: json.loads((ST / f"{n}_dryrun_summary.json").read_text(encoding="utf-8")) for n in DBS}
ipi_cells, entry_dist, rows_n = {}, {}, {}
for n in DBS:
snap = pickle.load(gzip.open(ST / f"{n}_before.pkl.gz", "rb")); cols = snap["columns"]
ipi_cells[n] = 0; rows_n[n] = len(snap["rows"])
for l in gzip.open(ST / f"{n}_second.ndjson.gz", "rt", encoding="utf-8"):
r = json.loads(l)
if r["field"] == "IPI" and r["status"] == "ALREADY_CORRECT" and r["before"] not in (None, ""):
ipi_cells[n] += 1
entry_dist[n] = Counter((r[cols.index("CFOP_ENTRADA")] or "").strip()[:1] or "(vazio)" for r in snap["rows"].values())
crt = {n: summ[n]["company"]["crt"] for n in DBS}
by = lambda key: [q for q in Q if q["KEY"] == key and q["CATEGORY"] == "C"] # noqa: E731
regime = {"1": "Simples Nacional", "3": "Regime normal (Lucro Presumido/Real)"}
out = ["# Perguntas de empresa — respostas necessárias ANTES da camada de IA", "",
"Só entram decisões que valem para a **empresa inteira**. Nada aqui é preenchido automaticamente: sem resposta, as células continuam pendentes.",
"Como responder: preencher a coluna `VALUE` da linha do mesmo QUESTION_ID em `docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.csv` e importar com `scripts/import_fiscal_context.py`.", ""]
n_q = 0; cells_total = 0
def block(qid, banco, pergunta, opcoes, rec, prods, cells, expl):
global n_q, cells_total
n_q += 1; cells_total += cells
out.extend([f"### {qid} — {banco}", f"- **Pergunta:** {pergunta}", f"- **Opções:** {opcoes}", f"- **Recomendação:** {rec}", f"- **Produtos afetados:** {prods}", f"- **Células afetadas:** {cells}", f"- **Explicação:** {expl}", ""])
out.append("## 1. Perfil de PIS/COFINS (por empresa)")
for q in by("pis_cofins_profile"):
n = q["DATABASE"]
block(q["QUESTION_ID"], f"{n} ({regime.get(crt[n], crt[n])})", "Qual o enquadramento de PIS/COFINS desta empresa para produtos que NÃO estão em regime especial (monofásico, alíquota zero, etc.)?",
"JSON com pis_in_cst, pis_out_cst, pis_rate, cofins_in_cst, cofins_out_cst, cofins_rate (CSTs da tabela oficial 4.3.3/4.3.4). Ex. regime não cumulativo: saída 01, alíquotas 1,65 / 7,6; cumulativo: 0,65 / 3,0",
"nenhuma provada (depende do regime tributário real da empresa: cumulativo ou não cumulativo, dado que só o contador confirma)", q["PRODUCTS_AFFECTED"], q["CELLS"],
"A tabela oficial da Receita já resolveu os produtos em regime especial; os demais dependem do enquadramento da empresa. " + ("Nesta base o SGV não usa estes campos na emissão (100% dos itens de NFC-e saíram sem CST de PIS/COFINS); a resposta só completa poucos produtos com valor inválido no cadastro." if summ[n]["company"].get("pis_cofins_unused_by_emission") else "O histórico de NFC-e desta base emite CST de PIS/COFINS preenchido, então o campo do produto é usado na emissão."))
out.append("## 2. Escopo das compras (CFOP de entrada)")
for q in by("entry_scope"):
n = q["DATABASE"]; d = entry_dist[n]; tot = sum(d.values())
block(q["QUESTION_ID"], f"{n}", "As compras de mercadoria desta empresa são, em regra, dentro do estado (INTRA, CFOP 1xxx) ou de outro estado (INTER, CFOP 2xxx)?",
"INTRA | INTER", f"nenhuma provada. Só informativo: nos cadastros existentes desta base, CFOP de entrada 1xxx = {d.get('1', 0)}, 2xxx = {d.get('2', 0)}, vazio = {d.get('(vazio)', 0)} de {tot} produtos (declarações históricas, não prova de regra)",
q["PRODUCTS_AFFECTED"], q["CELLS"], "Só é usado nos produtos cujo CFOP de entrada está vazio. Produtos que já têm CFOP de entrada válido não são alterados.")
out.append("## 3. Alíquota padrão de ICMS")
for q in by("icms_rate"):
block(q["QUESTION_ID"], q["DATABASE"], "Qual a alíquota interna de ICMS padrão desta empresa (regime normal)?", "número em % (ex.: 20,5)", "nenhuma provada (a alíquota depende da mercadoria; não vale 20,5% universal)", q["PRODUCTS_AFFECTED"], q["CELLS"],
"Um único produto da base de testes depende desta resposta. As demais empresas são Simples Nacional e não destacam ICMS: não perguntado.")
out.append("## 4. IPI no cadastro de empresa varejista (STORE_IPI_PROFILE)")
out.append("Evidência do próprio SGV: em TODAS as bases `CONTRIBUINTE_IPI = N`; 745 itens vendidos na base de testes de produtos com alíquota de IPI cadastrada geraram IPI = 0 (nenhum valor de IPI foi calculado), e mais de 1,6 milhão de itens vendidos nas demais bases também têm IPI 0. O campo do cadastro é, portanto, apenas referência para empresa não contribuinte. **O SGV não impõe preencher.** Decisão de política:")
out.append("")
for n in DBS:
n_q += 1; cells_total += ipi_cells[n]
out.extend([f"### QIPI-{n} — {n}", "- **Pergunta:** Guardar no cadastro dos produtos a alíquota oficial da TIPI como referência (mesmo a empresa não sendo indústria/importadora)?", "- **Opções:** `POPULATE_REFERENCE_RATE` (guardar) | `LEAVE_NOT_APPLICABLE` (deixar em branco)",
"- **Recomendação:** nenhuma (é política da empresa; ambas as opções são válidas)", f"- **Produtos afetados:** {ipi_cells[n]}", f"- **Células afetadas:** {ipi_cells[n]} (já gravadas com o valor oficial; se responder LEAVE_NOT_APPLICABLE, serão revertidas com `scripts/rollback_field.py`)",
"- **Explicação:** enquanto não houver resposta, nenhum novo valor de IPI é gravado. Chave: `store_ipi_profile` (escopo COMPANY).", ""])
out.append("## 5. Origem da mercadoria — NÃO é decisão de empresa")
out.append("A origem (0 a 8) é atributo real de cada mercadoria e o ERP não a registra em lugar nenhum (o primeiro dígito do CST é só preenchimento). **Não existe uma resposta padrão de empresa** e nenhum valor será assumido. Só responda por GRUPO/PRODUTO quando puder garantir (ex.: grupo inteiro de importados = 1). Essas perguntas continuam nos arquivos V2, fora desta lista.")
out += ["", f"**Total desta lista: {n_q} perguntas de empresa.**"]
(ROOT / "docs/OPERATOR_COMPANY_QUESTIONS_V1.md").write_text("\n".join(out) + "\n", encoding="utf-8")
print(n_q, cells_total)
+58
View File
@@ -0,0 +1,58 @@
"""Questionnaire V2: aggregates every remaining MANUAL_INPUT_REQUIRED cell (docs/runs/local_completion/*_second.ndjson.gz) into the fewest operator questions.
Only categories C (company configuration), D (product-group/NCM configuration) and E (product fact) may become questions."""
import csv, gzip, json, pickle, sys
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]; ST = ROOT / "docs" / "runs" / "local_completion"
sys.path.insert(0, str(ROOT))
from central.app.context_keys import ENUMS # noqa: E402
DBS = ["CONSTRUCAO", "MMMODAS", "RINCAO", "UNIAO", "TESTESOFICIAIS"]
CAT = {"origin": ("E", "COMPANY"), "pis_cofins_profile": ("C", "COMPANY"), "entry_scope": ("C", "COMPANY"), "icms_rate": ("C", "COMPANY"), "cest": ("D", "NCM"), "ex_tipi": ("D", "NCM"),
"st_withheld": ("D", "GROUP"), "st_status": ("D", "GROUP"), "icms_treatment": ("D", "GROUP"), "product_role": ("D", "GROUP"), "ncm": ("E", "PRODUCT"), "duplicate_code": ("E", "PRODUCT")}
QUESTION = {"origin": "Qual a origem da mercadoria (0-8) dos produtos {scope}? (nenhuma evidencia no ERP; o digito do CST e apenas preenchimento)",
"pis_cofins_profile": "Qual o perfil PIS/COFINS da empresa (CST de entrada/saida e aliquotas)? JSON com pis_in_cst, pis_out_cst, pis_rate, cofins_in_cst, cofins_out_cst, cofins_rate",
"entry_scope": "As compras de mercadoria desta empresa sao predominantemente INTRA (dentro da UF) ou INTER (outra UF)?",
"icms_rate": "Qual a aliquota de ICMS padrao desta empresa (regime normal)?",
"cest": "Qual o CEST correto para o NCM {scope} (ou NONE quando o produto nao esta sujeito)?",
"ex_tipi": "Qual o EX da TIPI aplicavel ao NCM {scope} (ou NONE)?",
"st_withheld": "Os produtos {scope} sujeitos a ST tem o ICMS-ST ja retido na compra (Y/N)?",
"st_status": "Os produtos {scope} estao sujeitos a ST (SUBJECT/NOT_SUBJECT)? O ERP declara ST retida mas a tabela oficial nao lista o NCM",
"icms_treatment": "Qual o tratamento de ICMS dos produtos {scope}?",
"product_role": "Os produtos {scope} sao revenda, producao propria, insumo ou servico?",
"ncm": "Qual o NCM correto do produto {scope} (o atual nao existe na tabela oficial)?",
"duplicate_code": "O CODIGO {scope} aparece em 2 linhas fisicas (violacao de PK na copia); corrigir a integridade do banco (gfix/validate) antes de classificar"}
WHY = {"origin": "CST/CSOSN e a origem do NF-e dependem dela; nao pode ser presumida 0", "pis_cofins_profile": "PIS/COFINS fora dos regimes especiais das tabelas SPED 4.3.x", "entry_scope": "CFOP de entrada (1xxx vs 2xxx)",
"icms_rate": "aliquota interna", "cest": "CEST oficial para o NCM tem varios candidatos ou o CEST do ERP nao consta da lista oficial", "ex_tipi": "varias linhas TIPI com aliquotas diferentes para o NCM",
"st_withheld": "ST comprovada pela tabela BA mas retencao desconhecida", "st_status": "conflito entre declaracao do ERP e tabela oficial", "icms_treatment": "CST de ICMS nao derivavel", "product_role": "revenda/producao define CFOP e IPPT",
"ncm": "NCM invalido bloqueia IPI/EX/CEST/ST/CFOP/CSOSN", "duplicate_code": "UPDATE por CODIGO exige rowcount 1"}
agg = defaultdict(lambda: {"products": set(), "cells": 0, "fields": defaultdict(int), "examples": []})
for n in DBS:
snap = pickle.load(gzip.open(ST / f"{n}_before.pkl.gz", "rb")); cols = snap["columns"]; rows = snap["rows"]
for l in gzip.open(ST / f"{n}_second.ndjson.gz", "rt", encoding="utf-8"):
r = json.loads(l)
if r["status"] != "MANUAL_INPUT_REQUIRED":
continue
k = r["input_key"]; cat, scope = CAT[k]; row = rows.get(r["codigo"]); g = (row[cols.index("CODGRUPO")] if row else None); ncm = (row[cols.index("CLASSIFICACAO_FISCAL")] if row else None)
sk = {"COMPANY": "", "GROUP": g or "", "NCM": ncm or "", "PRODUCT": r["codigo"]}[scope]
a = agg[(n, k, scope, sk)]; a["products"].add(r["row_key"]); a["cells"] += 1; a["fields"][r["field"]] += 1
if len(a["examples"]) < 3 and row: a["examples"].append(row[cols.index("PRODUTO")])
out = []
for (n, k, scope, sk), a in agg.items():
cat = CAT[k][0]
opts = sorted(ENUMS[k]) if k in ENUMS else ("JSON" if k == "pis_cofins_profile" else "7 digits or NONE" if k == "cest" else "8 digits" if k == "ncm" else "free")
who = "toda a empresa" if scope == "COMPANY" else f"{scope} {sk}"
out.append({"DATABASE": n, "COMPANY": n, "SCOPE": scope, "GROUP": sk if scope == "GROUP" else "", "NCM": sk if scope == "NCM" else "", "FIELD": "|".join(sorted(a["fields"])), "KEY": k, "CATEGORY": cat,
"QUESTION": QUESTION[k].format(scope=who), "VALID_OPTIONS": opts, "PRODUCTS_AFFECTED": len(a["products"]), "WHY_NEEDED": WHY[k], "WHAT_IT_WILL_RESOLVE": f"{a['cells']} celulas", "CELLS": a["cells"],
"EXAMPLES": "; ".join(a["examples"])})
out.sort(key=lambda x: -x["CELLS"])
for i, q in enumerate(out, 1): q["QUESTION_ID"] = f"Q{i:04d}"
(ROOT / "docs" / "LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json").write_text(json.dumps({"artifact": "LOCAL_FISCAL_CONTEXT_QUESTIONS_V2", "total_questions": len(out), "total_cells": sum(q["CELLS"] for q in out), "questions": out}, ensure_ascii=False, indent=1), encoding="utf-8")
hdr = ["QUESTION_ID", "DATABASE", "COMPANY", "SCOPE", "GROUP", "NCM", "FIELD", "KEY", "CATEGORY", "QUESTION", "VALID_OPTIONS", "PRODUCTS_AFFECTED", "WHY_NEEDED", "WHAT_IT_WILL_RESOLVE", "CELLS", "EXAMPLES", "VALUE"]
with open(ROOT / "docs" / "LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.csv", "w", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=hdr, extrasaction="ignore"); w.writeheader()
for q in out: w.writerow({**q, "VALID_OPTIONS": q["VALID_OPTIONS"] if isinstance(q["VALID_OPTIONS"], str) else "|".join(q["VALID_OPTIONS"]), "VALUE": ""})
from collections import Counter
print(len(out), sum(q["CELLS"] for q in out), dict(Counter(q["CATEGORY"] for q in out)), dict(Counter(q["KEY"] for q in out)))
print([(q["QUESTION_ID"], q["DATABASE"], q["KEY"], q["CELLS"]) for q in out[:8]])
+56
View File
@@ -0,0 +1,56 @@
"""Parse the official SPED EFD-Contribuições tables 4.3.x (Word .doc, converted with antiword) into a versioned NCM-keyed dataset.
Source: sped.rfb.gov.br/arquivo/download/<id> (current files, fetched directly on 2026-09-21).
Writes docs/sources/SPED_PISCOFINS_TABLES_V3.json (per table: source id, sha256 of the .doc, version text, entries {code, description, ncm_prefixes, start, end, pis_rate, cofins_rate})."""
import datetime, hashlib, json, re, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]; SRC = ROOT / "sources_cache" / "cur"
TABLES = { # sped item id -> (table, CST it defines for the SAIDA side, meaning)
1638: ("4.3.10", ["02", "04"], "MONOFASICA_OU_PAUTA"), 1642: ("4.3.12", ["05"], "SUBSTITUICAO_TRIBUTARIA"), 1643: ("4.3.13", ["06"], "ALIQUOTA_ZERO"),
1646: ("4.3.14", ["07"], "ISENTA"), 1651: ("4.3.15", ["08"], "SEM_INCIDENCIA"), 5786: ("4.3.11", ["03", "04"], "MONOFASICA_AD_REM")}
NCM = re.compile(r"\d{4}(?:\.\d{2}(?:\.\d{2}(?:\.\d{2})?)?)?")
DATE = re.compile(r"\d{2}/\d{4}|\d{2}/\d{2}/\d{4}")
def parse(txt):
rows, cur = [], None
for line in txt.splitlines():
if not line.startswith("|"):
continue
cells = [c.strip() for c in line.strip().strip("|").split("|")]
if cells and re.fullmatch(r"\d{3}", cells[0] or ""):
cur = {"code": cells[0], "cells": [[c] for c in cells[1:]]}; rows.append(cur)
elif cur is not None and len(cells) > 1 and not cells[0]:
for i, c in enumerate(cells[1:]):
if i < len(cur["cells"]) and c:
cur["cells"][i].append(c)
return rows
out = {"artifact": "SPED_PISCOFINS_TABLES_V3", "tables": {}}
for sid, (tab, csts, meaning) in TABLES.items():
raw = (SRC / f"sped_{sid}.bin").read_bytes(); txt = (SRC / f"sped_{sid}.txt").read_text(encoding="utf-8", errors="replace")
hdr = (SRC / f"h_{sid}.txt").read_text(errors="replace"); lm = re.search(r"Last-Modified: (.*)", hdr); lm = lm.group(1).strip() if lm else None
head_all = " ".join(txt.splitlines()[:60])
ver = re.search(r"Vers[ãa]o (\d+(?:\.\d+)+)", head_all); eff = re.search(r"Atualizad[ao]s? em (\d{2}/\d{2}/\d{4})", head_all)
head = " ".join(l.strip() for l in txt.splitlines()[:8] if l.strip() and not l.startswith("|"))
entries = []
for r in parse(txt):
cells = [" ".join(c) for c in r["cells"]] or [""]; dcells = ["".join(c) for c in r["cells"]]
joined_ncm = cells[1] if len(cells) > 1 else ""
pref = sorted({m.group(0).replace(".", "") for m in NCM.finditer(joined_ncm)})
dates = [d for c in dcells[2:] for d in DATE.findall(c)]
rates = re.findall(r"\d+,\d+", " ".join(cells[2:4])) if tab in ("4.3.10", "4.3.12") else []
entries.append({"code": r["code"], "description": cells[0] if cells else "", "ncm_prefixes": pref, "applies_to_all_ncm": not pref and joined_ncm.strip() in ("-", ""), "start": dates[0] if dates else None, "end": dates[1] if len(dates) > 1 else None, "rates_text": rates})
out["tables"][tab] = {"sped_item": sid, "source_url": f"http://sped.rfb.gov.br/arquivo/download/{sid}", "download_date": datetime.date.today().isoformat(), "http_last_modified": lm, "version": ver.group(1) if ver else None, "effective_date": eff.group(1) if eff else None, "sha256_doc": hashlib.sha256(raw).hexdigest(), "version_text": head, "saida_cst": csts, "meaning": meaning,
"fetched_via": "direct official download over http (sped.rfb.gov.br)", "entries": entries, "ncm_prefix_count": len({p for e in entries for p in e["ncm_prefixes"]})}
print(tab, len(entries), out["tables"][tab]["ncm_prefix_count"], head[:110])
for sid, name in ((1629, "4.3.3"), (1630, "4.3.4")):
raw = (SRC / f"sped_{sid}.bin").read_bytes(); txt = (SRC / f"sped_{sid}.txt").read_text(encoding="utf-8", errors="replace")
cst = {}
for line in txt.splitlines():
m = re.match(r"\|(\d{2})\s*\|([^|]+)\|", line)
if m: cst[m.group(1)] = m.group(2).strip()
out["tables"][name] = {"sped_item": sid, "sha256_doc": hashlib.sha256(raw).hexdigest(), "cst_domain": cst}
print(name, sorted(cst))
(ROOT / "docs" / "sources" / "SPED_PISCOFINS_TABLES_V3.json").write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
+46
View File
@@ -0,0 +1,46 @@
"""Runs INSIDE the fiscal-hub container (read-only on PostgreSQL): classifies [ncm, description] pairs with the
production Classifier through a memoizing repository wrapper. Same code path as /v1/classify, minus HTTP and the
classification_cache write; the wrapper only memoizes pure dataset lookups (datasets are static during the run).
usage: python /tmp/bulk_classify_in_container.py /tmp/in.json /tmp/out.json
"""
import json
import sys
from functools import lru_cache
from central.app.classification import Classifier
from central.app.datasets.service import DatasetService
from central.app.db import SessionLocal
class Memo:
def __init__(self, service):
self._s = service; self._cache = {}
def __getattr__(self, name):
attr = getattr(self._s, name)
if not callable(attr):
return attr
def wrapped(*a, **kw):
key = (name, a, tuple(sorted(kw.items())))
try:
hash(key)
except TypeError:
return attr(*a, **kw)
if key not in self._cache:
self._cache[key] = attr(*a, **kw)
return self._cache[key]
return wrapped
def main():
pairs = json.load(open(sys.argv[1], encoding="utf-8"))
out = []
with SessionLocal() as session:
classifier = Classifier(Memo(DatasetService(session)))
for ncm, desc in pairs:
out.append(classifier.classify({"current_ncm": ncm, "description": desc}))
json.dump(out, open(sys.argv[2], "w", encoding="utf-8"), ensure_ascii=False, default=str)
print(len(out))
main()
+33
View File
@@ -0,0 +1,33 @@
"""Read-only Firebird schema/context discovery (no DML/DDL). Writes JSON to stdout."""
import json, os, re, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src"))
from gti_fiscal_agent.firebird_reader import _connect # noqa: E402
for l in (ROOT / ".env").read_text(encoding="utf-8").splitlines():
if "=" in l and not l.startswith("#"):
k, _, v = l.partition("="); os.environ.setdefault(k.strip(), v.strip())
KW = re.compile(r"CFOP|CST|CSOSN|ORIGEM|PIS|COFINS|CEST|IPI|MVA|FCP|CLASSTRIB|IBS|CBS|NCM|ICMS|IVA|MARGEM|ALIQ|NATUREZA|CRT|REGIME|TRIB|PERC|BASE_", re.I)
conn = _connect(os.environ); cur = conn.cursor()
cur.execute("select trim(RDB$RELATION_NAME), case when RDB$VIEW_SOURCE is null then 0 else 1 end from RDB$RELATIONS where coalesce(RDB$SYSTEM_FLAG,0)=0")
rels = cur.fetchall()
cur.execute("select trim(rf.RDB$RELATION_NAME), trim(rf.RDB$FIELD_NAME), f.RDB$FIELD_TYPE, coalesce(f.RDB$CHARACTER_LENGTH,f.RDB$FIELD_LENGTH), f.RDB$FIELD_SCALE from RDB$RELATION_FIELDS rf join RDB$FIELDS f on f.RDB$FIELD_NAME=rf.RDB$FIELD_SOURCE where coalesce(rf.RDB$SYSTEM_FLAG,0)=0")
cols = {}
for t, c, ty, ln, sc in cur.fetchall():
cols.setdefault(t, []).append((c, ty, ln, sc))
out = {"tables": {}}
for t, isview in rels:
try:
cur.execute(f'select count(*) from "{t}"'); n = cur.fetchone()[0]
except Exception as e:
n = None
fc = [c for c in cols.get(t, []) if KW.search(c[0])]
out["tables"][t] = {"rows": n, "view": bool(isview), "ncols": len(cols.get(t, [])), "fiscal_cols": [c[0] for c in fc]}
cur.execute("select trim(RDB$PROCEDURE_NAME) from RDB$PROCEDURES where coalesce(RDB$SYSTEM_FLAG,0)=0"); out["procedures"] = [r[0] for r in cur.fetchall()]
cur.execute("select trim(RDB$TRIGGER_NAME), trim(RDB$RELATION_NAME) from RDB$TRIGGERS where coalesce(RDB$SYSTEM_FLAG,0)=0"); out["triggers"] = [list(r) for r in cur.fetchall()]
cur.execute("""select trim(rc.RDB$RELATION_NAME), trim(s.RDB$FIELD_NAME), trim(rc2.RDB$RELATION_NAME) from RDB$RELATION_CONSTRAINTS rc
join RDB$REF_CONSTRAINTS r on r.RDB$CONSTRAINT_NAME=rc.RDB$CONSTRAINT_NAME join RDB$RELATION_CONSTRAINTS rc2 on rc2.RDB$CONSTRAINT_NAME=r.RDB$CONST_NAME_UQ
join RDB$INDEX_SEGMENTS s on s.RDB$INDEX_NAME=rc.RDB$INDEX_NAME where rc.RDB$CONSTRAINT_TYPE='FOREIGN KEY'""")
out["fks"] = [list(r) for r in cur.fetchall()]
conn.close()
print(json.dumps(out, ensure_ascii=False))
+144
View File
@@ -0,0 +1,144 @@
"""Extract control->column bindings from the SGV executable's embedded Delphi binary DFM forms (READ-ONLY).
python scripts/extract_sgv_dfm_bindings.py <SGV.exe> [--needle COD_ORIGEM_MERCADORIA] [--out file.json]
Reads the exe bytes (never modifies it), locates every 'TPF0' form stream that contains the needle, parses the
Delphi binary DFM and reports, for every data-aware control, its DataField/DataSource, Caption/Text-like properties
and the chain of parent containers (group boxes, tab sheets) — i.e. the on-screen label context.
"""
import argparse
import json
import re
import struct
import sys
VA = dict(NULL=0, LIST=1, INT8=2, INT16=3, INT32=4, EXTENDED=5, STRING=6, IDENT=7, FALSE=8, TRUE=9, BINARY=10, SET=11, LSTRING=12, NIL=13,
COLLECTION=14, SINGLE=15, CURRENCY=16, DATE=17, WSTRING=18, INT64=19, UTF8=20, DOUBLE=21)
class R:
def __init__(self, data, pos=0):
self.d, self.p = data, pos
def u8(self):
v = self.d[self.p]; self.p += 1; return v
def take(self, n):
v = self.d[self.p:self.p + n]
if len(v) < n:
raise ValueError("eof")
self.p += n; return v
def sstr(self):
return self.take(self.u8()).decode("latin-1")
def value(self):
t = self.u8()
if t == VA["NULL"] or t == VA["NIL"]: return None
if t == VA["LIST"]:
out = []
while self.d[self.p] != 0:
out.append(self.value())
self.p += 1; return out
if t == VA["INT8"]: return struct.unpack("<b", self.take(1))[0]
if t == VA["INT16"]: return struct.unpack("<h", self.take(2))[0]
if t == VA["INT32"]: return struct.unpack("<i", self.take(4))[0]
if t == VA["EXTENDED"]: self.take(10); return "<ext>"
if t in (VA["STRING"], VA["IDENT"]): return self.sstr()
if t == VA["FALSE"]: return False
if t == VA["TRUE"]: return True
if t == VA["BINARY"]:
n = struct.unpack("<I", self.take(4))[0]; self.take(n); return "<binary>"
if t == VA["SET"]:
out = []
while True:
s = self.sstr()
if not s: break
out.append(s)
return out
if t == VA["LSTRING"] or t == VA["UTF8"]:
n = struct.unpack("<I", self.take(4))[0]; return self.take(n).decode("utf-8", "replace" if t == VA["UTF8"] else "replace")
if t == VA["WSTRING"]:
n = struct.unpack("<I", self.take(4))[0]; return self.take(n * 2).decode("utf-16-le", "replace")
if t == VA["SINGLE"]: self.take(4); return "<single>"
if t == VA["CURRENCY"] or t == VA["DATE"] or t == VA["DOUBLE"] or t == VA["INT64"]: self.take(8); return "<8b>"
if t == VA["COLLECTION"]:
out = []
while self.d[self.p] != 0:
if self.d[self.p] in (2, 3, 4): # optional collection order value
self.value()
if self.d[self.p] == 1: # vaList marker that opens every collection item
self.p += 1
item = {}
while self.d[self.p] != 0:
key = self.sstr() # evaluate the name BEFORE the value (Python evaluates the RHS first)
item[key] = self.value()
self.p += 1; out.append(item)
self.p += 1; return out
raise ValueError(f"unknown value type {t} at {self.p}")
def component(self):
first = self.u8()
if first & 0xF0 == 0xF0: # ObjInherit / ObjFlags prefix
if first & 2: # ffChildPos
self.value()
cls = self.sstr()
else:
self.p -= 1; cls = self.sstr()
name = self.sstr()
props = {}
while self.d[self.p] != 0:
pn = self.sstr(); props[pn] = self.value()
self.p += 1
kids = []
while self.d[self.p] != 0:
kids.append(self.component())
self.p += 1
return {"class": cls, "name": name, "props": props, "children": kids}
def walk(node, chain, out):
cap = node["props"].get("Caption")
here = chain + ([f"{node['class']}:{node['name']}" + (f"[{cap}]" if isinstance(cap, str) and cap else "")] if node["class"] != "TQuery" else [])
p = node["props"]
if "DataField" in p or node["class"].startswith(("TLabel", "TRz", "TGroup", "TTab", "TRadio")):
out.append({"class": node["class"], "name": node["name"], "DataField": p.get("DataField"), "DataSource": p.get("DataSource"), "Caption": cap,
"Items": p.get("Items"), "Values": p.get("Values"), "FocusControl": p.get("FocusControl"), "Left": p.get("Left"), "Top": p.get("Top"), "path": [c for c in chain]})
for k in node["children"]:
walk(k, here, out)
def find_forms(data, needle):
forms = []
for m in re.finditer(b"TPF0", data):
start = m.start()
try:
r = R(data, start + 4)
tree = r.component()
except Exception:
continue
blob = data[start:r.p]
if needle in blob:
forms.append((start, r.p, tree))
return forms
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("exe"); ap.add_argument("--needle", default="COD_ORIGEM_MERCADORIA"); ap.add_argument("--out")
a = ap.parse_args(argv)
data = open(a.exe, "rb").read()
forms = find_forms(data, a.needle.encode())
result = []
for start, end, tree in forms:
controls = []
walk(tree, [], controls)
result.append({"offset": start, "size": end - start, "form_class": tree["class"], "form_name": tree["name"], "controls": controls})
text = json.dumps(result, ensure_ascii=False, indent=1, default=str)
if a.out:
open(a.out, "w", encoding="utf-8").write(text)
print(f"forms containing {a.needle!r}: {[(f['form_class'], f['size'], len(f['controls'])) for f in result]}")
if __name__ == "__main__":
main()
+368
View File
@@ -0,0 +1,368 @@
"""Minimal guarded Firebird write executor for the MVP.
Scope is intentionally narrow: a single allowlisted table/field
(C000025.IPI), a single authorized database file, and an optimistic
compare-before-write. This is not a generic ORM/executor framework.
Per CLAUDE.md's "Firebird write policy": writes are only ever permitted
against the disposable test database TESTESOFICIAIS.FDB. Any other path,
or any table/field outside the allowlist, fails closed before touching
the connection.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any
AUTHORIZED_WRITE_DATABASE_PATH = Path(r"D:\PROJETOS\GTISISTEMAS\Projeto\TESTESOFICIAIS.FDB")
# table -> allowed writable fields. Anything not listed here is refused.
_FIELD_ALLOWLIST: dict[str, set[str]] = {
# C000025.ALIQ_IPI is VARCHAR(1) and unused (0/521 rows) — confirmed via
# the vendor's own DDL scripts and live schema inspection to NOT be the
# IPI rate field. C000025.IPI NUMERIC(15,2) is. See Mission notes.
"C000025": {"IPI"},
}
# Local completion mission: every field with WRITE_ALLOWED=true in docs/FISCAL_FIELD_CONTRACT_V1.json (kept equal to
# central.app.field_contract.WRITE_FIELDS by a test). Still C000025 only; still exact-path authorized.
_LOCAL_COMPLETION_FIELDS = {"CLASSIFICACAO_FISCAL", "IPI", "EX_TIPI", "CEST", "SITUACAO_TRIBUTARIA", "CFOP", "CFOP_ENTRADA", "CST", "CST_ENTRADA", "CSOSN", "ALIQUOTA", "COD_ORIGEM_MERCADORIA",
"CST_IPI", "COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS", "INCIDENCIA_PISCOFINS", "PISCOFINS", "IAT", "IPPT"}
_LOCAL_ALLOWLIST_FILE = Path(__file__).resolve().parents[1] / "docs" / "LOCAL_WRITE_ALLOWLIST_V1.json"
_KEY_FIELD = "CODIGO"
class WriteRefused(Exception):
"""Raised whenever a guard rejects the write before it reaches the database."""
def _normalized_path(path: os.PathLike[str] | str) -> str:
return os.path.normcase(os.path.normpath(str(Path(path).resolve(strict=False))))
def assert_authorized_write_path(
path: os.PathLike[str] | str,
authorized_path: os.PathLike[str] | str | None = None,
) -> Path:
"""Fail closed unless `path` is exactly the authorized write target.
`authorized_path` defaults to the development disposable test database
(`AUTHORIZED_WRITE_DATABASE_PATH`) for backward compatibility with the
dev environment. A pilot/client installation MUST pass its own
`authorized_path` explicitly (from that installation's own config,
e.g. `AUTHORIZED_WRITE_DATABASE_PATH` env var) — authorization is never
inferred from the filename or extension alone, and never defaults to
"any .FDB is fine". If a caller sets FIREBIRD_WRITES=1 in a pilot
environment without also setting an explicit authorized_path, this
still falls back to the dev-only path and therefore fails closed for
any real client database, which is the safe behavior.
"""
candidate = Path(path)
authorized = Path(authorized_path) if authorized_path else AUTHORIZED_WRITE_DATABASE_PATH
if _normalized_path(candidate) != _normalized_path(authorized):
raise WriteRefused(f"path is not the authorized write target: {candidate}")
return authorized
def assert_schema_fingerprint_authorized(observed_fingerprint: str | None, expected_fingerprint: str | None) -> None:
"""Fail closed unless the live schema fingerprint matches what this
installation was registered/configured to expect. Prevents writing
against a database whose schema silently changed or was never verified."""
if not expected_fingerprint:
raise WriteRefused("no expected schema fingerprint configured for this installation")
if observed_fingerprint != expected_fingerprint:
raise WriteRefused(f"schema fingerprint mismatch: expected {expected_fingerprint!r}, observed {observed_fingerprint!r}")
def assert_allowed_field(table: str, field: str) -> None:
allowed = _FIELD_ALLOWLIST.get(table.upper())
if not allowed or field.upper() not in allowed:
raise WriteRefused(f"{table}.{field} is not in the write allowlist")
def assert_write_eligible(proposal: dict[str, Any]) -> None:
if not proposal.get("write_eligible"):
raise WriteRefused(
f"proposal for {proposal.get('product_code')}/{proposal.get('field')} "
f"is not write_eligible (status={proposal.get('status')})"
)
def assert_schema_ok(cursor: Any, table: str, field: str, proposed_value: Any = None) -> None:
"""Confirm the target column exists and, if it is a fixed-length text field,
that the proposed value actually fits without truncation. Never coerce or
truncate a value to make it fit."""
cursor.execute(
"SELECT f.RDB$FIELD_TYPE, f.RDB$FIELD_LENGTH FROM RDB$RELATION_FIELDS rf "
"JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE "
"WHERE rf.RDB$RELATION_NAME = ? AND rf.RDB$FIELD_NAME = ?",
(table, field),
)
row = cursor.fetchone()
if row is None:
raise WriteRefused(f"schema guard failed: {table}.{field} not found in live schema")
field_type, field_length = row
# Firebird character types (CHAR=14, VARCHAR=37): reject a value that would truncate.
if proposed_value is not None and field_type in (14, 37) and field_length is not None:
if len(str(proposed_value)) > field_length:
raise WriteRefused(
f"schema guard failed: {table}.{field} is {field_length} char(s) wide, "
f"proposed value {proposed_value!r} ({len(str(proposed_value))} chars) would truncate"
)
@dataclass(frozen=True)
class WriteAuditRecord:
product_code: str
field: str
before_value: Any
proposed_value: Any
after_value: Any
rowcount: int
source_dataset: str | None
source_version: str | None
timestamp: str
def as_dict(self) -> dict[str, Any]:
return {
"product_code": self.product_code,
"field": self.field,
"before_value": self.before_value,
"proposed_value": self.proposed_value,
"after_value": self.after_value,
"rowcount": self.rowcount,
"source_dataset": self.source_dataset,
"source_version": self.source_version,
"timestamp": self.timestamp,
}
def execute_guarded_update(
connection: Any,
*,
database_path: os.PathLike[str] | str,
proposal: dict[str, Any],
table: str = "C000025",
key_field: str = _KEY_FIELD,
now: str,
authorized_path: os.PathLike[str] | str | None = None,
) -> WriteAuditRecord:
"""Perform one allowlisted, compare-before-write UPDATE and verify it.
Never commits if any guard fails. Returns an audit record with no secrets.
`authorized_path` defaults to the dev disposable test database; a pilot
installation must pass its own registered path explicitly.
"""
assert_authorized_write_path(database_path, authorized_path=authorized_path)
assert_write_eligible(proposal)
field = proposal["field"]
assert_allowed_field(table, field)
product_code = proposal["product_code"]
expected_before = proposal["current_value"]
proposed_value = proposal["proposed_value"]
if proposed_value is None:
raise WriteRefused("refuse to write a null proposed_value")
cursor = connection.cursor()
try:
assert_schema_ok(cursor, table, field, proposed_value)
cursor.execute(
f"SELECT {key_field}, {field} FROM {table} WHERE {key_field} = ?",
(product_code,),
)
row = cursor.fetchone()
if row is None:
raise WriteRefused(f"product {product_code} not found in {table}")
live_before = row[1]
if live_before != expected_before:
raise WriteRefused(
f"drift detected for {product_code}.{field}: expected {expected_before!r}, "
f"found {live_before!r}"
)
if expected_before is None:
sql = f"UPDATE {table} SET {field} = ? WHERE {key_field} = ? AND {field} IS NULL"
params = (proposed_value, product_code)
else:
sql = f"UPDATE {table} SET {field} = ? WHERE {key_field} = ? AND {field} = ?"
params = (proposed_value, product_code, expected_before)
cursor.execute(sql, params)
rowcount = cursor.rowcount
if rowcount != 1:
connection.rollback()
raise WriteRefused(f"expected rowcount 1, got {rowcount}; rolled back")
connection.commit()
cursor.execute(f"SELECT {field} FROM {table} WHERE {key_field} = ?", (product_code,))
after_row = cursor.fetchone()
after_value = after_row[0] if after_row else None
if after_value != proposed_value:
raise WriteRefused(
f"post-write verification failed for {product_code}.{field}: "
f"expected {proposed_value!r}, found {after_value!r}"
)
return WriteAuditRecord(
product_code=product_code,
field=field,
before_value=live_before,
proposed_value=proposed_value,
after_value=after_value,
rowcount=rowcount,
source_dataset=proposal.get("source_dataset"),
source_version=proposal.get("source_version"),
timestamp=now,
)
except Exception:
try:
connection.rollback()
except Exception:
pass
raise
finally:
cursor.close()
def execute_guarded_restore(
connection: Any,
*,
database_path: os.PathLike[str] | str,
table: str,
field: str,
key_field: str,
product_code: str,
written_value: Any,
restore_value: Any,
now: str,
) -> WriteAuditRecord:
"""Restore a field to `restore_value`, guarded the same way as the forward write."""
assert_authorized_write_path(database_path)
assert_allowed_field(table, field)
cursor = connection.cursor()
try:
assert_schema_ok(cursor, table, field)
cursor.execute(f"SELECT {field} FROM {table} WHERE {key_field} = ?", (product_code,))
row = cursor.fetchone()
if row is None:
raise WriteRefused(f"product {product_code} not found in {table}")
if row[0] != written_value:
raise WriteRefused(f"drift before restore: expected {written_value!r}, found {row[0]!r}")
if restore_value is None:
sql = f"UPDATE {table} SET {field} = NULL WHERE {key_field} = ? AND {field} = ?"
params = (product_code, written_value)
else:
sql = f"UPDATE {table} SET {field} = ? WHERE {key_field} = ? AND {field} = ?"
params = (restore_value, product_code, written_value)
cursor.execute(sql, params)
rowcount = cursor.rowcount
if rowcount != 1:
connection.rollback()
raise WriteRefused(f"expected rowcount 1 on restore, got {rowcount}; rolled back")
connection.commit()
cursor.execute(f"SELECT {field} FROM {table} WHERE {key_field} = ?", (product_code,))
after_row = cursor.fetchone()
after_value = after_row[0] if after_row else None
if after_value != restore_value:
raise WriteRefused(
f"post-restore verification failed: expected {restore_value!r}, found {after_value!r}"
)
return WriteAuditRecord(
product_code=product_code,
field=field,
before_value=written_value,
proposed_value=restore_value,
after_value=after_value,
rowcount=rowcount,
source_dataset=None,
source_version=None,
timestamp=now,
)
except Exception:
try:
connection.rollback()
except Exception:
pass
raise
finally:
cursor.close()
def load_local_allowlist(path=None):
"""Exact canonical paths authorized for the local completion mission (never by directory/extension)."""
import json
doc = json.loads(Path(path or _LOCAL_ALLOWLIST_FILE).read_text(encoding="utf-8"))
return {os.path.normcase(os.path.abspath(d["path"])) for d in doc["databases"]}
def assert_local_completion_target(database_path, allowlist=None) -> None:
allow = allowlist if allowlist is not None else load_local_allowlist()
if os.path.normcase(os.path.abspath(str(database_path))) not in allow:
raise WriteRefused(f"{database_path} is not in the exact local-completion write allowlist")
def execute_guarded_product_update(connection, *, database_path, product_code, changes: dict, now: str, allowlist=None, table: str = "C000025", key_field: str = _KEY_FIELD, restore_before: bool = False):
"""One transaction per product: every field is compare-before-write, ONE UPDATE, rowcount must be 1, then an independent re-read.
changes: {field: (expected_before, proposed)}. restore_before=True (rollback of an audited write to its recorded BEFORE) additionally allows a NULL target. Refuses any field outside the local-completion field set, any null proposal,
any path outside the exact allowlist, and any value that would truncate. Returns an audit dict (no secrets)."""
assert_local_completion_target(database_path, allowlist)
if table != "C000025":
raise WriteRefused("only C000025 may be written")
if not changes:
raise WriteRefused("no changes")
for field, (_, proposed) in changes.items():
if field not in _LOCAL_COMPLETION_FIELDS:
raise WriteRefused(f"{table}.{field} is not a contract write target")
if proposed is None and not restore_before:
raise WriteRefused(f"refuse to write a null proposed_value for {field}")
cursor = connection.cursor()
try:
for field, (_, proposed) in changes.items():
if proposed is not None:
assert_schema_ok(cursor, table, field, proposed)
fields = list(changes)
cursor.execute(f"SELECT {', '.join(fields)} FROM {table} WHERE {key_field} = ?", (product_code,))
row = cursor.fetchone()
if row is None:
raise WriteRefused(f"product {product_code} not found in {table}")
for field, live in zip(fields, row):
expected = changes[field][0]
if _norm(live) != _norm(expected):
raise WriteRefused(f"drift detected for {product_code}.{field}: expected {expected!r}, found {live!r}")
sets = ", ".join(f"{f} = ?" for f in fields)
cursor.execute(f"UPDATE {table} SET {sets} WHERE {key_field} = ?", tuple(changes[f][1] for f in fields) + (product_code,))
if cursor.rowcount != 1:
connection.rollback()
raise WriteRefused(f"expected rowcount 1, got {cursor.rowcount}; rolled back")
connection.commit()
cursor.execute(f"SELECT {', '.join(fields)} FROM {table} WHERE {key_field} = ?", (product_code,))
after = cursor.fetchone()
bad = [f for f, a in zip(fields, after) if _norm(a) != _norm(changes[f][1])]
if bad:
raise WriteRefused(f"post-write verification failed for {product_code}: {bad}")
return {"product_code": product_code, "rowcount": 1, "fields": {f: {"before": changes[f][0], "proposed": changes[f][1], "after": a} for f, a in zip(fields, after)}, "timestamp": now}
except Exception:
try:
connection.rollback()
except Exception:
pass
raise
finally:
cursor.close()
def _norm(v):
"""Comparable form: text trimmed, numbers as float (Decimal/int/float from the driver), None stays None."""
if v is None:
return None
if isinstance(v, (int, float)) or v.__class__.__name__ == "Decimal":
return round(float(v), 6)
return str(v).strip()
+19
View File
@@ -0,0 +1,19 @@
"""Compare the cell states before/after a source refresh: gate_before/*_second.ndjson.gz vs local_completion/<stage>.ndjson.gz."""
import gzip, json, sys
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]; stage = sys.argv[1] if len(sys.argv) > 1 else "dryrun"
DBS = ["CONSTRUCAO", "MMMODAS", "RINCAO", "UNIAO", "TESTESOFICIAIS"]
trans = Counter(); ex = {}; man_b = man_a = 0
for n in DBS:
old = {}
for l in gzip.open(ROOT / "docs/runs/gate_before" / f"{n}_second.ndjson.gz", "rt", encoding="utf-8"):
r = json.loads(l); old[(r["row_key"], r["field"])] = r
for l in gzip.open(ROOT / "docs/runs/local_completion" / f"{n}_{stage}.ndjson.gz", "rt", encoding="utf-8"):
r = json.loads(l); o = old[(r["row_key"], r["field"])]
man_b += o["status"] == "MANUAL_INPUT_REQUIRED"; man_a += r["status"] == "MANUAL_INPUT_REQUIRED"
if o["status"] != r["status"] or o["proposed"] != r["proposed"]:
k = (n, r["field"], o["status"], o["reason"], "->", r["status"], r["reason"][:40]); trans[k] += 1
ex.setdefault(k, (r["codigo"], o["before"], r["proposed"]))
print("MANUAL before", man_b, "after", man_a)
for k, v in trans.most_common(30): print(v, k, ex[k])
+117
View File
@@ -0,0 +1,117 @@
"""Generate the dry-run ERP write plan artifact for the 20-product sample.
Calls the running fiscal-hub /v1/classify/batch (read-only classification,
no Firebird access, no dataset creation) and turns each decision into
field-level write proposals via central.app.dry_run_planner. Never issues
an ERP write.
"""
import argparse
import json
import sys
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "central"))
from app.dry_run_planner import build_field_proposals, mva_evidence, split_by_eligibility # noqa: E402
def classify_batch(hub_url, token, products):
payload = {"products": [{"current_ncm": p["CLASSIFICACAO_FISCAL"], "description": p["PRODUTO"]} for p in products]}
request = urllib.request.Request(
f"{hub_url}/v1/classify/batch",
data=json.dumps(payload, ensure_ascii=True).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
method="POST",
)
with urllib.request.urlopen(request) as response:
return json.load(response)["results"]
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--sample", default=str(ROOT / "docs" / "SAMPLE_PRODUCTS_V1.json"))
parser.add_argument("--hub-url", default="http://localhost:8000")
parser.add_argument("--token", required=True)
parser.add_argument("--output", default=str(ROOT / "docs" / "DRY_RUN_PLAN_V1.json"))
parser.add_argument("--artifact-name", default="DRY_RUN_PLAN_V1")
parser.add_argument("--firebird-current", default=None, help="JSON file with fresh CODIGO/IPI/CEST read from Firebird (read-only). Omit to keep current values unknown.")
args = parser.parse_args()
sample = json.loads(Path(args.sample).read_text(encoding="utf-8"))
products = sample["sample_without_ncm"]
decisions = classify_batch(args.hub_url, args.token, products)
fresh_by_code = {}
if args.firebird_current:
for row in json.loads(Path(args.firebird_current).read_text(encoding="utf-8")):
fresh_by_code[row["CODIGO"]] = row
field_proposals, mva_records = [], []
for product, decision in zip(products, decisions):
code = product["CODIGO"]
fresh = fresh_by_code.get(code)
if fresh is not None:
current = {"ipi": fresh.get("IPI"), "cest": fresh.get("CEST"), "ex_tipi": fresh.get("EX_TIPI")}
else:
current = {}
if "CEST" in product:
current["cest"] = product["CEST"]
field_proposals.extend(build_field_proposals(code, decision, current))
mva_records.append(mva_evidence(code, decision))
split = split_by_eligibility(field_proposals)
ipi_rows = [p for p in field_proposals if p["field"] == "IPI"]
ex_rows = [p for p in field_proposals if p["field"] == "EX_TIPI"]
cest_rows = [p for p in field_proposals if p["field"] == "CEST"]
ipi_metrics = {
"ipi_numeric_proposals": sum(1 for p in ipi_rows if p["status"] in ("SAME_VALUE", "CHANGE_REQUIRED", "CURRENT_UNKNOWN")),
"ipi_non_numeric_status": sum(1 for p in ipi_rows if p["status"] == "NON_NUMERIC_IPI_STATUS"),
"ipi_source_blank": sum(1 for p in ipi_rows if p["status"] == "SOURCE_BLANK"),
"ipi_same_value": sum(1 for p in ipi_rows if p["status"] == "SAME_VALUE"),
"ipi_change_required": sum(1 for p in ipi_rows if p["status"] == "CHANGE_REQUIRED"),
"ipi_write_eligible": sum(1 for p in ipi_rows if p["write_eligible"]),
# every emitted EX_TIPI row implies TIPI had at least one EX-specific entry for that NCM.
"ex_tipi_db_match_products": len(ex_rows),
"ex_tipi_concrete_proposals": sum(1 for p in ex_rows if p["status"] in ("SAME_VALUE", "CHANGE_REQUIRED", "CURRENT_UNKNOWN")),
"ex_tipi_candidates": sum(1 for p in ex_rows if p["status"] == "CANDIDATE"),
"ex_tipi_write_eligible": sum(1 for p in ex_rows if p["write_eligible"]),
}
cest_metrics = {
"cest_db_match_products": sum(1 for p in cest_rows if p["status"] != "NO_CEST_MAPPING_FOUND" and p["reason_code"] != "NO_CEST_MAPPING_FOUND_FOR_NCM"),
"cest_confirmed": sum(1 for p in cest_rows if p["status"] in ("SAME_VALUE", "CHANGE_REQUIRED", "CURRENT_UNKNOWN")),
"cest_candidates": sum(1 for p in cest_rows if p["status"] == "CANDIDATE"),
"cest_unresolved": sum(1 for p in cest_rows if p["status"] == "UNRESOLVED"),
"cest_write_eligible": sum(1 for p in cest_rows if p["write_eligible"]),
}
artifact = {
"artifact": args.artifact_name,
"firebird_read": "OK" if fresh_by_code else "BLOCKED_EXTERNAL_MISSING_CREDENTIALS",
"firebird_writes": 0,
"products_planned": len(products),
"current_value_source": (
f"live read-only SELECT from C000025 via {args.firebird_current}"
if fresh_by_code else
"docs/SAMPLE_PRODUCTS_V1.json (CEST) — C000025.IPI has no authorized offline source; FB_USER/FB_PASSWORD unavailable in this environment"
),
"field_proposals": field_proposals,
"mva_evidence": mva_records,
"ipi_metrics": ipi_metrics,
"cest_metrics": cest_metrics,
"write_eligible_count": len(split["eligible"]),
"write_blocked_count": len(split["blocked"]),
}
Path(args.output).write_text(json.dumps(artifact, indent=2, ensure_ascii=False), encoding="utf-8")
print(json.dumps({
"products": len(products),
"field_proposals": len(field_proposals),
"write_eligible": len(split["eligible"]),
"write_blocked": len(split["blocked"]),
"output": args.output,
}, indent=2))
if __name__ == "__main__":
main()
+193
View File
@@ -0,0 +1,193 @@
"""Compose ERP_FISCAL_FIELD_MAP_V1, FISCAL_CONTEXT_REQUIREMENTS_V1, FISCAL_CONTEXT_COVERAGE_V1 and
NEXT_FISCAL_FIELD_IMPLEMENTATION_V1 from the read-only discovery outputs (docs/runs/*).
No database access."""
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOCS = ROOT / "docs"
schema = json.loads((DOCS / "runs" / "erp_schema_inventory.json").read_text(encoding="utf-8"))
hist = json.loads((DOCS / "runs" / "erp_fiscal_history_analysis.json").read_text(encoding="utf-8"))
extra = json.loads((DOCS / "runs" / "erp_fiscal_history_extra.json").read_text(encoding="utf-8"))
audit = json.loads((DOCS / "runs" / "erp_c000025_audit.json").read_text(encoding="utf-8"))
cov_v1 = json.loads((DOCS / "FISCAL_DECISION_COVERAGE_V1.json").read_text(encoding="utf-8"))
tables = schema["tables"]
fiscal_tables = {t: v for t, v in tables.items() if v["fiscal_cols"] and v["rows"]}
consistent = extra["agreement_consistent_joins_only"]
nf = hist["nfce_join"]
items_analyzed = nf["items_joined_to_C000025"] + hist["C000032"]["joined"]["JOINED"] + hist["C000062"]["joined"]["JOINED"]
VENDOR = "vendor scripts D:/PROJETOS/GTISISTEMAS/Diretorios/rel/Novo_Script_Banco.sql + atualiza/v3.5.5.NFCe/script/*"
def f(table, field, dbtype, semantic, usage, conf, evidence, sources, samples, write=False):
return {"table": table, "field": field, "db_type": dbtype, "semantic_name": semantic, "usage_class": usage, "confidence": conf,
"evidence": evidence, "source_objects": sources, "sample_values": samples, "write_candidate": write}
agree_all = nf["agreement_item_vs_product_default"]
FIELDS = [
f("C000025", "CFOP", "VARCHAR(4)", "CFOP padrão de saída do produto", "PRODUCT_DEFAULT", "HIGH",
f"NFC-e item CFOP equals product default in {agree_all['CFOP']['MATCH']}/{agree_all['CFOP']['MATCH']+agree_all['CFOP']['MISMATCH']} joined items (identity-consistent: {consistent['CFOP']['MATCH']}/{consistent['CFOP']['MATCH']+consistent['CFOP']['MISMATCH']}); values are official 5xxx CFOPs; item CFOP is the emitted value. Still operation-dependent (C000030 naturezas carry 5102/5405/6102...).",
["C000025", "NFCE_ITENS", "C000030"], ["5102", "5405", "5656"]),
f("C000025", "CFOP_ENTRADA", "CHAR(4)", "CFOP padrão de entrada (campo existe no DDL do fornecedor)", "LEGACY_UNUSED", "MEDIUM",
f"0/521 populated; DDL exists ({VENDOR}); company-level default lives in C000001.IND_CFOP_ENTRADA=1949", ["C000025", "C000001"], [None]),
f("C000025", "CST", "VARCHAR(10)", "CST ICMS de saída = origem(1)+CST(2)", "PRODUCT_DEFAULT", "HIGH",
f"vendor table C000083 (11 rows 000..090 'NACIONAL ...') defines CST as 3-digit origin+CST; NFC-e item CST matches product default in {consistent['CST']['MATCH']}/{consistent['CST']['MATCH']} identity-consistent joins",
["C000025", "C000083", "NFCE_ITENS"], ["040", "000", "060"]),
f("C000025", "CST_ENTRADA", "CHAR(4)", "CST padrão de entrada", "LEGACY_UNUSED", "MEDIUM", "0/521 populated; DDL exists in vendor scripts", ["C000025"], [None]),
f("C000025", "CSOSN", "VARCHAR(60)", "CSOSN de saída (Simples Nacional)", "PRODUCT_DEFAULT", "HIGH",
f"NFC-e item CSOSN equals product default in {consistent['CSOSN']['MATCH']}/{consistent['CSOSN']['MATCH']+consistent['CSOSN']['MISMATCH']} identity-consistent joins; company default C000004.ICMS_CSOSN=102; values 101/102/103/500 in CSOSN dataset",
["C000025", "C000004", "NFCE_ITENS"], ["102", "500", "101"]),
f("C000025", "GRUPO_ICMS", "VARCHAR(9)", "grupo ICMS do produto (espelha CSOSN/ST nas NFC-e)", "PRODUCT_DEFAULT", "MEDIUM",
"487 '102', 2 '90'; NFCE_ITENS.GRUPO_ICMS carries 102/500/blank; exact semantics not documented", ["C000025", "NFCE_ITENS"], ["102", "90"]),
f("C000025", "ORIGEM", "VARCHAR(1)", "origem (campo textual legado)", "LEGACY_UNUSED", "HIGH", "0/521 and 0/4905 (C000032.ORIGEM) populated", ["C000025", "C000032"], [None]),
f("C000025", "COD_ORIGEM_MERCADORIA", "INTEGER", "código de origem da mercadoria (0-8)", "PRODUCT_DEFAULT", "LOW",
f"exists in vendor DDL as INTEGER; only 1/521 populated (value 2); origin is otherwise embedded in CST digit 1; not used in any emitted NFC-e column", ["C000025"], [2]),
f("C000025", "COD_ENTRADA_PIS", "VARCHAR(3)", "CST PIS de entrada", "LEGACY_UNUSED", "LOW", "2/521 populated with inconsistent formatting ('2','73')", ["C000025"], ["2", "73"]),
f("C000025", "COD_SAIDA_PIS", "VARCHAR(3)", "CST PIS de saída", "LEGACY_UNUSED", "LOW",
"3/521 populated ('01','04','2'); NFC-e items carry CST_PIS_COFINS '01' regardless, so the product field is not the source", ["C000025", "NFCE_ITENS"], ["01", "04", "2"]),
f("C000025", "COD_ENTRADA_COFINS", "VARCHAR(3)", "CST COFINS de entrada", "LEGACY_UNUSED", "LOW", "2/521 populated", ["C000025"], ["2", "73"]),
f("C000025", "COD_SAIDA_COFINS", "VARCHAR(3)", "CST COFINS de saída", "LEGACY_UNUSED", "LOW", "3/521 populated", ["C000025"], ["01", "04", "2"]),
f("C000025", "ALIQ_PIS", "VARCHAR(8)", "alíquota PIS (texto)", "LEGACY_UNUSED", "LOW", "3/521 populated; NFC-e ALIQUOTA_PIS is always 0 (Simples)", ["C000025", "NFCE_ITENS"], ["0,65", "2"]),
f("C000025", "ALIQ_COFINS", "VARCHAR(8)", "alíquota COFINS (texto)", "LEGACY_UNUSED", "LOW", "3/521 populated; NFC-e ALIQUOTA_COFINS is always 0", ["C000025", "NFCE_ITENS"], ["2", "3"]),
f("C000025", "CST_IPI", "SMALLINT", "CST do IPI", "LEGACY_UNUSED", "LOW", "1/521 populated ('040', 3 digits although IPI CST has 2)", ["C000025"], ["040"]),
f("C000025", "INCIDENCIA_PISCOFINS", "VARCHAR(30)", "tipo de incidência PIS/COFINS do produto", "PRODUCT_DEFAULT", "HIGH", "NENHUMA 519, MONOFÁSICA 1, SUBST.TRIBUTÁRIA 1; ties to C000025.PISCOFINS flag (N/S)", ["C000025"], ["NENHUMA", "MONOFÁSICA"]),
f("C000025", "CEST", "VARCHAR(7)", "CEST do produto (7 dígitos sem pontuação)", "PRODUCT_DEFAULT", "HIGH",
f"NFC-e item CEST digits equal product CEST in {consistent['CEST']['MATCH']}/{consistent['CEST']['MATCH']+consistent['CEST']['MISMATCH']} identity-consistent joins", ["C000025", "NFCE_ITENS"], ["0601100", "1708400"], write=True),
f("C000025", "IPI", "DOUBLE PRECISION", "alíquota de IPI %", "PRODUCT_DEFAULT", "HIGH", "confirmed in earlier missions and post-write verified; 109 populated", ["C000025"], [19.5, 3.25], write=True),
f("C000025", "EX_TIPI", "VARCHAR(3)", "código EX da TIPI", "PRODUCT_DEFAULT", "HIGH", "confirmed in earlier missions; 0 populated", ["C000025"], [None], write=True),
f("C000025", "ALIQUOTA", "NUMERIC(15,2)", "alíquota ICMS % do produto", "UNKNOWN", "LOW",
"product default 20.5 (492 rows) but NFC-e items use ALIQUOTA 18 (29825 rows) — product value is not what is emitted", ["C000025", "NFCE_ITENS"], [20.5, 17]),
f("C000025", "MARGEM_AGREGADA", "NUMERIC(15,2)", "candidato a MVA/margem de ST", "LEGACY_UNUSED", "LOW",
f"0/521 populated; C000062.MARGEM_AGREGADA is 0.0 in all 693 rows even though ICMS_SUB<>0 in {hist['C000062_mva_usage'][2]} rows (no calculation use evidenced); group-level PRODUTOS_MARGEM_AGREGADA(MVA_INDUSTRIA/MVA_DISTRIBUIDOR) has {hist['PRODUTOS_MARGEM_AGREGADA_rows']} rows and no link to C000025",
["C000025", "C000062", "PRODUTOS_MARGEM_AGREGADA"], [None]),
f("C000004", "CRT", "VARCHAR(1)", "código de regime tributário da empresa", "COMPANY_DEPENDENT", "HIGH", "value 1; EMISSOR.EMITENTE_CRT=1 agrees; OPTANTE_SIMPLES=S", ["C000004", "EMISSOR"], ["1"]),
f("C000004", "UF", "VARCHAR(2)", "UF do emitente", "COMPANY_DEPENDENT", "HIGH", "BA; matches IBGE 2928703 (Bahia)", ["C000004"], ["BA"]),
f("C000004", "ICMS_CSOSN", "VARCHAR(150)", "CSOSN padrão da empresa", "COMPANY_DEPENDENT", "MEDIUM", "value 102; equals 487/521 product defaults", ["C000004"], ["102"]),
f("C000004", "SUBSTITUTO_TRIBUTARIO / CONTRIBUINTE_IPI / OPTANTE_SIMPLES", "VARCHAR(1)", "flags fiscais da empresa", "COMPANY_DEPENDENT", "HIGH", "N / N / S", ["C000004"], ["N", "N", "S"]),
f("C000030", "CFOP,NATUREZA,TIPO", "VARCHAR/INTEGER", "cadastro de naturezas de operação (TIPO 1=entrada 272, 2=saída 318)", "OPERATION_DEFAULT", "HIGH",
f"590 rows loaded by vendor script 'Script CFOP C000030.txt'; CFOP first digit distribution {hist['C000030_cfop_first_digit']}; contains 5102/5405/5656/6102/1102", ["C000030"], ["1102 Compra p/ comercialização", "5102"]),
f("C000001", "IND_CFOP_ENTRADA/SAIDA/PERDA", "VARCHAR(5)", "CFOP padrão da empresa por finalidade", "OPERATION_DEFAULT", "HIGH", f"values {hist['C000001_cfop_defaults']} (1949/5949/5927)", ["C000001"], hist["C000001_cfop_defaults"]),
f("NFCE_ITENS", "CFOP,CST,GRUPO_ICMS,CSOSN,NCM,CEST,CST_PIS_COFINS", "mixed", "valores efetivamente emitidos em NFC-e (34,946 itens 2018-03..2026-08)", "DOCUMENT_CALCULATED", "HIGH",
"item-level emitted fiscal values; NCM/CEST/CSOSN/CST_PIS_COFINS populated only on 4,668 newer items (2022-06+)", ["NFCE_ITENS", "NFCE"], ["5102", "040", "102", "01"]),
f("NFCE_ITENS", "ALIQUOTA_PIS/ALIQUOTA_COFINS/BASE_CALCULO_*", "DOUBLE/NUMERIC", "PIS/COFINS emitido", "DOCUMENT_CALCULATED", "HIGH", "always 0 on 4,668 items (Simples Nacional: no PIS/COFINS calculation in NFC-e)", ["NFCE_ITENS"], [0]),
f("NFE_ALIQUOTA_INTERESTADUAL", "ICMS,ORIGEM,...", "mixed", "tabela de alíquotas interestaduais (729 linhas)", "OPERATION_DEFAULT", "MEDIUM", "reference table keyed by UF pair/origin; relevant only for interstate operations", ["NFE_ALIQUOTA_INTERESTADUAL"], []),
]
fmap = {"artifact": "ERP_FISCAL_FIELD_MAP_V1", "database": "TESTESOFICIAIS.FDB (read-only)", "firebird_writes": 0,
"evidence_sources": ["live RDB$ catalog (334 tables)", "value distributions", "NFCE_ITENS history join", VENDOR],
"caveat": "57% of NFC-e items with an NCM carry a different NCM than the product currently has for the same CODPRODUTO (identity drift); history comparisons are therefore reported on identity-consistent joins.",
"fields": FIELDS}
(DOCS / "ERP_FISCAL_FIELD_MAP_V1.json").write_text(json.dumps(fmap, ensure_ascii=False, indent=1), encoding="utf-8")
# ---------------------------------------------------------------- context requirements
AV, CFG, JOB, MISS = "AVAILABLE_FROM_ERP", "AVAILABLE_FROM_AGENT_CONFIG", "AVAILABLE_FROM_JOB", "MISSING"
REQ = {
"CFOP": {"official_basis": "CFOP table (IT 2023.002): direction, UF scope, ST, purpose in title/description/ind* flags", "inputs": {
"direction (entrada/saída)": [AV, "C000030.TIPO (1/2) per natureza; NFC-e is always saída"],
"operation_type / natureza": [AV, "C000030 (590 rows) at document level; not derivable from a product alone"],
"origin_uf": [AV, "C000004.UF=BA"],
"destination_uf": [JOB, "NFC-e is intra-UF by nature; NF-e needs customer UF (customer tables) — absent from a product-only job"],
"customer_taxpayer_status": [MISS, "per-document/customer; not in product request"],
"st_applicability": [AV, "BA_ST_MVA + CEST datasets, only 31 products MVA_RESOLVED"],
"purpose (revenda/consumo/devolução)": [MISS, "only natureza text; no structured field"]}},
"CSOSN": {"official_basis": "MOC CSOSN table + CRT=1", "inputs": {
"CRT": [AV, "C000004.CRT=1, EMISSOR.EMITENTE_CRT=1"],
"company_default_csosn": [AV, "C000004.ICMS_CSOSN=102"],
"st_applicability": [AV, "same as CFOP (partial)"],
"st_already_retained_by_supplier": [MISS, "requires purchase documents per product; not structured"],
"operation_type": [MISS, "document-level"],
"destination_context": [JOB, "document-level"]}},
"CST_ICMS": {"official_basis": "not emitted while CRT=1 (CSOSN used)", "inputs": {
"CRT": [AV, "C000004.CRT"], "regime_change_flag": [CFG, "only if client leaves Simples"], "operation_type": [MISS, "document-level"]}},
"ORIGEM": {"official_basis": "MOC origin table 0-8", "inputs": {
"objective_product_origin (import/national)": [MISS, "C000025.ORIGEM 0/521; COD_ORIGEM_MERCADORIA 1/521; ERP CST digit 1 is a config value not proof"]}},
"PIS_COFINS": {"official_basis": "CST PIS/COFINS table + regime; rate tables 4.3.x deferred", "inputs": {
"regime": [AV, "CRT=1 / OPTANTE_SIMPLES=S (NFC-e PIS/COFINS always 0)"],
"product_treatment": [AV, "C000025.INCIDENCIA_PISCOFINS (519 NENHUMA, 1 MONOFÁSICA, 1 SUBST.TRIBUTÁRIA)"],
"cst_rule_table": [MISS, "no NCM→CST PIS/COFINS rule in acquired datasets (deferred by business scope)"],
"operation_type": [MISS, "document-level"]}},
"FCP": {"official_basis": "FCP rate per UF (dataset) + state item list", "inputs": {
"company_uf": [AV, "C000004.UF=BA"], "destination_uf": [JOB, "document-level"],
"item_in_fcp_list_for_uf": [MISS, "no NCM/CEST list of FCP-subject items in datasets"],
"operation_interstate_or_internal": [JOB, "document-level"]}},
"RTC_CCLASSTRIB": {"official_basis": "cClassTrib table + LC 214 annexes", "inputs": {
"ncm_to_annex_mapping": [MISS, "LC 214 annex NCM lists are not in acquired datasets"],
"operation_nature": [MISS, "document-level"], "customer_type": [MISS, "document-level"], "CRT": [AV, "C000004.CRT"]}},
"RTC_CBS_RATE": {"official_basis": "IT 2026.002: 2026=0.009, 2027 pending legislation", "inputs": {
"cclasstrib_resolved": [MISS, "blocked by RTC_CCLASSTRIB"], "reference_year": [JOB, "document date"]}},
"RTC_CCREDPRES": {"official_basis": "cCredPres official conditions", "inputs": {
"condition_flags (ZFM, presumed credit eligibility)": [MISS, "no ERP field"]}},
"MVA": {"official_basis": "RICMS-BA Anexo 1 item/CEST", "inputs": {
"confirmed_cest": [AV, "only 15 CEST candidates remain, 1 historically supported"], "erp_destination_field": [MISS, "NO_PRODUCT_FIELD proven"]}},
}
n_av = n_cfg = n_job = n_miss = 0
for dom in REQ.values():
for k, (state, _) in dom["inputs"].items():
n_av += state == AV; n_cfg += state == CFG; n_job += state == JOB; n_miss += state == MISS
REQ_DOC = {"artifact": "FISCAL_CONTEXT_REQUIREMENTS_V1", "domains": {d: {"official_basis": v["official_basis"], "inputs": {k: {"status": s, "evidence": e} for k, (s, e) in v["inputs"].items()}} for d, v in REQ.items()},
"summary": {"AVAILABLE_FROM_ERP": n_av, "AVAILABLE_FROM_AGENT_CONFIG": n_cfg, "AVAILABLE_FROM_JOB": n_job, "MISSING": n_miss}}
(DOCS / "FISCAL_CONTEXT_REQUIREMENTS_V1.json").write_text(json.dumps(REQ_DOC, ensure_ascii=False, indent=1), encoding="utf-8")
def ctx(d):
st = [s for s, _ in REQ[d]["inputs"].values()]
return {"available": sum(1 for s in st if s in (AV, CFG)), "job_dependent": st.count(JOB), "missing": st.count(MISS), "total": len(st), "complete": all(s in (AV, CFG) for s in st)}
sv = hist["shadow_validation"]
matches = sum(v.get("MATCH", 0) for k, v in sv.items() if k != "items")
mism = sum(v.get("MISMATCH", 0) for k, v in sv.items() if k != "items")
unres = sum(v.get("UNRESOLVED", 0) for k, v in sv.items() if k != "items")
COV = {"artifact": "FISCAL_CONTEXT_COVERAGE_V1", "TOTAL_PRODUCTS": 521, "firebird_writes": 0,
"ERP_TABLES_INSPECTED": len(tables), "ERP_TABLES_NON_EMPTY": sum(1 for v in tables.values() if v["rows"]),
"ERP_CONTEXT_TABLES_FOUND": sorted(["C000004", "C000001", "C000030", "C000083", "NFE_ALIQUOTA_INTERESTADUAL", "EMISSOR"]),
"ERP_FISCAL_TABLES_FOUND": len(fiscal_tables),
"HISTORICAL_FISCAL_DOCUMENT_TABLES": ["NFCE", "NFCE_ITENS", "C000032", "C000061", "C000062"],
"HISTORICAL_FISCAL_DOCUMENTS_FOUND": {"NFCE": hist["nfce_docs"]["rows"], "C000061": 50},
"HISTORICAL_ITEMS_ANALYZED": items_analyzed,
"CFOP_MAPPING_CONFIRMED": True, "CST_MAPPING_CONFIRMED": True, "CSOSN_MAPPING_CONFIRMED": True,
"PIS_MAPPING_CONFIRMED": False, "COFINS_MAPPING_CONFIRMED": False, "ORIGIN_MAPPING_CONFIRMED": False, "MVA_FIELD_CONFIRMED": False,
"CFOP_CONTEXT_AVAILABLE": ctx("CFOP"), "CSOSN_CONTEXT_AVAILABLE": ctx("CSOSN"), "CST_CONTEXT_AVAILABLE": ctx("CST_ICMS"),
"PIS_COFINS_CONTEXT_AVAILABLE": ctx("PIS_COFINS"), "FCP_CONTEXT_AVAILABLE": ctx("FCP"), "RTC_CONTEXT_AVAILABLE": ctx("RTC_CCLASSTRIB"),
"CONTEXT_AVAILABLE": n_av + n_cfg, "CONTEXT_JOB_DEPENDENT": n_job, "CONTEXT_MISSING": n_miss,
"history_agreement_all_joins": agree_all, "history_agreement_identity_consistent_joins": consistent,
"identity_drift": {"consistent_items": extra["ncm_identity_consistent_items"], "inconsistent_items": extra["ncm_identity_inconsistent_items"]},
"cest_historically_supported": extra["cest_historically_supported"],
"shadow_validation_engine_vs_history": {"items": sv["items"], "MATCH": matches, "MISMATCH": mism, "UNRESOLVED": unres, "by_domain": {k: v for k, v in sv.items() if k != "items"},
"note": "NCM 'mismatch' = product's current NCM differs from the NCM emitted on that CODPRODUTO (identity drift), not an engine error; all operation-dependent domains stay UNRESOLVED by design."}}
(DOCS / "FISCAL_CONTEXT_COVERAGE_V1.json").write_text(json.dumps(COV, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
# ---------------------------------------------------------------- priorities
d = cov_v1["domains"]
def hv(field, ok):
return ok
ROWS = [
# field, mapping, ctx_complete, rule, potential, hist_validation, next_action
("IPI", "CONFIRMED", True, True, d["IPI"]["RULE_RESOLVED"], "n/a in NFC-e items (no IPI column)", "Maintain; add NT/blank policy decision."),
("CEST", "CONFIRMED", True, True, d["CEST"]["RULE_CANDIDATE"], f"{consistent['CEST']['MATCH']}/{consistent['CEST']['MATCH']+consistent['CEST']['MISMATCH']} product==emitted; {extra['cest_historically_supported']}/15 candidates history-supported", "Use NFC-e history as EVIDENCE to narrow candidates; write only single defensible matches."),
("EX_TIPI", "CONFIRMED", True, True, d["EX_TIPI"]["RULE_CANDIDATE"], "none", "Description-evidence rule already in engine; review queue only."),
("NCM", "CONFIRMED", True, True, d["NCM"]["RULE_CANDIDATE"], "identity drift: 57% of NFC-e NCMs differ from current product NCM", "Investigate product-code reuse before trusting history; keep review-only."),
("CFOP", "CONFIRMED", False, False, 521, f"{consistent['CFOP']['MATCH']}/{consistent['CFOP']['MATCH']+consistent['CFOP']['MISMATCH']} product default==emitted", "Model NFC-e sale context (saída, intra-UF, consumidor final) and define an official-table-backed rule; validate vs NFCE_ITENS."),
("CSOSN", "CONFIRMED", False, False, 521, f"{consistent['CSOSN']['MATCH']}/{consistent['CSOSN']['MATCH']+consistent['CSOSN']['MISMATCH']} product default==emitted", "Need ST-retention context; build shadow rule (CRT=1 + ST applicability) and score against NFCE_ITENS."),
("CST_ICMS", "CONFIRMED", False, False, 521, f"{consistent['CST']['MATCH']}/{consistent['CST']['MATCH']} product==emitted", "Not needed while CRT=1; keep as validated default."),
("MVA", "NO_PRODUCT_FIELD", False, True, d["MVA"]["RULE_RESOLVED"], "none (MARGEM_AGREGADA unused)", "Decide with vendor whether MARGEM_AGREGADA is honoured by the emitter before any write."),
("PIS_COFINS_CST", "UNCONFIRMED", False, False, 521, "NFC-e always '01'/blank, rates 0", "Needs vendor confirmation of which field feeds CST_PIS_COFINS (not C000025.COD_SAIDA_*)."),
("ORIGEM", "UNCONFIRMED", False, False, 521, "CST digit 1 consistent (0)", "Vendor confirmation of COD_ORIGEM_MERCADORIA usage; obtain objective origin."),
("FCP", "n/a (no product field)", False, False, 521, "none", "Needs FCP item list per UF; document-level only."),
("RTC", "n/a (no ERP field)", False, False, 521, "none", "Needs NCM→LC 214 annex mapping dataset; then document-level."),
]
def score(r):
return (r[1] == "CONFIRMED", r[2], r[3], r[4], "n/a" not in r[5] and r[5] != "none")
ROWS.sort(key=score, reverse=True)
md = ["# NEXT_FISCAL_FIELD_IMPLEMENTATION_V1", "",
"Mechanical ordering: (1) ERP mapping confirmed, (2) required context available, (3) official rule available, (4) potential products, (5) history validation exists. Source: `docs/ERP_FISCAL_FIELD_MAP_V1.json`, `docs/FISCAL_CONTEXT_REQUIREMENTS_V1.json`, `docs/FISCAL_CONTEXT_COVERAGE_V1.json`.", "",
"| FIELD | ERP_MAPPING | CONTEXT_AVAILABLE | RULE_AVAILABLE | POTENTIAL_PRODUCTS | HISTORICAL_VALIDATION | NEXT_ACTION |", "|---|---|---|---|---|---|---|"]
for r in ROWS:
md.append(f"| {r[0]} | {r[1]} | {'YES' if r[2] else 'NO'} | {'YES' if r[3] else 'NO'} | {r[4]} | {r[5]} | {r[6]} |")
md += ["", "Caveat: NFC-e history joins by CODPRODUTO show identity drift (item NCM != product NCM on 57% of items); trust only identity-consistent joins."]
(DOCS / "NEXT_FISCAL_FIELD_IMPLEMENTATION_V1.md").write_text("\n".join(md) + "\n", encoding="utf-8")
print(json.dumps({"tables": len(tables), "fiscal_tables": len(fiscal_tables), "items_analyzed": items_analyzed, "ctx": [n_av, n_cfg, n_job, n_miss], "sim": [matches, mism, unres],
"confirmed": sum(1 for x in FIELDS if x["confidence"] in ("HIGH", "MEDIUM")), "fields": len(FIELDS)}))
+35
View File
@@ -0,0 +1,35 @@
"""Write docs/FISCAL_FIELD_CONTRACT_V1.json from central.app.field_contract (single source of truth). No database access."""
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from central.app.field_contract import CONTRACT, CONTRACT_VERSION, RTC_CAPABILITY, WRITE_FIELDS # noqa: E402
from central.app.context_keys import ALLOWED_KEYS # noqa: E402
ui = json.loads((ROOT / "docs" / "SGV_PRODUCT_FISCAL_UI_MAP_V1.json").read_text(encoding="utf-8"))
bound = {f["field"]: f for f in ui["fields"] if f.get("field")}
doc = {"artifact": "FISCAL_FIELD_CONTRACT_V1", "contract_version": CONTRACT_VERSION, "product_table": "C000025", "write_targets": len(WRITE_FIELDS),
"final_states_per_product_field": ["ALREADY_CORRECT", "CHANGE_REQUIRED", "CORRECTED", "NOT_APPLICABLE", "MANUAL_INPUT_REQUIRED(<input_key>)"],
"declared_valid_policy": "A valid ERP declaration that no proven evidence contradicts is ALREADY_CORRECT with reason DECLARED_VALID_NOT_CONTRADICTED; rule-proven results carry other reasons.",
"operator_context_keys": sorted(ALLOWED_KEYS), "context_hierarchy": "PRODUCT > GROUP (CODGRUPO) > NCM > COMPANY (Hub table fiscal_context_entries; API /v1/context; CLI scripts/import_fiscal_context.py)",
"unresolved_official_sources": {
"PIS/COFINS": "No official rule table in the ACTIVE core datasets maps regime/NCM to CST PIS/COFINS or rates (SPED Tabelas 4.3.x + the Simples Nacional PIS/COFINS guidance are the missing sources; they are manual downloads and were deferred by business scope). Contract answer: company-level `pis_cofins_profile` supplied by the accountant; never assumed 0%.",
"ICMS internal rate (regime normal)": "RICMS-BA articles on internal rates/FECP are not in the acquired datasets: `icms_rate` is an operator fact for CRT 2/3 taxed items.",
"Monofásico incidence": "Official monophasic NCM list not acquired: INCIDENCIA_PISCOFINS keeps the ERP declaration or takes an operator `incidence`."},
"rtc": {"status": {"RTC_CCLASSTRIB": "SUPPORTED_BY_CURRENT_ERP_SCHEMA_UI_BINDING_UNCONFIRMED", "RTC_CCREDPRES": "ERP_SCHEMA_NOT_READY", "RTC_CBS_RATES": "ERP_SCHEMA_NOT_READY"},
"evidence": "C000025.CCLASSTRIB VARCHAR(6) exists (all NULL) in CONSTRUCAO, MMMODAS, RINCAO and UNIAO (a newer SGV schema) and is absent in TESTESOFICIAIS. The SGV executable available locally (5.0.6.124) neither binds nor mentions it and the fiscal screen shows no such control, so the binding cannot be confirmed; no cCredPres/CBS/IBS column exists in any database.",
"write_target": False, "why_not_written": "no proven UI binding and no NCM -> LC 214 annex (cClassTrib) source in the ACTIVE datasets; a value here would be invented",
"to_close": ["the newer SGV executable/screen that binds CCLASSTRIB (binding proof)", "official LC 214/2025 annex NCM lists to derive cClassTrib deterministically"]},
"not_persisted_controls": {"CSOSN significado (ComboBox1)": "presentation only, not data-aware", "CODALIQUOTA": "exists in the ERP, empty, not on the fiscal screen: not a requirement without new evidence"},
"fields": {}}
for f in WRITE_FIELDS:
c = dict(CONTRACT[f])
c["UI_BINDING"] = {"classification": bound[f]["classification"] if f in bound else "CONFIRMED_STRONG", "evidence": bound[f]["evidence_basis"] if f in bound else None}
doc["fields"][f] = c
# EX_TIPI is not a control on the screen (audited separately) and COD_ORIGEM binding is closed by the DFM OnClick handler:
doc["fields"]["EX_TIPI"]["UI_BINDING"] = {"classification": "CONFIRMED_STRONG", "evidence": "user-declared earlier missions; TIPI column EX_TIPI"}
doc["fields"]["COD_ORIGEM_MERCADORIA"]["UI_BINDING"] = {"classification": "CONFIRMED_STRONG", "evidence": "DFM: RgOrigemMercadoria (TRadioGroup, OnClick=RgOrigemMercadoriaClick, no DataField) + hidden data-aware TDBEdit CodOrigemMercadoria bound to COD_ORIGEM_MERCADORIA in the same tab; the only origin-bound control of the form; values 0/1/2 observed in 4 databases"}
(ROOT / "docs" / "FISCAL_FIELD_CONTRACT_V1.json").write_text(json.dumps(doc, ensure_ascii=False, indent=1), encoding="utf-8")
print(len(doc["fields"]), "contracts")
+151
View File
@@ -0,0 +1,151 @@
"""Read-only fiscal coverage matrix over all C000025 products in TESTESOFICIAIS.FDB.
No Firebird writes. Classifies every product via the Hub's /v1/classify/batch
(itself read-only against Firebird — the Hub never touches Firebird at all)
and separates DATA_AVAILABLE (a dataset exists to consult) from RULE_RESOLVED
(a single defensible determination was reached) per the mission's own
instruction not to conflate the two.
"""
import json
import os
import sys
import urllib.request
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src"))
from gti_fiscal_agent.firebird_reader import read_products # noqa: E402
def load_dotenv(path: Path) -> None:
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
os.environ.setdefault(key.strip(), value.strip())
def classify_batch(hub_url, token, products, chunk_size=100):
results = []
for start in range(0, len(products), chunk_size):
chunk = products[start:start + chunk_size]
payload = {"products": [{"current_ncm": p["CLASSIFICACAO_FISCAL"], "description": p["PRODUTO"] or ""} for p in chunk]}
request = urllib.request.Request(
f"{hub_url}/v1/classify/batch",
data=json.dumps(payload, ensure_ascii=True).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
method="POST",
)
with urllib.request.urlopen(request, timeout=60) as response:
results.extend(json.load(response)["results"])
return results
def get_dataset_status(hub_url, token):
request = urllib.request.Request(f"{hub_url}/v1/datasets/status", headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(request, timeout=15) as response:
return json.load(response)
def main():
token = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("AGENT_DEV_TOKEN", "")
hub_url = os.environ.get("GTI_HUB_URL", "http://localhost:8000")
load_dotenv(ROOT / ".env")
products = read_products(os.environ)
total = len(products)
decisions = classify_batch(hub_url, token, products)
datasets = get_dataset_status(hub_url, token)
active_kinds = {d["kind"] for d in datasets if d["status"] == "ACTIVE"}
ncm_status = Counter(d.get("status") for d in decisions)
ncm_missing = sum(1 for p in products if not (p["CLASSIFICACAO_FISCAL"] or "").strip())
ncm_valid = ncm_status.get("EXACT_NCM", 0)
ncm_legacy = ncm_status.get("LEGACY_NCM", 0)
ncm_invalid = sum(
1 for p, d in zip(products, decisions)
if (p["CLASSIFICACAO_FISCAL"] or "").strip() and d.get("status") not in ("EXACT_NCM", "LEGACY_NCM")
)
ipi_buckets = Counter()
cest_match = cest_confirmed = cest_candidates = 0
mva_match = mva_resolved = mva_candidates = 0
for decision in decisions:
ipi_status = decision.get("ipi_status")
if ipi_status == "LEGACY_NCM":
pass
elif ipi_status == "NO_TIPI_MAPPING_FOUND":
ipi_buckets["NO_MATCH"] += 1
elif ipi_status == "IPI_CANDIDATES":
ipi_buckets["AMBIGUOUS"] += 1
elif ipi_status == "IPI_RESOLVED":
rate_kind = (decision.get("classification", {}).get("ipi") or {}).get("rate_kind")
ipi_buckets[{"NUMERIC": "NUMERIC", "NON_NUMERIC": "NON_NUMERIC", "BLANK": "SOURCE_BLANK"}.get(rate_kind, "NO_MATCH")] += 1
else:
ipi_buckets["NO_MATCH"] += 1
cest_status = decision.get("cest_status")
if cest_status not in (None, "NO_CEST_MAPPING_FOUND", "LEGACY_NCM"):
cest_match += 1
if cest_status == "CEST_CONFIRMED":
cest_confirmed += 1
elif cest_status == "CEST_CANDIDATES":
cest_candidates += 1
mva_status = decision.get("ba_st_mva_status")
if mva_status not in (None, "NO_MVA_MAPPING_FOUND", "LEGACY_NCM"):
mva_match += 1
if mva_status == "MVA_RESOLVED":
mva_resolved += 1
elif mva_status == "MVA_CANDIDATES":
mva_candidates += 1
coverage = {
"artifact": "FISCAL_COVERAGE_FULL_V1",
"firebird_writes": 0,
"total_products": total,
"ncm_valid": ncm_valid,
"ncm_legacy": ncm_legacy,
"ncm_invalid": ncm_invalid,
"ncm_missing": ncm_missing,
"ipi": {
"numeric": ipi_buckets["NUMERIC"],
"non_numeric": ipi_buckets["NON_NUMERIC"],
"source_blank": ipi_buckets["SOURCE_BLANK"],
"ambiguous": ipi_buckets["AMBIGUOUS"],
"no_match": ipi_buckets["NO_MATCH"],
},
"cest": {"match": cest_match, "confirmed": cest_confirmed, "candidates": cest_candidates},
"ba_st_mva": {"match": mva_match, "resolved": mva_resolved, "candidates": mva_candidates},
"pis_cofins": {
"data_available": True, # CST_PIS/CST_COFINS domain tables exist (fiscal_code_entries), no product-level linkage yet
"match": 0,
"unresolved": total,
"note": "CST_PIS/CST_COFINS code domains are ACTIVE and consultable, but no NCM-linked determination table (SPED Tabela 4.3.10-4.3.17) is available to resolve a specific product's PIS/COFINS situation yet. DATA_AVAILABLE=true, RULE_RESOLVED=0 for all products.",
},
"cfop": {"dataset_available": False, "note": "CFOP full table FAILED to acquire this session (portal-gated); MOC7 only references CFOP, does not enumerate it. See docs/SOURCE_REGISTRY_V2.md."},
"cst_icms": {"dataset_available": "NFE_MOC_TABLES" in active_kinds, "record_count": 11},
"csosn": {"dataset_available": "NFE_MOC_TABLES" in active_kinds, "record_count": 10},
"origin": {"dataset_available": "NFE_MOC_TABLES" in active_kinds, "record_count": 9},
"fcp": {"dataset_available": False, "note": "No single canonical national table exists; FAILED (see docs/SOURCE_REGISTRY_V2.md)."},
"icms_monophasic": {"match": 0, "dataset_available": False},
"anp": {"match": 0, "dataset_available": False},
"ncm_utrib": {"dataset_available": False},
"rtc": {
"cclasstrib_match": 0, "ccredpres_match": 0, "cbs_rate_match": 0, "biofuel_index_available": False,
"note": "All RTC (Reforma Tributária) tables FAILED to acquire — 2025/26-era portal documents, not present in MOC7 or locally.",
},
"active_dataset_kinds": sorted(active_kinds),
}
output_path = ROOT / "docs" / "FISCAL_COVERAGE_FULL_V1.json"
output_path.write_text(json.dumps(coverage, indent=2, ensure_ascii=False), encoding="utf-8")
print(json.dumps({k: v for k, v in coverage.items() if k not in ("pis_cofins", "rtc")}, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
+167
View File
@@ -0,0 +1,167 @@
"""Read-only fiscal coverage matrix (V2) over all C000025 products in
TESTESOFICIAIS.FDB, after importing CFOP/FCP/ANP/NCM_UTRIB/RTC_BIOFUEL_INDEX/
RTC_CCREDPRES from sources/raw/bootstrap_v1/.
No Firebird writes. Separates DATA_AVAILABLE (a dataset exists) from
MATCH_FOUND (this product's own field — NCM or CFOP — actually matches an
official row) from RULE_RESOLVED (a fiscal determination was made) — the
mission's own required distinction. Never invents a rule to inflate
RULE_RESOLVED.
"""
import json
import os
import sys
import urllib.request
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src"))
from gti_fiscal_agent.firebird_reader import read_products # noqa: E402
def load_dotenv(path: Path) -> None:
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
os.environ.setdefault(key.strip(), value.strip())
def http_get(url, token):
request = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(request, timeout=15) as response:
return json.load(response)
def classify_batch(hub_url, token, products, chunk_size=100):
results = []
for start in range(0, len(products), chunk_size):
chunk = products[start:start + chunk_size]
payload = {"products": [{"current_ncm": p["CLASSIFICACAO_FISCAL"], "description": p["PRODUTO"] or ""} for p in chunk]}
request = urllib.request.Request(
f"{hub_url}/v1/classify/batch",
data=json.dumps(payload, ensure_ascii=True).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
method="POST",
)
with urllib.request.urlopen(request, timeout=60) as response:
results.extend(json.load(response)["results"])
return results
def main():
token = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("AGENT_DEV_TOKEN", "")
hub_url = os.environ.get("GTI_HUB_URL", "http://localhost:8000")
load_dotenv(ROOT / ".env")
products = read_products(os.environ)
total = len(products)
decisions = classify_batch(hub_url, token, products)
datasets = http_get(f"{hub_url}/v1/datasets/status", token)
active_kinds = {d["kind"] for d in datasets if d["status"] == "ACTIVE"}
record_counts = {d["kind"]: d["record_count"] for d in datasets if d["status"] == "ACTIVE"}
ncm_status = Counter(d.get("status") for d in decisions)
ncm_missing = sum(1 for p in products if not (p["CLASSIFICACAO_FISCAL"] or "").strip())
ncm_valid = ncm_status.get("EXACT_NCM", 0)
ncm_legacy = ncm_status.get("LEGACY_NCM", 0)
ncm_invalid = sum(
1 for p, d in zip(products, decisions)
if (p["CLASSIFICACAO_FISCAL"] or "").strip() and d.get("status") not in ("EXACT_NCM", "LEGACY_NCM")
)
ipi_buckets = Counter()
cest_match = cest_confirmed = cest_candidates = 0
mva_match = mva_resolved = mva_candidates = 0
ncm_utrib_match = 0
cfop_match = 0
cfop_seen = {}
ncm_utrib_seen = {}
for product, decision in zip(products, decisions):
ipi_status = decision.get("ipi_status")
if ipi_status == "LEGACY_NCM":
pass
elif ipi_status == "NO_TIPI_MAPPING_FOUND":
ipi_buckets["NO_MATCH"] += 1
elif ipi_status == "IPI_CANDIDATES":
ipi_buckets["AMBIGUOUS"] += 1
elif ipi_status == "IPI_RESOLVED":
rate_kind = (decision.get("classification", {}).get("ipi") or {}).get("rate_kind")
ipi_buckets[{"NUMERIC": "NUMERIC", "NON_NUMERIC": "NON_NUMERIC", "BLANK": "SOURCE_BLANK"}.get(rate_kind, "NO_MATCH")] += 1
else:
ipi_buckets["NO_MATCH"] += 1
cest_status = decision.get("cest_status")
if cest_status not in (None, "NO_CEST_MAPPING_FOUND", "LEGACY_NCM"):
cest_match += 1
if cest_status == "CEST_CONFIRMED":
cest_confirmed += 1
elif cest_status == "CEST_CANDIDATES":
cest_candidates += 1
mva_status = decision.get("ba_st_mva_status")
if mva_status not in (None, "NO_MVA_MAPPING_FOUND", "LEGACY_NCM"):
mva_match += 1
if mva_status == "MVA_RESOLVED":
mva_resolved += 1
elif mva_status == "MVA_CANDIDATES":
mva_candidates += 1
matched_ncm = decision.get("classification", {}).get("ncm")
if matched_ncm:
if matched_ncm not in ncm_utrib_seen:
ncm_utrib_seen[matched_ncm] = http_get(f"{hub_url}/v1/reference/ncm_utrib/{matched_ncm}", token)["found"]
if ncm_utrib_seen[matched_ncm]:
ncm_utrib_match += 1
cfop_value = (product.get("CFOP") or "").strip()
if cfop_value:
if cfop_value not in cfop_seen:
cfop_seen[cfop_value] = http_get(f"{hub_url}/v1/reference/cfop/{cfop_value}", token)["found"]
if cfop_seen[cfop_value]:
cfop_match += 1
coverage = {
"artifact": "FISCAL_COVERAGE_FULL_V2",
"firebird_writes": 0,
"total_products": total,
"ncm_valid": ncm_valid, "ncm_legacy": ncm_legacy, "ncm_invalid": ncm_invalid, "ncm_missing": ncm_missing,
"ipi": {
"numeric": ipi_buckets["NUMERIC"], "non_numeric": ipi_buckets["NON_NUMERIC"],
"source_blank": ipi_buckets["SOURCE_BLANK"], "ambiguous": ipi_buckets["AMBIGUOUS"], "no_match": ipi_buckets["NO_MATCH"],
},
"cest": {"match": cest_match, "confirmed": cest_confirmed, "candidates": cest_candidates},
"ba_st_mva": {"match": mva_match, "resolved": mva_resolved, "candidates": mva_candidates},
"pis_cofins": {
"data_available": True, "match_found": 0, "rule_resolved": 0, "rule_unresolved": total,
"note": "CST_PIS/CST_COFINS code domain is ACTIVE (66 codes) but no NCM-linked SPED Tabela 4.3.10-4.3.17 determination table was available to acquire (MISSING_BOOTSTRAP_FILE) — no per-product PIS/COFINS rule can be resolved.",
},
"cfop": {"dataset_available": "CFOP" in active_kinds, "record_count": record_counts.get("CFOP", 0), "match_found": cfop_match, "note": "match = this product's own C000025.CFOP value exists in the official table"},
"cst_icms": {"dataset_available": "NFE_MOC_TABLES" in active_kinds, "record_count": 11},
"csosn": {"dataset_available": "NFE_MOC_TABLES" in active_kinds, "record_count": 10},
"origin": {"dataset_available": "NFE_MOC_TABLES" in active_kinds, "record_count": 9},
"fcp": {"dataset_available": "FCP" in active_kinds, "record_count": record_counts.get("FCP", 0), "note": "per-UF rate table; not linked to a product-level UF field in C000025, so match_found is not meaningful per-product"},
"icms_monophasic": {"match_found": 0, "dataset_available": False, "status": "MISSING_BOOTSTRAP_FILE"},
"anp": {"dataset_available": "ANP" in active_kinds, "record_count": record_counts.get("ANP", 0), "match_found": 0, "note": "C000025 has no ANP/fuel product code field to join on; dataset exists but is not applicable to this ERP's product catalog"},
"ncm_utrib": {"dataset_available": "NCM_UTRIB" in active_kinds, "record_count": record_counts.get("NCM_UTRIB", 0), "match_found": ncm_utrib_match},
"rtc": {
"cclasstrib_match": 0, "cclasstrib_available": False, "cclasstrib_status": "MISSING_BOOTSTRAP_FILE",
"ccredpres_match": 0, "ccredpres_available": "RTC_CCREDPRES" in active_kinds, "ccredpres_record_count": record_counts.get("RTC_CCREDPRES", 0),
"cbs_rate_match": 0, "cbs_rate_available": False, "cbs_rate_status": "MISSING_BOOTSTRAP_FILE",
"biofuel_match": 0, "biofuel_available": "RTC_BIOFUEL_INDEX" in active_kinds, "biofuel_record_count": record_counts.get("RTC_BIOFUEL_INDEX", 0),
"note": "cCredPres and biofuel index datasets are ACTIVE but have no join key present in C000025 (no cProdANP/cCredPres field on this ERP's grocery/restaurant catalog), so match_found=0 is a data-shape fact, not a gap.",
},
"active_dataset_kinds": sorted(active_kinds),
}
output_path = ROOT / "docs" / "FISCAL_COVERAGE_FULL_V2.json"
output_path.write_text(json.dumps(coverage, indent=2, ensure_ascii=False), encoding="utf-8")
print(json.dumps({k: v for k, v in coverage.items() if k not in ("pis_cofins", "rtc")}, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
+171
View File
@@ -0,0 +1,171 @@
"""Read-only fiscal coverage matrix (V3) over all C000025 products in
TESTESOFICIAIS.FDB, after importing CFOP/FCP/ANP/NCM_UTRIB/RTC_BIOFUEL_INDEX/
RTC_CCREDPRES from sources/raw/bootstrap_v1/.
No Firebird writes. Separates DATA_AVAILABLE (a dataset exists) from
MATCH_FOUND (this product's own field — NCM or CFOP — actually matches an
official row) from RULE_RESOLVED (a fiscal determination was made) — the
mission's own required distinction. Never invents a rule to inflate
RULE_RESOLVED.
"""
import json
import os
import sys
import urllib.request
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src"))
from gti_fiscal_agent.firebird_reader import read_products # noqa: E402
def load_dotenv(path: Path) -> None:
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
os.environ.setdefault(key.strip(), value.strip())
def http_get(url, token):
request = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(request, timeout=15) as response:
return json.load(response)
def classify_batch(hub_url, token, products, chunk_size=100):
results = []
for start in range(0, len(products), chunk_size):
chunk = products[start:start + chunk_size]
payload = {"products": [{"current_ncm": p["CLASSIFICACAO_FISCAL"], "description": p["PRODUTO"] or ""} for p in chunk]}
request = urllib.request.Request(
f"{hub_url}/v1/classify/batch",
data=json.dumps(payload, ensure_ascii=True).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
method="POST",
)
with urllib.request.urlopen(request, timeout=60) as response:
results.extend(json.load(response)["results"])
return results
def main():
token = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("AGENT_DEV_TOKEN", "")
hub_url = os.environ.get("GTI_HUB_URL", "http://localhost:8000")
load_dotenv(ROOT / ".env")
products = read_products(os.environ)
total = len(products)
decisions = classify_batch(hub_url, token, products)
datasets = http_get(f"{hub_url}/v1/datasets/status", token)
active_kinds = {d["kind"] for d in datasets if d["status"] == "ACTIVE"}
record_counts = {d["kind"]: d["record_count"] for d in datasets if d["status"] == "ACTIVE"}
ncm_status = Counter(d.get("status") for d in decisions)
ncm_missing = sum(1 for p in products if not (p["CLASSIFICACAO_FISCAL"] or "").strip())
ncm_valid = ncm_status.get("EXACT_NCM", 0)
ncm_legacy = ncm_status.get("LEGACY_NCM", 0)
ncm_invalid = sum(
1 for p, d in zip(products, decisions)
if (p["CLASSIFICACAO_FISCAL"] or "").strip() and d.get("status") not in ("EXACT_NCM", "LEGACY_NCM")
)
ipi_buckets = Counter()
cest_match = cest_confirmed = cest_candidates = 0
mva_match = mva_resolved = mva_candidates = 0
ncm_utrib_match = 0
cfop_match = 0
cfop_seen = {}
ncm_utrib_seen = {}
for product, decision in zip(products, decisions):
ipi_status = decision.get("ipi_status")
if ipi_status == "LEGACY_NCM":
pass
elif ipi_status == "NO_TIPI_MAPPING_FOUND":
ipi_buckets["NO_MATCH"] += 1
elif ipi_status == "IPI_CANDIDATES":
ipi_buckets["AMBIGUOUS"] += 1
elif ipi_status == "IPI_RESOLVED":
rate_kind = (decision.get("classification", {}).get("ipi") or {}).get("rate_kind")
ipi_buckets[{"NUMERIC": "NUMERIC", "NON_NUMERIC": "NON_NUMERIC", "BLANK": "SOURCE_BLANK"}.get(rate_kind, "NO_MATCH")] += 1
else:
ipi_buckets["NO_MATCH"] += 1
cest_status = decision.get("cest_status")
if cest_status not in (None, "NO_CEST_MAPPING_FOUND", "LEGACY_NCM"):
cest_match += 1
if cest_status == "CEST_CONFIRMED":
cest_confirmed += 1
elif cest_status == "CEST_CANDIDATES":
cest_candidates += 1
mva_status = decision.get("ba_st_mva_status")
if mva_status not in (None, "NO_MVA_MAPPING_FOUND", "LEGACY_NCM"):
mva_match += 1
if mva_status == "MVA_RESOLVED":
mva_resolved += 1
elif mva_status == "MVA_CANDIDATES":
mva_candidates += 1
matched_ncm = decision.get("classification", {}).get("ncm")
if matched_ncm:
if matched_ncm not in ncm_utrib_seen:
ncm_utrib_seen[matched_ncm] = http_get(f"{hub_url}/v1/reference/ncm_utrib/{matched_ncm}", token)["found"]
if ncm_utrib_seen[matched_ncm]:
ncm_utrib_match += 1
cfop_value = (product.get("CFOP") or "").strip()
if cfop_value:
if cfop_value not in cfop_seen:
cfop_seen[cfop_value] = http_get(f"{hub_url}/v1/reference/cfop/{cfop_value}", token)["found"]
if cfop_seen[cfop_value]:
cfop_match += 1
cbs_2026 = http_get(f"{hub_url}/v1/reference/rtc_cbs_rate/2026", token)
coverage = {
"artifact": "FISCAL_COVERAGE_FULL_V3",
"firebird_writes": 0,
"total_products": total,
"ncm_valid": ncm_valid, "ncm_legacy": ncm_legacy, "ncm_invalid": ncm_invalid, "ncm_missing": ncm_missing,
"ipi": {
"numeric": ipi_buckets["NUMERIC"], "non_numeric": ipi_buckets["NON_NUMERIC"],
"source_blank": ipi_buckets["SOURCE_BLANK"], "ambiguous": ipi_buckets["AMBIGUOUS"], "no_match": ipi_buckets["NO_MATCH"],
},
"cest": {"match": cest_match, "confirmed": cest_confirmed, "candidates": cest_candidates},
"ba_st_mva": {"match": mva_match, "resolved": mva_resolved, "candidates": mva_candidates},
"pis_cofins_4_3_10_17": "DEFERRED_BY_BUSINESS_SCOPE",
"pis_cofins": {
"data_available": True, "match_found": 0, "rule_resolved": 0, "rule_unresolved": total,
"note": "CST_PIS/CST_COFINS code domain is ACTIVE (66 codes) but no NCM-linked SPED Tabela 4.3.10-4.3.17 determination table was available to acquire (MISSING_BOOTSTRAP_FILE) — no per-product PIS/COFINS rule can be resolved.",
},
"cfop": {"dataset_available": "CFOP" in active_kinds, "record_count": record_counts.get("CFOP", 0), "match_found": cfop_match, "note": "match = this product's own C000025.CFOP value exists in the official table"},
"cst_icms": {"dataset_available": "NFE_MOC_TABLES" in active_kinds, "record_count": 11},
"csosn": {"dataset_available": "NFE_MOC_TABLES" in active_kinds, "record_count": 10},
"origin": {"dataset_available": "NFE_MOC_TABLES" in active_kinds, "record_count": 9},
"fcp": {"dataset_available": "FCP" in active_kinds, "record_count": record_counts.get("FCP", 0), "note": "per-UF rate table; not linked to a product-level UF field in C000025, so match_found is not meaningful per-product"},
"icms_monophasic": {"match_found": 0, "dataset_available": False, "status": "OUT_OF_SCOPE_MVP"},
"anp": {"scope": "ACTIVE_REFERENCE_ONLY", "dataset_available": "ANP" in active_kinds, "record_count": record_counts.get("ANP", 0), "match_found": 0, "note": "C000025 has no ANP/fuel product code field to join on; dataset exists but is not applicable to this ERP's product catalog"},
"ncm_utrib": {"dataset_available": "NCM_UTRIB" in active_kinds, "record_count": record_counts.get("NCM_UTRIB", 0), "match_found": ncm_utrib_match},
"rtc": {
"cclasstrib_available": "RTC_CCLASSTRIB" in active_kinds, "cclasstrib_record_count": record_counts.get("RTC_CCLASSTRIB", 0), "cclasstrib_match": 0,
"cbs_rate_available": "RTC_CBS_RATES" in active_kinds, "cbs_rate_record_count": record_counts.get("RTC_CBS_RATES", 0),
"cbs_rate_2026": cbs_2026,
"ccredpres_available": "RTC_CCREDPRES" in active_kinds, "ccredpres_record_count": record_counts.get("RTC_CCREDPRES", 0),
"biofuel_available": "RTC_BIOFUEL_INDEX" in active_kinds, "biofuel_scope": "ACTIVE_REFERENCE_ONLY",
"note": "cClassTrib/cCredPres are DATA_AVAILABLE only: C000025 carries no cClassTrib/cCredPres field to join on, so match_found=0 and rule_resolved=0 are data-shape facts, not gaps. No rule was invented.",
},
"core_domains_active": {d: (k in active_kinds) for d, k in {"NCM": "NCM", "TIPI_IPI": "TIPI_IPI", "CEST": "CEST", "BA_ST_MVA": "BA_ST_MVA", "CFOP": "CFOP", "ORIGIN": "NFE_MOC_TABLES", "CST_ICMS": "NFE_MOC_TABLES", "CSOSN": "NFE_MOC_TABLES", "CST_PIS": "NFE_MOC_TABLES", "CST_COFINS": "NFE_MOC_TABLES", "FCP": "FCP", "RTC_CCLASSTRIB": "RTC_CCLASSTRIB", "RTC_CCREDPRES": "RTC_CCREDPRES", "RTC_CBS_RATES": "RTC_CBS_RATES"}.items()},
"active_dataset_kinds": sorted(active_kinds),
}
output_path = ROOT / "docs" / "FISCAL_COVERAGE_FULL_V3.json"
output_path.write_text(json.dumps(coverage, indent=2, ensure_ascii=False), encoding="utf-8")
print(json.dumps({k: v for k, v in coverage.items() if k not in ("pis_cofins", "rtc")}, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
+72
View File
@@ -0,0 +1,72 @@
"""Compose docs/MULTI_DATABASE_FINDINGS_V1.md from the read-only audit + coverage artifacts. No database access."""
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
D = ROOT / "docs"
audit = json.loads((D / "MULTI_DATABASE_SCHEMA_AUDIT_V1.json").read_text(encoding="utf-8"))
cov = json.loads((D / "MULTI_DATABASE_FISCAL_COVERAGE_V1.json").read_text(encoding="utf-8"))
compat = json.loads((D / "MULTI_DATABASE_FIELD_COMPATIBILITY_V1.json").read_text(encoding="utf-8"))
raw = {p.stem: json.loads(p.read_text(encoding="utf-8")) for p in (D / "runs" / "multi_db").glob("*.json") if not p.stem.endswith("_coverage")}
A = {a["file"]: a for a in audit["databases"]}
C = {c["file"]: c for c in cov["databases"]}
def seg(f):
return A[f]["segment_inferred"]["segment"]
def pct(a, b):
return f"{100 * a / b:.1f}%" if b else "n/a"
lines = ["# MULTI_DATABASE_FINDINGS_V1", "",
"Read-only audit of the copies in `Banco/` (no Firebird writes, no new fiscal rules, no new datasets). Segment = inferred from table population and the company name stored in C000004, never from the file name.", "",
"## Databases", "", "| file | segment (evidence) | products | company UF/CRT (C000004) | EMISSOR.EMITENTE_CRT | schema class |", "|---|---|---|---|---|---|"]
for f, a in A.items():
co = a["company"]
crt = f"{co.get('uf')}/{co.get('crt')}" if co.get("status") == "OK" else f"UNAVAILABLE ({co.get('rows')} company rows)"
lines.append(f"| {f} | {seg(f)} | {a['product_count']} | {crt} | {co.get('emissor_crt')} | {a['schema_class']} |")
lines += ["", "Company-context findings: C000004.CRT disagrees with EMISSOR.EMITENTE_CRT in 3 of the 4 copies, OPTANTE_SIMPLES=S coexists with CRT=3 (UNIAO) and OPTANTE_SIMPLES=N with CRT=2 (MMMODAS); RINCAO's C000004 holds 3 company rows (fail-closed: context ambiguous). The Agent must not guess: it needs an explicit company selection and a decided CRT source of truth.", "",
"## Fiscal coverage (Fiscal Engine V1, read-only)", "", "| metric | " + " | ".join(C) + " |", "|---|" + "---|" * len(C)]
keys = ["TOTAL_PRODUCTS", "NCM_VALID", "NCM_LEGACY", "NCM_INVALID", "NCM_MISSING", "IPI_NUMERIC", "IPI_NON_NUMERIC", "IPI_SOURCE_BLANK", "IPI_AMBIGUOUS", "IPI_NO_MATCH", "IPI_ALREADY_CORRECT", "IPI_CHANGE_REQUIRED",
"IPI_WRITE_ELIGIBLE", "CEST_CONFIRMED", "CEST_CANDIDATES", "CFOP_RESOLVED", "CFOP_CONTEXT_REQUIRED", "CSOSN_RESOLVED", "CSOSN_CANDIDATES", "CSOSN_CONTEXT_REQUIRED", "ST_CONFIRMED", "ST_CANDIDATE", "ST_UNKNOWN",
"ST_NOT_APPLICABLE", "PRODUCT_ROLE_KNOWN", "PRODUCT_ROLE_UNKNOWN", "ST_WITHHOLDING_CONFIRMED", "ST_WITHHOLDING_NOT_CONFIRMED", "ST_WITHHOLDING_UNKNOWN"]
for k in keys:
lines.append(f"| {k} | " + " | ".join(str(C[f]["TOTAL_PRODUCTS"] if k == "TOTAL_PRODUCTS" else C[f]["metrics"].get(k, 0)) for f in C) + " |")
lines += ["", "Guards: ST/MVA dataset is BA-only and CSOSN logic needs CRT=1; when the company context does not satisfy them the engine reports context-required instead of applying a rule (`guards_applied` in the coverage artifact).", "",
"## Segment comparison", "", "| indicator | " + " | ".join(f"{f} ({seg(f)})" for f in C) + " |", "|---|" + "---|" * len(C)]
q = lambda f, k: C[f]["quality"][k] # noqa: E731
n = lambda f: C[f]["TOTAL_PRODUCTS"] # noqa: E731
rows = [("NCM valid share", lambda f: pct(C[f]["metrics"].get("NCM_VALID", 0), n(f))), ("CEST filled", lambda f: pct(q(f, "cest_filled"), n(f))), ("CSOSN filled", lambda f: pct(q(f, "csosn_filled"), n(f))),
("CFOP distinct values", lambda f: q(f, "cfop_distinct")), ("CFOP top", lambda f: q(f, "cfop_top")[:3]), ("SITUACAO_TRIBUTARIA", lambda f: q(f, "situacao_tributaria")),
("ST_CANDIDATE+CONFIRMED (BA dataset)", lambda f: C[f]["metrics"].get("ST_CANDIDATE", 0) + C[f]["metrics"].get("ST_CONFIRMED", 0)), ("purchase items C000088", lambda f: q(f, "purchase_items_C000088")),
("products with purchase evidence", lambda f: q(f, "products_with_purchase_evidence")), ("recipes / composed", lambda f: f"{q(f, 'recipes')} / {q(f, 'composed_products')}"),
("role known share", lambda f: pct(C[f]["metrics"].get("PRODUCT_ROLE_KNOWN", 0), n(f))), ("IPI numeric share", lambda f: pct(C[f]["metrics"].get("IPI_NUMERIC", 0), n(f))), ("IPI already stored", lambda f: pct(q(f, "ipi_filled"), n(f)))]
for name, fn in rows:
lines.append(f"| {name} | " + " | ".join(str(fn(f)) for f in C) + " |")
# mechanical attribution of gaps
def all_zero(k):
return all(C[f]["metrics"].get(k, 0) == 0 for f in C)
erp_wide = [k for k in ("CEST_CONFIRMED", "CSOSN_RESOLVED", "ST_CONFIRMED", "ST_WITHHOLDING_CONFIRMED", "IPI_WRITE_ELIGIBLE") if all_zero(k)]
lines += ["", "## Where the gaps come from", "",
f"- **ERP-wide (0 in every database):** {', '.join(erp_wide) or 'none'}. These are limits of the SGV data model / current rules, not of one client.",
"- **Client/segment:** CEST filling varies from near-empty (apparel) to mostly filled (market/restaurant/construction) — segment-shaped (goods under ST agreements), but each segment has ONE database here, so client vs segment cannot be separated.",
"- **Company context (client-specific and ERP-wide):** CRT/regime data is inconsistent inside the same ERP (C000004 vs EMISSOR vs OPTANTE_SIMPLES); it is a data-quality/config issue every client needs to settle.",
"- **No auto-repair (oficina) database was provided** (`Banco/` holds construction, apparel, restaurant and market copies); auto-repair conclusions are NOT drawn.", "",
"## GUI map validation (21 write targets + origin)", "",
f"All {compat['write_targets_count']} write-target columns exist in all databases ({len(compat['write_targets_present_in_all'])} present everywhere); the only type variation is {json.dumps(compat['variations'], ensure_ascii=False)}. "
"Value patterns repeat across databases (CST 3-digit origin+CST, CSOSN 102/500/101, SITUACAO_TRIBUTARIA T, IAT/IPPT/INCIDENCIA constants, PIS/COFINS columns almost empty), which supports the DFM-derived map without changing it.", ""]
org = compat["fields"]["COD_ORIGEM_MERCADORIA"]
lines += ["Origin (`COD_ORIGEM_MERCADORIA`): populated counts per database " + json.dumps(org["population_per_db"]) + "; observed values " + json.dumps({k: v for k, v in org["distribution_top_per_db"].items()}, ensure_ascii=False) +
". Values stay inside the 0/1/2 domain, but so few rows are populated that this is weak correlation: the field stays **PROBABLE** until a UI diff on the radio group.", "",
"## Architecture answer", "",
"**CAN_USE_ONE_GENERIC_AGENT=true.** All four databases expose C000004 and C000025 with every write-target column; differences are optional tables, one column length (CEST VARCHAR(10) vs (7)) and company-context quality. Required capabilities: "
"`supports_ncm`, `supports_ipi` (only IPI is written today), `supports_cest`, C000025+C000004 present, an unambiguous (or explicitly configured) company context, and the write executor's live column-length/type check. "
"Detected-but-optional: `supports_csosn/cfop/cst/pis/cofins/origin/iat/incidence/ippt`, and evidence tables (C000088, C000100, RECEITA_INGREDIENTES, NFCE_ITENS) which only enrich validation. "
"No per-client Agent is needed; per-client configuration is: company selector (C000004.CODIGO), CRT source of truth, and the authorized write path/fingerprint (pilot two-layer authorization).", ""]
(D / "MULTI_DATABASE_FINDINGS_V1.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
print("ok", erp_wide)
+116
View File
@@ -0,0 +1,116 @@
"""Build docs/SGV_PRODUCT_FISCAL_UI_MAP_V1.json from the SGV form definition (Delphi DFM inside SGV.exe) — READ-ONLY.
Evidence hierarchy per UI control:
1. DFM binding: data-aware control on form Tfrmproduto whose DataSource is dsproduto2 (dataset qrproduto =
'select * from c000025') and whose DataField names the column; the on-screen caption/position matches the label.
2. Column type/domain compatibility (live schema + live values).
3. Optional operator UI diff (docs/SGV_PRODUCT_FISCAL_UI_DIFF_SESSION_V1.json), which can only confirm/upgrade.
Classes: CONFIRMED_STRONG | PROBABLE | AMBIGUOUS | NO_MATCH.
"""
import json
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src"))
from gti_fiscal_agent.firebird_reader import _connect # noqa: E402
for line in (ROOT / ".env").read_text(encoding="utf-8").splitlines():
if "=" in line and not line.startswith("#"):
k, _, v = line.partition("=")
os.environ.setdefault(k.strip(), v.strip())
DFM_JSON = ROOT / "docs" / "runs" / "sgv_product_form_bindings.json"
SESSION = ROOT / "docs" / "SGV_PRODUCT_FISCAL_UI_DIFF_SESSION_V1.json"
dfm = next(f for f in json.loads(DFM_JSON.read_text(encoding="utf-8")) if f["form_class"] == "Tfrmproduto")
bound = {c["DataField"]: c for c in dfm["controls"] if c.get("DataField") and c.get("DataSource") == "dsproduto2"}
by_label = {}
if SESSION.exists():
for r in json.loads(SESSION.read_text(encoding="utf-8")).get("results", []):
by_label[r["ui_label"]] = r
# ui_label, screen area, column (None = no column), direction, semantic, expected control name, notes
UI = [
("NCM", "Aba Estoque > Posição do Estoque/Informações", "CLASSIFICACAO_FISCAL", "n/a", "NCM canônico (8 dígitos); C000025.NCM is not on the screen", "DBEdit14", "NCM lookup control; column NCM (C000025.NCM) has no control."),
("CFOP (Nota Fiscal de Entrada)", "Config. Impostos > Nota Fiscal de Entrada", "CFOP_ENTRADA", "entrada", "CFOP padrão de entrada", "DBEdit72", "Column is empty (0/521) but the screen writes it."),
("CST de ICMS (Nota Fiscal de Entrada)", "Config. Impostos > Nota Fiscal de Entrada", "CST_ENTRADA", "entrada", "CST ICMS de entrada", "DBEdit73", "Column is empty (0/521) but the screen writes it."),
("CFOP (Nota Fiscal de Saída)", "Config. Impostos > Nota Fiscal de Saída GRUPO DE ICMS", "CFOP", "saída", "CFOP padrão de saída", "DbEdit71", ""),
("CST de ICMS (Nota Fiscal de Saída)", "Config. Impostos > Nota Fiscal de Saída GRUPO DE ICMS", "CST", "saída", "CST ICMS 3 dígitos (origem+CST)", "dbedit13", "CRT=1 emission uses CSOSN."),
("Aliq. ICMS", "Config. Impostos > Nota Fiscal de Saída GRUPO DE ICMS", "ALIQUOTA", "saída", "alíquota ICMS %", "ealiq", "Binding proven; emitted NFC-e values differ from this default (see earlier missions)."),
("CEST", "Config. Impostos > Nota Fiscal de Saída GRUPO DE ICMS", "CEST", "n/a", "CEST (7 dígitos)", "DBEdit74", ""),
("Seletor ao lado do CEST (T/I/N/S)", "Config. Impostos > Nota Fiscal de Saída GRUPO DE ICMS", "SITUACAO_TRIBUTARIA", "saída", "situação tributária do produto: T=Tributado, I=Isento, N=Não Tributado, S=Substituição Tributária", "wwDBComboBox2", "Hint of the control lists the domain; 'S' is the ERP's own ST flag."),
("Cofins Entrada", "Config. Impostos > CST PIS/COFINS", "COD_ENTRADA_COFINS", "entrada", "CST COFINS de entrada", "RzDBButtonEdit3", ""),
("Cofins Saída", "Config. Impostos > CST PIS/COFINS", "COD_SAIDA_COFINS", "saída", "CST COFINS de saída", "RzDBButtonEdit1", ""),
("Alíquota (Cofins)", "Config. Impostos > CST PIS/COFINS", "ALIQ_COFINS", "saída", "alíquota COFINS (texto)", "RzDBEdit4", ""),
("Pis Entrada", "Config. Impostos > CST PIS/COFINS", "COD_ENTRADA_PIS", "entrada", "CST PIS de entrada", "RzDBButtonEdit5", ""),
("Pis Saída", "Config. Impostos > CST PIS/COFINS", "COD_SAIDA_PIS", "saída", "CST PIS de saída", "RzDBButtonEdit2", ""),
("Alíquota (Pis)", "Config. Impostos > CST PIS/COFINS", "ALIQ_PIS", "saída", "alíquota PIS (texto)", "RzDBEdit3", ""),
("CSOSN Classificação", "Config. Impostos > CSOSN", "CSOSN", "saída", "CSOSN (código)", "DBEdit75", ""),
("CSOSN Informações - Significados", "Config. Impostos > CSOSN", None, "n/a", "descrição do CSOSN (apoio visual)", "ComboBox1", "Plain TComboBox, not data-aware: presentation helper, no column."),
("Alíquota IPI %", "Config. Impostos > IPI", "IPI", "saída", "alíquota IPI %", "RzDBEdit2", "Cálculo Preço tab has other IPI/PIS/COFINS controls bound to C000026 (price calculation), not fiscal defaults."),
("CST IPI", "Config. Impostos > IPI", "CST_IPI", "saída", "CST do IPI", "RzDBEdit1", ""),
("Código da origem da Mercadoria (0/1/2)", "Config. Impostos", "COD_ORIGEM_MERCADORIA", "n/a", "origem da mercadoria (0 Nacional, 1 Estrangeira-Importação Direta, 2 Estrangeira-Mercado Interno)", "CodOrigemMercadoria",
"Radio group RgOrigemMercadoria is not data-aware; a hidden data-aware edit bound to COD_ORIGEM_MERCADORIA sits beside it. The radio->edit copy lives in event code."),
("IAT", "Config. Impostos", "IAT", "n/a", "arredondamento/truncamento (A/T)", "wwDBComboBox3", ""),
("Incidência", "Config. Impostos", "INCIDENCIA_PISCOFINS", "n/a", "tipo de incidência PIS/COFINS", "wwDBComboBox1", ""),
("PIS/COFINS (combo)", "Config. Impostos", "PISCOFINS", "n/a", "flag PIS/COFINS (N/S)", "ecombo_piscofins", ""),
("IPPT", "Config. Impostos", "IPPT", "n/a", "produção própria/terceiros (P/T)", "wwDBComboBox4", ""),
]
cur = _connect(os.environ).cursor()
cur.execute("select trim(rf.RDB$FIELD_NAME), f.RDB$FIELD_TYPE, coalesce(f.RDB$CHARACTER_LENGTH,f.RDB$FIELD_LENGTH), f.RDB$FIELD_SCALE from RDB$RELATION_FIELDS rf join RDB$FIELDS f on f.RDB$FIELD_NAME=rf.RDB$FIELD_SOURCE where rf.RDB$RELATION_NAME='C000025'")
TN = {7: "SMALLINT", 8: "INTEGER", 14: "CHAR", 37: "VARCHAR", 27: "DOUBLE PRECISION", 16: "NUMERIC", 10: "FLOAT", 35: "TIMESTAMP", 12: "DATE"}
meta = {}
for n, t, ln, sc in cur.fetchall():
meta[n] = f"{TN.get(t, t)}({ln})" if t in (14, 37) else (f"NUMERIC(*,{-sc})" if t == 16 and sc else TN.get(t, str(t)))
def stats(col):
cur.execute(f'select "{col}", count(*) from C000025 group by "{col}" order by 2 desc')
vals = [(None if v is None else str(v).strip(), n) for v, n in cur.fetchall()]
return sum(n for v, n in vals if v not in (None, "")), [v for v, _ in vals[:5]]
entries = []
for label, area, col, direction, sem, ctrl, notes in UI:
diff = by_label.get(label)
if col is None:
entries.append({"ui_label": label, "screen_area": area, "table": "C000025", "field": None, "db_type": None, "direction": direction, "semantic": sem, "before": None, "after": None,
"confirmed_by_ui_diff": False, "classification": "NO_MATCH", "confidence": "HIGH", "binding_type": "NOT_DATA_AWARE", "evidence_basis": f"DFM: {ctrl} is not data-aware",
"write_target": False, "current_population": None, "sample_values": [], "notes": notes})
continue
c = bound.get(col)
dfm_ok = bool(c) and c["name"] == ctrl and col in meta
pop, samples = stats(col) if col in meta else (None, [])
if not dfm_ok:
klass, binding, basis = "NO_MATCH", "NO_DFM_BINDING", f"expected control {ctrl} bound to {col} not found in DFM"
elif ctrl == "CodOrigemMercadoria":
klass, binding, basis = "CONFIRMED_STRONG", "DFM_RADIO_ONCLICK_HIDDEN_EDIT", "DFM: RgOrigemMercadoria (TRadioGroup, no DataField, OnClick=RgOrigemMercadoriaClick) + hidden data-aware TDBEdit CodOrigemMercadoria bound to COD_ORIGEM_MERCADORIA in the same tab, the only origin-bound control of the form; the handler and both symbols exist in the executable; values 0/1/2 observed in 5 databases"
else:
klass, binding = "CONFIRMED_STRONG", "DFM_DATAFIELD"
basis = f"DFM: {c['class']} {c['name']} DataField={col} DataSource=dsproduto2 (qrproduto = select * from c000025)"
ui_diff = bool(diff and diff.get("binding_type") == "DIRECT_BINDING" and col in diff.get("db_fields_changed", []))
if ui_diff:
klass, binding = "CONFIRMED_STRONG", "DFM_DATAFIELD+UI_DIFF"
entries.append({"ui_label": label, "screen_area": area, "table": "C000025", "field": col, "db_type": meta.get(col), "direction": direction, "semantic": sem, "before": None, "after": None,
"confirmed_by_ui_diff": ui_diff, "classification": klass, "confidence": "HIGH" if klass == "CONFIRMED_STRONG" else "MEDIUM", "binding_type": binding, "evidence_basis": basis,
"dfm_control": {"class": c["class"], "name": c["name"], "left": c.get("Left"), "top": c.get("Top"), "container_path": [p.split(":")[1] for p in c["path"][-3:]]} if c else None,
"write_target": klass == "CONFIRMED_STRONG", "current_population": pop, "sample_values": samples, "notes": notes})
cur.connection.close()
not_on_screen = [f for f in ("ORIGEM", "GRUPO_ICMS", "MARGEM_AGREGADA", "CFOP_CUPOM", "NCM") if f not in bound]
cnt = lambda k: sum(1 for e in entries if e["classification"] == k) # noqa: E731
doc = {"artifact": "SGV_PRODUCT_FISCAL_UI_MAP_V1", "product_table": "C000025", "company_table": "C000004", "company_fields": {"CRT": "C000004.CRT", "UF": "C000004.UF"},
"evidence_source": "Delphi DFM of form Tfrmproduto extracted from SGV_5.0.6.124.exe (docs/runs/sgv_product_form_bindings.json); dsproduto2 -> qrproduto = 'select * from c000025'",
"architecture_rule": "GTI Fiscal writes ONLY the canonical product record (C000025). Transactional/historical tables are evidence only.",
"transactional_table_writes_allowed": False,
"forbidden_write_tables": ["NFCE", "NFCE_ITENS", "C000032", "C000061", "C000062", "C000088", "INVENTARIO", "C000026 (price calculation)", "any historical/document/item table"],
"columns_with_no_screen_control": [f for f in not_on_screen],
"summary": {"ui_fields_total": len(entries), "CONFIRMED_STRONG": cnt("CONFIRMED_STRONG"), "PROBABLE": cnt("PROBABLE"), "AMBIGUOUS": cnt("AMBIGUOUS"), "NO_MATCH": cnt("NO_MATCH"),
"write_target_fields": [e["field"] for e in entries if e["write_target"]],
"fields_requiring_manual_ui_diff": [e["ui_label"] for e in entries if e["classification"] in ("PROBABLE", "AMBIGUOUS")],
"unidentified_controls": ["Panel1.ealiquota -> CODALIQUOTA (data-aware, not visible in the supplied screenshots; column empty 0/521)"]},
"fields": entries}
(ROOT / "docs" / "SGV_PRODUCT_FISCAL_UI_MAP_V1.json").write_text(json.dumps(doc, ensure_ascii=False, indent=1), encoding="utf-8")
print(json.dumps(doc["summary"], ensure_ascii=False))
+70
View File
@@ -0,0 +1,70 @@
"""Import operator fiscal context (CSV or JSON) into the Hub — the answers to docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V1.csv.
python scripts/import_fiscal_context.py <file.csv|file.json> --token <AGENT_TOKEN> [--hub http://localhost:8000] [--dry-run]
CSV columns (the questionnaire CSV works as-is): company_key, scope, scope_key, key, value, reason (rows with an empty `value` are skipped).
JSON: {"company_key": "...", "entries": [{"scope","scope_key","key","value","reason"}]} or a list of rows that carry company_key.
Entries are validated locally (same rules as the Hub) and sent per company; the Hub applies each batch all-or-nothing.
"""
import argparse
import csv
import json
import sys
import urllib.error
import urllib.request
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from central.app.context_keys import validate_entry # noqa: E402
def load_rows(path):
p = Path(path)
if p.suffix.lower() == ".json":
data = json.loads(p.read_text(encoding="utf-8"))
if isinstance(data, dict):
return [{"company_key": data.get("company_key"), **e} for e in data.get("entries", [])]
return data
with p.open(newline="", encoding="utf-8-sig") as f:
return list(csv.DictReader(f))
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("file"); ap.add_argument("--token", required=True); ap.add_argument("--hub", default="http://localhost:8000"); ap.add_argument("--dry-run", action="store_true")
a = ap.parse_args(argv)
by_company, errors, skipped = defaultdict(list), [], 0
for i, row in enumerate(load_rows(a.file), start=1):
if not str(row.get("value") or "").strip():
skipped += 1
continue
entry, err = validate_entry(row)
if err:
errors.append((i, err))
else:
by_company[str(row.get("company_key") or "").strip()].append(entry)
if errors:
for i, e in errors[:30]:
print(f"row {i}: {e}")
raise SystemExit(f"{len(errors)} invalid row(s); nothing was sent")
total = 0
for company, entries in by_company.items():
if not company:
raise SystemExit("company_key is required on every row")
total += len(entries)
if a.dry_run:
continue
req = urllib.request.Request(f"{a.hub}/v1/context/import", data=json.dumps({"company_key": company, "entries": entries}).encode(), method="POST",
headers={"Authorization": f"Bearer {a.token}", "Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=60) as r:
print(company, json.load(r))
except urllib.error.HTTPError as e:
raise SystemExit(f"Hub refused {company}: {e.read().decode()[:300]}")
print(f"{'validated' if a.dry_run else 'imported'} {total} entries ({skipped} unanswered rows skipped)")
if __name__ == "__main__":
main()
+22
View File
@@ -0,0 +1,22 @@
"""Read-only: per-column stats for a table. usage: inspect_erp_table.py TABLE [regex-of-cols]"""
import os, re, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src"))
from gti_fiscal_agent.firebird_reader import _connect
for l in (ROOT / ".env").read_text(encoding="utf-8").splitlines():
if "=" in l and not l.startswith("#"):
k, _, v = l.partition("="); os.environ.setdefault(k.strip(), v.strip())
t = sys.argv[1]; rx = re.compile(sys.argv[2], re.I) if len(sys.argv) > 2 else None
conn = _connect(os.environ); cur = conn.cursor()
cur.execute("select trim(rf.RDB$FIELD_NAME), f.RDB$FIELD_TYPE, coalesce(f.RDB$CHARACTER_LENGTH,f.RDB$FIELD_LENGTH) from RDB$RELATION_FIELDS rf join RDB$FIELDS f on f.RDB$FIELD_NAME=rf.RDB$FIELD_SOURCE where rf.RDB$RELATION_NAME=? order by rf.RDB$FIELD_POSITION", (t,))
for name, ty, ln in cur.fetchall():
if rx and not rx.search(name): continue
if ty in (261,): # blob
print(f"{name} BLOB"); continue
cur.execute(f'select "{name}", count(*) from "{t}" group by "{name}" order by 2 desc rows 1 to 400')
rows = cur.fetchall()
nn = sum(c for v, c in rows if v is not None)
top = [(str(v).strip()[:24] if v is not None else None, c) for v, c in rows[:5]]
print(f"{name} t{ty}/{ln} nn={nn} distinct{'>=' if len(rows)>=400 else '='}{len(rows)} {top}")
conn.close()
+397
View File
@@ -0,0 +1,397 @@
"""GTI Fiscal LOCAL COMPLETION V1 runner: snapshot -> dry-run -> guarded apply -> verify -> second pass -> reports/questions.
python scripts/local_completion.py <stage> [--db NAME ...]
stages: snapshot | dryrun | apply | verify | second | report | questions | all
Databases are ONLY those in docs/LOCAL_WRITE_ALLOWLIST_V1.json (exact canonical paths). Writes go through
scripts/firebird_write_executor.execute_guarded_product_update (path allowlist, schema/type/length guard, compare-before-write,
one UPDATE per product, rowcount, independent re-read). Nothing is written by any stage except `apply` (and `second`, which must write 0).
State lives in docs/runs/local_completion/ (git-ignored bulk files).
"""
import argparse
import gzip
import hashlib
import importlib.util
import json
import os
import pickle
import subprocess
import sys
import tempfile
from collections import Counter, defaultdict
from datetime import datetime, timezone
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 fdb # noqa: E402
import run_fiscal_decision_v1 as base # noqa: E402
from central.app.field_contract import ALREADY, CHANGE, CONTRACT, CONTRACT_VERSION, CORRECTED, MANUAL, NA, WRITE_FIELDS, Context, resolve_product # noqa: E402
from gti_fiscal_agent import company_context as cc # noqa: E402
from gti_fiscal_agent.coverage import build_refs # noqa: E402
from gti_fiscal_agent.firebird_reader import read_evidence # noqa: E402
STATE = ROOT / "docs" / "runs" / "local_completion"
HUB = "http://localhost:8000"
# Per-database company configuration (operator decisions recorded in docs/AGENT_COMPANY_CONTEXT_RULES_V1.md): the three databases with a CRT conflict use the
# emission authority EMISSOR; the company is always the C000004 row 000001 (the one whose CNPJ equals the EMISSOR CNPJ).
COMPANY_CFG = {"CONSTRUCAO": {"company_code": "000001"}, "MMMODAS": {"company_code": "000001", "crt_source": "EMISSOR"}, "RINCAO": {"company_code": "000001", "crt_source": "EMISSOR"},
"UNIAO": {"company_code": "000001", "crt_source": "EMISSOR"}, "TESTESOFICIAIS": {}}
BASE_COLS = ["CODIGO", "PRODUTO", "CODGRUPO", "TIPO", "PROD_COZINHA", "PRODUTO_SERVICO"]
s_ = lambda v: None if v is None else str(v).strip() # noqa: E731
def load_executor():
spec = importlib.util.spec_from_file_location("firebird_write_executor", ROOT / "scripts" / "firebird_write_executor.py")
mod = importlib.util.module_from_spec(spec); sys.modules[spec.name] = mod; spec.loader.exec_module(mod)
return mod
def allowlist():
doc = json.loads((ROOT / "docs" / "LOCAL_WRITE_ALLOWLIST_V1.json").read_text(encoding="utf-8"))
return {d["name"]: d["path"] for d in doc["databases"]}
def connect(path, rw=False):
kw = dict(database=path, user=os.environ["FB_USER"], password=os.environ["FB_PASSWORD"], fb_library_name=os.environ.get("FIREBIRD_CLIENT_LIBRARY"))
if not rw:
kw["isolation_level"] = fdb.ISOLATION_LEVEL_READ_COMMITED_RO
return fdb.connect(**kw)
def env_for(path):
return {"FIREBIRD_DATABASE_PATH": path, "FB_USER": os.environ["FB_USER"], "FB_PASSWORD": os.environ["FB_PASSWORD"], "FIREBIRD_CLIENT_LIBRARY": os.environ.get("FIREBIRD_CLIENT_LIBRARY")}
# ------------------------------------------------------------------------------------------------------------------ snapshot
def snapshot(name, path):
conn = connect(path); cur = conn.cursor()
cur.execute("SELECT TRIM(rf.RDB$FIELD_NAME), f.RDB$FIELD_TYPE FROM RDB$RELATION_FIELDS rf JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE WHERE rf.RDB$RELATION_NAME = 'C000025' ORDER BY rf.RDB$FIELD_POSITION")
cols = [c for c, t in cur.fetchall() if t != 261] # blobs (images) excluded
cur.execute(f"SELECT {', '.join(cols)} FROM C000025 ORDER BY CODIGO")
rows = {s_(r[cols.index("CODIGO")]): tuple(r) for r in cur.fetchall()}
side = {}
for t in ("C000004", "EMISSOR"):
try:
cur.execute(f"SELECT * FROM {t}")
names = [d[0] for d in cur.description]
data = [tuple(v.read() if hasattr(v, "read") else v for v in r) for r in cur.fetchall()]
side[t] = {"columns": names, "rows": data, "sha256": hashlib.sha256(repr(data).encode("utf-8", "replace")).hexdigest()}
except Exception as e: # noqa: BLE001
side[t] = {"error": type(e).__name__}
company = cc.resolve(cc.collect_evidence(cur), cc.config_from_env({k.upper(): v for k, v in _cfg_env(name).items()}))
conn.close()
snap = {"name": name, "path": path, "taken": datetime.now(timezone.utc).isoformat(timespec="seconds"), "columns": cols, "rows": rows, "side_tables": side,
"product_count": len(rows), "products_logical_sha256": hashlib.sha256(repr(sorted(rows.items())).encode("utf-8", "replace")).hexdigest(),
"company": {k: company.get(k) for k in ("status", "classification", "company_code", "company_cnpj", "company_uf", "company_crt", "source_of_truth", "confidence", "warnings", "reasons")}}
STATE.mkdir(parents=True, exist_ok=True)
with gzip.open(STATE / f"{name}_before.pkl.gz", "wb") as f:
pickle.dump(snap, f)
return snap
def _cfg_env(name):
cfg = COMPANY_CFG.get(name, {})
out = {}
if cfg.get("company_code"): out["COMPANY_CODE"] = cfg["company_code"]
if cfg.get("crt_source"): out["COMPANY_CRT_SOURCE"] = cfg["crt_source"]
return out
def load_snapshot(name):
with gzip.open(STATE / f"{name}_before.pkl.gz", "rb") as f:
return pickle.load(f)
# ------------------------------------------------------------------------------------------------------------------ analysis
def classify_pairs(pairs):
pairs = sorted(set(pairs))
with tempfile.TemporaryDirectory() as tmp:
fin, fout = Path(tmp) / "in.json", Path(tmp) / "out.json"
fin.write_text(json.dumps(pairs, ensure_ascii=False), encoding="utf-8")
subprocess.run(["docker", "cp", str(ROOT / "scripts" / "bulk_classify_in_container.py"), "projeto-fiscal-hub-1:/tmp/bulk.py"], check=True)
subprocess.run(["docker", "cp", str(fin), "projeto-fiscal-hub-1:/tmp/in.json"], check=True)
subprocess.run(["docker", "exec", "projeto-fiscal-hub-1", "sh", "-c", "cd /app && python /tmp/bulk.py /tmp/in.json /tmp/out.json"], check=True, capture_output=True)
subprocess.run(["docker", "cp", "projeto-fiscal-hub-1:/tmp/out.json", str(fout)], check=True)
return dict(zip(pairs, json.loads(fout.read_text(encoding="utf-8"))))
def hub_get(path, token):
return base.http(HUB + path, token)
def analyze(name, path, token, snap=None, extra_context=None):
"""Read the database (read-only) and resolve every product/field. Returns the full analysis."""
conn = connect(path); cur = conn.cursor()
cur.execute("SELECT TRIM(RDB$FIELD_NAME) FROM RDB$RELATION_FIELDS WHERE RDB$RELATION_NAME = 'C000025'")
have = {r[0] for r in cur.fetchall()}
cols = [c for c in BASE_COLS + WRITE_FIELDS if c in have]
cur.execute(f"SELECT {', '.join(cols)} FROM C000025 ORDER BY CODIGO")
products = []
for r in cur.fetchall():
p = {c: None for c in BASE_COLS + WRITE_FIELDS}
p.update(zip(cols, r))
for k in p:
if isinstance(p[k], str):
p[k] = p[k].strip()
products.append(p)
company = cc.resolve(cc.collect_evidence(cur), cc.config_from_env(_cfg_env(name)))
ev_rows = cc.collect_evidence(cur)
sel = next((c for c in ev_rows["c000004"] if c["code"] == company["company_code"]), {})
groups = {}
if "C000017" in {r[0] for r in _tables(cur)}:
cur.execute("SELECT TRIM(CODIGO), TRIM(GRUPO) FROM C000017")
groups = dict(cur.fetchall())
contrib = None
if "CONTRIBUINTE_IPI" in _cols(cur, "C000004"):
cur.execute("SELECT CODIGO, CONTRIBUINTE_IPI FROM C000004")
contrib = {s_(a): s_(b) for a, b in cur.fetchall()}.get(company["company_code"])
conn.close()
if company["status"] != "PASS":
raise RuntimeError(f"{name}: company context {company['classification']}: {company['reasons']}")
comp = {"uf": company["company_uf"], "crt": company["company_crt"], "contribuinte_ipi": contrib, "company_code": company["company_code"], "company_key": company["company_cnpj_fingerprint"]}
# ERP-emission evidence: the SGV NFC-e history shows whether the PIS/COFINS cadastral fields are consumed at emission (issued CST_PIS_COFINS non-blank share)
try:
_c = connect(path); _cur = _c.cursor()
_cur.execute("SELECT COUNT(*), COUNT(NULLIF(TRIM(CST_PIS_COFINS), '')) FROM NFCE_ITENS")
tot, nonblank = _cur.fetchone(); _c.close()
comp["pis_cofins_emission_items"] = tot; comp["pis_cofins_emission_nonblank"] = nonblank
comp["pis_cofins_unused_by_emission"] = bool(tot >= 1000 and nonblank / tot < 0.001)
except Exception as exc: # noqa: BLE001
comp["pis_cofins_unused_by_emission"] = False; comp["pis_cofins_evidence_error"] = str(exc)[:120]
evidence = read_evidence(env_for(path), company["company_code"])
refs = build_refs(lambda p: hub_get(p, token), products, comp["uf"])
entries = list(extra_context or [])
try:
entries += hub_get(f"/v1/context?company_key={comp['company_key']}", token)["entries"]
except Exception: # noqa: BLE001
pass
ctx = Context(entries)
manual = {}
for p in products:
v, _ = ctx.get("ncm", s_(p["CODIGO"]))
if v:
manual[s_(p["CODIGO"])] = v
pairs = [(s_(p["CLASSIFICACAO_FISCAL"]) or "", s_(p["PRODUTO"]) or "") for p in products] + [(v, s_(p["PRODUTO"]) or "") for p in products for v in [manual.get(s_(p["CODIGO"]))] if v]
dec = classify_pairs(pairs)
results = {}
dup_codes = {c: n for c, n in Counter(s_(p["CODIGO"]) for p in products).items() if n > 1}
seen = Counter()
for p in products:
code = s_(p["CODIGO"]); desc = s_(p["PRODUTO"]) or ""
seen[code] += 1
rk = code if seen[code] == 1 else f"{code}#{seen[code]}"
p["_rk"] = rk
m = manual.get(code)
md = dec[(m, desc)] if m else None
valid_manual = bool(md and md.get("status") == "EXACT_NCM")
d = md if valid_manual else dec[(s_(p["CLASSIFICACAO_FISCAL"]) or "", desc)]
results[rk] = resolve_product(p, d, comp, refs, evidence, ctx, manual_ncm=(m, valid_manual) if m else None)
if dup_codes.get(code, 1) > 1: # data-integrity defect (PK violation in the copy): every physical row that shares the code is blocked, all 23 cells stay accounted
for f in WRITE_FIELDS:
r = results[rk][f]
r.update(state=MANUAL, reason="DUPLICATE_KEY_BLOCKED", input_key="duplicate_code", input_scope="PRODUCT", write_eligible=False, proposed=None)
results[rk]["_ds"] = {k: (v or "")[:8] for k, v in (d.get("dataset_versions") or {}).items() if k in ("ncm", "tipi_ipi", "cest", "ba_st_mva", "cfop", "nfe_moc_tables")}
return {"name": name, "path": path, "products": products, "company": comp, "company_result": company, "results": results, "groups": groups, "context_entries": len(entries), "engine": {"contract": CONTRACT_VERSION}}
def _tables(cur):
cur.execute("SELECT TRIM(RDB$RELATION_NAME) FROM RDB$RELATIONS WHERE COALESCE(RDB$SYSTEM_FLAG,0)=0")
return cur.fetchall()
def _cols(cur, table):
cur.execute("SELECT TRIM(RDB$FIELD_NAME) FROM RDB$RELATION_FIELDS WHERE RDB$RELATION_NAME = ?", (table,))
return {r[0] for r in cur.fetchall()}
def summarize(an):
per = {f: Counter() for f in WRITE_FIELDS}
reasons = {f: Counter() for f in WRITE_FIELDS}
manual_keys = Counter()
for code, res in an["results"].items():
for f in WRITE_FIELDS:
r = res[f]
per[f][r["state"]] += 1
reasons[f][(r["state"], r["reason"])] += 1
if r["state"] == MANUAL:
manual_keys[(f, r["input_key"])] += 1
roles = Counter(res["_facts"]["role"] for res in an["results"].values())
return {"name": an["name"], "total_products": len(an["products"]), "company": an["company"], "context_entries": an["context_entries"],
"fields": {f: dict(per[f]) for f in WRITE_FIELDS}, "reasons": {f: {f"{s}|{r}": n for (s, r), n in reasons[f].most_common(12)} for f in WRITE_FIELDS},
"manual_inputs": {f"{f}|{k}": n for (f, k), n in manual_keys.most_common()}, "roles": dict(roles),
"change_required_cells": sum(per[f][CHANGE] for f in WRITE_FIELDS), "write_eligible_cells": sum(1 for res in an["results"].values() for f in WRITE_FIELDS if res[f]["write_eligible"])}
def write_ndjson(name, an, suffix):
with gzip.open(STATE / f"{name}_{suffix}.ndjson.gz", "wt", encoding="utf-8") as f:
for p in an["products"]:
code = s_(p["CODIGO"]); res = an["results"][p["_rk"]]
for fld in WRITE_FIELDS:
r = res[fld]
f.write(json.dumps({"db": an["name"], "codigo": code, "row_key": p["_rk"], "field": fld, "before": r["current"], "proposed": r["proposed"], "status": r["state"], "reason": r["reason"], "input_key": r["input_key"],
"provenance": f"{CONTRACT_VERSION}|" + ",".join(f"{k}:{v}" for k, v in res["_ds"].items()) + f"|role={res['_facts']['role']}", "write_eligible": r["write_eligible"]}, ensure_ascii=False, default=str) + "\n")
# ------------------------------------------------------------------------------------------------------------------ apply / verify
def apply(name, path, an, snap):
ex = load_executor()
ex.assert_local_completion_target(path)
now_company = cc.resolve(cc.collect_evidence(_ro_cursor(path)), cc.config_from_env(_cfg_env(name)))
if now_company["status"] != "PASS" or (now_company["company_uf"], now_company["company_crt"]) != (an["company"]["uf"], an["company"]["crt"]):
raise RuntimeError("company-context guard failed before writing")
cols = snap["columns"]
conn = connect(path, rw=True)
audit, failed, attempted, succeeded = [], [], 0, 0
try:
for p in an["products"]:
code = s_(p["CODIGO"]); res = an["results"][code]
changes = {}
for f in WRITE_FIELDS:
r = res[f]
if r["write_eligible"]:
changes[f] = (snap["rows"][code][cols.index(f)], r["proposed"])
if not changes:
continue
attempted += len(changes)
try:
rec = ex.execute_guarded_product_update(conn, database_path=path, product_code=code, changes=changes, now=datetime.now(timezone.utc).isoformat(timespec="seconds"))
audit.append({"db": name, **rec, "outcome": "SUCCEEDED"})
succeeded += len(changes)
except ex.WriteRefused as e:
failed.append({"db": name, "product_code": code, "fields": list(changes), "error": str(e)[:200]})
finally:
conn.close()
with gzip.open(STATE / f"{name}_write_audit.ndjson.gz", "wt", encoding="utf-8") as f:
for a in audit + [{**x, "outcome": "FAILED"} for x in failed]:
f.write(json.dumps(a, ensure_ascii=False, default=str) + "\n")
return {"name": name, "cells_attempted": attempted, "cells_succeeded": succeeded, "cells_failed": sum(len(x["fields"]) for x in failed), "products_updated": len(audit), "failures": failed[:20]}
def _ro_cursor(path):
return connect(path).cursor()
def verify(name, path, an, snap):
conn = connect(path); cur = conn.cursor()
cols = snap["columns"]
cur.execute(f"SELECT {', '.join(cols)} FROM C000025 ORDER BY CODIGO")
after = {s_(r[cols.index("CODIGO")]): tuple(r) for r in cur.fetchall()}
conn.close()
norm = lambda v: None if v is None else (round(float(v), 6) if isinstance(v, (int, float)) or v.__class__.__name__ == "Decimal" else str(v).strip()) # noqa: E731
planned = verified = bad_planned = unplanned_changes = 0
problems = []
for p in an["products"]:
code = s_(p["CODIGO"]); before = snap["rows"][code]; aft = after.get(code)
if aft is None:
problems.append((code, "MISSING_AFTER")); continue
exp = {f: an["results"][code][f]["proposed"] for f in WRITE_FIELDS if an["results"][code][f]["write_eligible"]}
for i, c in enumerate(cols):
if c in exp:
planned += 1
if norm(aft[i]) == norm(exp[c]):
verified += 1
else:
bad_planned += 1; problems.append((code, c, "AFTER!=PROPOSED", aft[i], exp[c]))
elif norm(aft[i]) != norm(before[i]):
unplanned_changes += 1; problems.append((code, c, "UNPLANNED_CHANGE", before[i], aft[i]))
return {"name": name, "products_after": len(after), "products_before": len(snap["rows"]), "planned_cells": planned, "post_write_verified": verified, "planned_mismatch": bad_planned,
"unplanned_changes": unplanned_changes, "problems": problems[:20]}
# ------------------------------------------------------------------------------------------------------------------ questions
def questions(analyses):
groups = defaultdict(lambda: {"products": [], "fields": Counter(), "choices": None, "examples": []})
for an in analyses:
by_code = {s_(p["CODIGO"]): p for p in an["products"]}
for code, res in an["results"].items():
p = by_code[code]
for f in WRITE_FIELDS:
r = res[f]
if r["state"] != MANUAL:
continue
key, scope = r["input_key"], r["input_scope"]
if key in ("ncm",):
gk = (an["name"], key, "PRODUCT", code, "")
elif key in ("cest", "ex_tipi"):
ncm = "".join(c for c in (p["CLASSIFICACAO_FISCAL"] or "") if c.isdigit())
gk = (an["name"], key, "NCM", ncm, "")
elif scope == "COMPANY" or key in ("entry_scope", "rounding", "icms_rate", "credit_rate", "pis_cofins_profile", "crt"):
gk = (an["name"], key, "COMPANY", "", "")
else:
grp = s_(p["CODGRUPO"]) or "(sem grupo)"
gk = (an["name"], key, "GROUP", grp, an["groups"].get(grp, ""))
g = groups[gk]
g["products"].append(code); g["fields"][f] += 1
if r.get("choices") and g["choices"] is None:
g["choices"] = r["choices"]
if len(g["examples"]) < 3:
g["examples"].append(f"{code} {s_(p['PRODUTO'])[:40]}")
from central.app.context_keys import ENUMS
out = []
for (db, key, scope, skey, gname), g in sorted(groups.items()):
allowed = sorted(ENUMS[key]) if key in ENUMS else ("JSON {pis_in_cst,pis_out_cst,pis_rate,cofins_in_cst,cofins_out_cst,cofins_rate}" if key == "pis_cofins_profile" else "free text (see key)")
out.append({"database": db, "company_key": next(a["company"]["company_key"] for a in analyses if a["name"] == db), "key": key, "scope": scope, "scope_key": skey, "group_name": gname,
"products_affected": len(set(g["products"])), "fields_affected": dict(g["fields"]), "allowed_values": allowed, "candidate_choices": g["choices"], "examples": g["examples"], "answer": ""})
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("stage"); ap.add_argument("--db", nargs="*")
a = ap.parse_args()
base.load_dotenv(ROOT / ".env")
token = os.environ["AGENT_DEV_TOKEN"]
allow = allowlist()
names = a.db or list(allow)
for n in names:
if n not in allow:
raise SystemExit(f"{n} is not in the exact write allowlist")
STATE.mkdir(parents=True, exist_ok=True)
analyses = []
for name in names:
path = allow[name]
print(f"== {name} :: {a.stage}", flush=True)
if a.stage in ("snapshot", "all"):
snap = snapshot(name, path)
print(f" snapshot: {snap['product_count']} products, company {snap['company'].get('classification')}", flush=True)
if a.stage in ("dryrun", "apply", "verify", "second", "report", "questions", "all"):
snap = load_snapshot(name)
if a.stage in ("dryrun", "all", "apply"):
an = analyze(name, path, token, snap)
(STATE / f"{name}_dryrun_summary.json").write_text(json.dumps(summarize(an), ensure_ascii=False, indent=1, default=str), encoding="utf-8")
write_ndjson(name, an, "dryrun")
print(" dry-run:", json.dumps({k: summarize(an)[k] for k in ("total_products", "change_required_cells", "write_eligible_cells")}), flush=True)
if a.stage in ("apply", "all"):
res = apply(name, path, an, snap)
(STATE / f"{name}_apply_summary.json").write_text(json.dumps(res, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
print(" apply:", json.dumps({k: res[k] for k in ("cells_attempted", "cells_succeeded", "cells_failed", "products_updated")}), flush=True)
v = verify(name, path, an, snap)
(STATE / f"{name}_verify_summary.json").write_text(json.dumps(v, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
print(" verify:", json.dumps({k: v[k] for k in ("planned_cells", "post_write_verified", "planned_mismatch", "unplanned_changes")}), flush=True)
if a.stage in ("second", "all"):
an2 = analyze(name, path, token, snap)
s2 = summarize(an2)
second = {"name": name, "SECOND_PASS_CHANGE_REQUIRED": s2["change_required_cells"], "SECOND_PASS_WRITES": 0, "fields": s2["fields"]}
if s2["write_eligible_cells"]:
second["SECOND_PASS_WRITES"] = apply(name, path, an2, snap)["cells_succeeded"]
(STATE / f"{name}_second_pass.json").write_text(json.dumps(second, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
write_ndjson(name, an2, "second")
print(" second pass:", second["SECOND_PASS_CHANGE_REQUIRED"], second["SECOND_PASS_WRITES"], flush=True)
if a.stage in ("questions",):
analyses.append(analyze(name, path, token, snap))
if a.stage == "questions":
q = questions(analyses)
(ROOT / "docs" / "LOCAL_FISCAL_CONTEXT_QUESTIONS_V1.json").write_text(json.dumps({"artifact": "LOCAL_FISCAL_CONTEXT_QUESTIONS_V1", "total_questions": len(q), "questions": q}, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
import csv
with open(ROOT / "docs" / "LOCAL_FISCAL_CONTEXT_QUESTIONS_V1.csv", "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["database", "company_key", "scope", "scope_key", "key", "group_name", "products_affected", "fields_affected", "allowed_values", "examples", "value", "reason"])
for x in q:
w.writerow([x["database"], x["company_key"], x["scope"], x["scope_key"], x["key"], x["group_name"], x["products_affected"], json.dumps(x["fields_affected"], ensure_ascii=False),
x["allowed_values"] if isinstance(x["allowed_values"], str) else "|".join(x["allowed_values"]), " ; ".join(x["examples"]), "", ""])
print("questions:", len(q))
if __name__ == "__main__":
main()
+69
View File
@@ -0,0 +1,69 @@
"""Compose the LOCAL COMPLETION V1 artifacts from docs/runs/local_completion/* (no database access):
docs/LOCAL_FULL_FISCAL_DRY_RUN_V1.json, docs/LOCAL_FULL_FISCAL_WRITE_AUDIT_V1.json, docs/LOCAL_DATABASE_VALIDATION_REPORT_V1.md, docs/local_completion_totals.json
"""
import gzip
import json
import sys
from collections import Counter, defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from central.app.field_contract import WRITE_FIELDS # noqa: E402
ST = ROOT / "docs" / "runs" / "local_completion"
NAMES = [d["name"] for d in json.loads((ROOT / "docs" / "LOCAL_WRITE_ALLOWLIST_V1.json").read_text(encoding="utf-8"))["databases"]]
load = lambda n, suffix: json.loads((ST / f"{n}_{suffix}.json").read_text(encoding="utf-8")) if (ST / f"{n}_{suffix}.json").exists() else None # noqa: E731
dry, audit_doc, per_db = {}, {}, {}
totals = Counter()
report = ["# LOCAL_DATABASE_VALIDATION_REPORT_V1", "",
"Local completion mission over the five authorized disposable copies. States per (product, field): ALREADY_CORRECT (incl. `DECLARED_VALID_NOT_CONTRADICTED`), CORRECTED (written and independently re-read), NOT_APPLICABLE, MANUAL_INPUT_REQUIRED(<input>), FAILED (write refused/failed). No cell is left in a generic state.", ""]
for n in NAMES:
d, ap, vf, sp = load(n, "dryrun_summary"), load(n, "apply_summary"), load(n, "verify_summary"), load(n, "second_pass")
if not d:
continue
failed_by_field = Counter()
ok_by_field = Counter()
p = ST / f"{n}_write_audit.ndjson.gz"
if p.exists():
with gzip.open(p, "rt", encoding="utf-8") as f:
for line in f:
a = json.loads(line)
if a.get("outcome") == "SUCCEEDED":
for fld in a["fields"]:
ok_by_field[fld] += 1
else:
for fld in a.get("fields", []):
failed_by_field[fld] += 1
rows = {}
for f in WRITE_FIELDS:
c = d["fields"][f]
corrected = ok_by_field.get(f, 0)
failed = failed_by_field.get(f, 0)
rows[f] = {"ALREADY_CORRECT": c.get("ALREADY_CORRECT", 0), "CORRECTED": corrected, "NOT_APPLICABLE": c.get("NOT_APPLICABLE", 0), "MANUAL_INPUT_REQUIRED": c.get("MANUAL_INPUT_REQUIRED", 0), "FAILED": failed,
"CHANGE_REQUIRED_planned": c.get("CHANGE_REQUIRED", 0)}
per_db[n] = {"total_products": d["total_products"], "fields": rows, "company": d["company"], "manual_inputs": d["manual_inputs"], "roles": d["roles"], "apply": ap, "verify": vf, "second_pass": sp}
for f in WRITE_FIELDS:
for k in ("ALREADY_CORRECT", "CORRECTED", "NOT_APPLICABLE", "MANUAL_INPUT_REQUIRED", "FAILED", "CHANGE_REQUIRED_planned"):
totals[k] += rows[f][k]
totals[f"{f}:{k}"] += rows[f][k]
dry[n] = {"total_products": d["total_products"], "company": d["company"], "states_before_writes": d["fields"], "top_reasons": d["reasons"], "manual_inputs": d["manual_inputs"], "roles": d["roles"],
"detail_file": f"docs/runs/local_completion/{n}_dryrun.ndjson.gz (git-ignored; one JSON line per product/field: BEFORE, PROPOSED, STATUS, REASON, PROVENANCE, WRITE_ELIGIBLE)"}
audit_doc[n] = {"path": "exact allowlist path", "apply": ap, "verify": vf, "second_pass": {k: sp[k] for k in ("SECOND_PASS_CHANGE_REQUIRED", "SECOND_PASS_WRITES")} if sp else None,
"cells_by_field": {f: {"attempted": rows[f]["CORRECTED"] + rows[f]["FAILED"], "succeeded": rows[f]["CORRECTED"], "failed": rows[f]["FAILED"]} for f in WRITE_FIELDS if rows[f]["CORRECTED"] + rows[f]["FAILED"]},
"detail_file": f"docs/runs/local_completion/{n}_write_audit.ndjson.gz"}
report += [f"## {n}", f"Produtos: {d['total_products']} | empresa: UF {d['company']['uf']} / CRT {d['company']['crt']} (código {d['company']['company_code']})", ""]
if ap:
report.append(f"Escrita: {ap['cells_succeeded']} células corrigidas em {ap['products_updated']} produtos ({ap['cells_failed']} falharam); verificação: {vf['post_write_verified']}/{vf['planned_cells']} conferem, {vf['unplanned_changes']} mudanças não planejadas. Segunda passagem: CHANGE_REQUIRED={sp['SECOND_PASS_CHANGE_REQUIRED']}, writes={sp['SECOND_PASS_WRITES']}." if sp else f"Escrita: {ap['cells_succeeded']} células corrigidas; verificação {vf['post_write_verified']}/{vf['planned_cells']}.")
report += ["", "| campo | ALREADY_CORRECT | CORRECTED | NOT_APPLICABLE | MANUAL_INPUT_REQUIRED | FAILED |", "|---|---|---|---|---|---|"]
for f in WRITE_FIELDS:
r = rows[f]
report.append(f"| {f} | {r['ALREADY_CORRECT']} | {r['CORRECTED']} | {r['NOT_APPLICABLE']} | {r['MANUAL_INPUT_REQUIRED']} | {r['FAILED']} |")
report += ["", f"Células fiscais corrigidas: **{sum(rows[f]['CORRECTED'] for f in WRITE_FIELDS)}**. Entradas manuais pedidas (campo|chave): " + ", ".join(f"{k}={v}" for k, v in list(d["manual_inputs"].items())[:8]) + " ...", ""]
(ROOT / "docs" / "LOCAL_FULL_FISCAL_DRY_RUN_V1.json").write_text(json.dumps({"artifact": "LOCAL_FULL_FISCAL_DRY_RUN_V1", "contract": "field-contract-v1", "databases": dry}, ensure_ascii=False, indent=1), encoding="utf-8")
(ROOT / "docs" / "LOCAL_FULL_FISCAL_WRITE_AUDIT_V1.json").write_text(json.dumps({"artifact": "LOCAL_FULL_FISCAL_WRITE_AUDIT_V1", "databases": audit_doc}, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
(ROOT / "docs" / "LOCAL_DATABASE_VALIDATION_REPORT_V1.md").write_text("\n".join(report) + "\n", encoding="utf-8")
(ROOT / "docs" / "runs" / "local_completion" / "totals.json").write_text(json.dumps({"totals": dict(totals), "per_db": per_db}, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
print(json.dumps({k: v for k, v in totals.items() if ":" not in k}))
+185
View File
@@ -0,0 +1,185 @@
"""Assisted single-session mapping of the SGV "Cadastro de Produto" fiscal screen (READ-ONLY tool).
python scripts/map_product_ui_fields.py --product <CODIGO>
For each UI field the tool shows CURRENT and SUGGESTED_TEMP, waits for ENTER while the operator changes ONLY that
field in the SGV and saves, re-reads the whole C000025 row, diffs it against the previous baseline and records
which column(s) changed. The tool never writes to Firebird and never touches the GUI. State is persisted after every
step (docs/SGV_PRODUCT_FISCAL_UI_DIFF_SESSION_V1.json) so the session can be resumed.
Binding classes: DIRECT_BINDING (one non-technical column changed) | DERIVED/COUPLED_FIELD (several changed;
ALL recorded, none chosen) | AUTOMATIC_METADATA (only timestamp-like technical columns) | AMBIGUOUS (nothing changed
or several changed without the expected one).
"""
import argparse
import json
import os
import sys
from datetime import datetime
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src")); sys.path.insert(0, str(ROOT / "scripts"))
SESSION = ROOT / "docs" / "SGV_PRODUCT_FISCAL_UI_DIFF_SESSION_V1.json"
# Columns the ERP rewrites by itself on every save. Only these are ignored, and they are still listed in the record.
TECHNICAL = {"ULTIMA_ALTERACAO", "DATA_ALTERACAO", "DATA_ULTIMA_ALTERACAO", "USUARIO_ALTERACAO"}
# ui_label, hypothesised column (never trusted), candidate temporary values (first one different from CURRENT wins)
SEQUENCE = [
("NCM", "CLASSIFICACAO_FISCAL", ["22030000", "19022000"]),
("CFOP entrada", "CFOP_ENTRADA", ["1102", "1949"]),
("CFOP saída", "CFOP", ["5405", "5102", "5949"]),
("CST ICMS entrada", "CST_ENTRADA", ["040", "000", "060"]),
("CST ICMS saída", "CST", ["041", "000", "060"]),
("Alíquota ICMS", "ALIQUOTA", ["12", "18", "17"]),
("CEST", "CEST", ["0601100", "1708400"]),
("CSOSN", "CSOSN", ["103", "500", "102"]),
("PIS entrada", "COD_ENTRADA_PIS", ["73", "98", "50"]),
("PIS saída", "COD_SAIDA_PIS", ["04", "01", "49"]),
("Alíquota PIS", "ALIQ_PIS", ["0,65", "1,65"]),
("COFINS entrada", "COD_ENTRADA_COFINS", ["73", "98", "50"]),
("COFINS saída", "COD_SAIDA_COFINS", ["04", "01", "49"]),
("Alíquota COFINS", "ALIQ_COFINS", ["3", "7,6"]),
("IPI", "IPI", ["5", "10", "15"]),
("CST IPI", "CST_IPI", ["50", "00", "99"]),
("EX TIPI", "EX_TIPI", ["1", "2"]),
("Origem", "ORIGEM", ["1", "2", "0"]),
("IAT", "IAT", ["T", "A"]),
("Incidência PIS/COFINS", "INCIDENCIA_PISCOFINS", ["MONOFÁSICA", "SUBST.TRIBUTÁRIA", "NENHUMA"]),
("IPPT", "IPPT", ["P", "T"]),
]
def _same(a, b):
if a is None or b is None:
return a is None and b is None
try:
return float(str(a).replace(",", ".")) == float(str(b).replace(",", "."))
except ValueError:
return str(a).strip().upper() == str(b).strip().upper()
def suggest_temp(current, candidates):
"""First candidate that differs from the current value (never the same value, never blank)."""
for c in candidates:
if not _same(current, c):
return c
return None
def classify(changes, expected_column):
"""changes: [{'FIELD','BEFORE','AFTER','technical'}]. Never picks a column when several changed."""
real = [c["FIELD"] for c in changes if not c["technical"]]
tech = [c["FIELD"] for c in changes if c["technical"]]
if not real:
return "AUTOMATIC_METADATA" if tech else "AMBIGUOUS", "no fiscal column changed" if not tech else "only technical timestamp columns changed"
if len(real) == 1:
return "DIRECT_BINDING", "exactly one column changed" + ("" if real[0] == expected_column else f" (differs from hypothesis {expected_column})")
if expected_column in real:
return "DERIVED/COUPLED_FIELD", f"{len(real)} columns changed together; none selected"
return "AMBIGUOUS", f"{len(real)} columns changed and none is the hypothesis {expected_column}"
def compute_diff(before, after):
return [{"FIELD": f, "BEFORE": before.get(f), "AFTER": after.get(f), "technical": f in TECHNICAL}
for f in sorted(set(before) | set(after)) if before.get(f) != after.get(f)]
def new_session(product):
return {"artifact": "SGV_PRODUCT_FISCAL_UI_DIFF_SESSION_V1", "database": "TESTESOFICIAIS.FDB", "product": product, "started": datetime.now().isoformat(timespec="seconds"),
"initial_snapshot": None, "baseline": None, "results": [], "firebird_writes": 0}
def save(session):
SESSION.write_text(json.dumps(session, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
def load_env():
for line in (ROOT / ".env").read_text(encoding="utf-8").splitlines():
if "=" in line and not line.startswith("#"):
k, _, v = line.partition("=")
os.environ.setdefault(k.strip(), v.strip())
def run_step(session, ui_label, column, candidates, read_row, ask):
baseline = session["baseline"]
current = baseline.get(column) if column else None
temp = suggest_temp(current, candidates) if candidates else None
print(f"\nUI_FIELD={ui_label}\nCURRENT={current}\nSUGGESTED_TEMP={temp}\nHYPOTHESIS_COLUMN={column} (not trusted)")
answer = ask("Change ONLY this field in the SGV and save, then press ENTER [s=skip (not on screen), q=quit]: ").strip().lower()
if answer == "q":
return "quit"
if answer == "s":
session["results"].append({"ui_label": ui_label, "hypothesis_column": column, "status": "NOT_ON_SCREEN", "at": datetime.now().isoformat(timespec="seconds")})
save(session)
return "skipped"
after = read_row()
changes = compute_diff(baseline, after)
binding, why = classify(changes, column)
rec = {"ui_label": ui_label, "hypothesis_column": column, "current": current, "suggested_temp": temp, "status": "TESTED", "binding_type": binding, "reason": why,
"db_fields_changed": [c["FIELD"] for c in changes if not c["technical"]], "technical_fields_changed": [c["FIELD"] for c in changes if c["technical"]],
"changes": changes, "at": datetime.now().isoformat(timespec="seconds")}
session["results"].append(rec)
session["baseline"] = after # new state is the baseline for the next field
save(session)
print(f"UI_FIELD={ui_label}\nDB_FIELDS_CHANGED={rec['db_fields_changed']}\nBEFORE={ {c['FIELD']: c['BEFORE'] for c in changes} }\nAFTER={ {c['FIELD']: c['AFTER'] for c in changes} }\nBINDING={binding} ({why})")
return "tested"
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("--product", required=True)
ap.add_argument("--restart", action="store_true", help="discard a previous session file for this product")
a = ap.parse_args(argv)
load_env()
from sgv_product_snapshot import read_row # read-only single-row reader (read-only isolation)
from gti_fiscal_agent.firebird_reader import parse_company_context, _connect
path = os.environ.get("FIREBIRD_DATABASE_PATH", "")
if Path(path).name.upper() != "TESTESOFICIAIS.FDB":
raise SystemExit("this experiment is only for the disposable TESTESOFICIAIS.FDB (FIREBIRD_DATABASE_PATH)")
reader = lambda: read_row(os.environ, a.product) # noqa: E731
if SESSION.exists() and not a.restart and json.loads(SESSION.read_text(encoding="utf-8")).get("product") == a.product:
session = json.loads(SESSION.read_text(encoding="utf-8"))
print(f"Resuming session ({len(session['results'])} results already recorded).")
session["baseline"] = reader() # honour any manual change since the last step
else:
session = new_session(a.product)
session["initial_snapshot"] = session["baseline"] = reader()
save(session)
conn = _connect(os.environ)
try:
cur = conn.cursor(); cur.execute("SELECT UF, CRT FROM C000004")
session["company_context_read_from_db"] = parse_company_context(cur.fetchall())
finally:
conn.close()
done = {r["ui_label"] for r in session["results"]}
for ui_label, column, candidates in SEQUENCE:
if ui_label in done:
continue
if run_step(session, ui_label, column, candidates, reader, input) == "quit":
break
else:
while True:
extra = input("\nAny OTHER fiscal field visible on the screen? Type its label (or ENTER to finish): ").strip()
if not extra:
break
if run_step(session, extra, None, [], reader, input) == "quit":
break
session["finished"] = datetime.now().isoformat(timespec="seconds")
save(session)
changed = {}
for r in session["results"]:
for c in r.get("changes", []):
if not c["technical"]:
changed.setdefault(c["FIELD"], session["initial_snapshot"].get(c["FIELD"]))
print("\nSession saved:", SESSION)
if changed:
print("RESTORE_ORIGINAL_VALUES (set these back in the SGV):")
for f, v in changed.items():
print(f" {f} = {v}")
if __name__ == "__main__":
main()
+67
View File
@@ -0,0 +1,67 @@
"""Manually set C000025.IPI back to NULL for one test product, to simulate ERP
drift for the incremental auto-apply test. Guarded the same way as any other
write here (path + allowlist + schema), executed once, by hand, outside the
normal Agent flow — this is test-fixture setup, not something the Agent does.
"""
import importlib.util
import json
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def load_dotenv(path):
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
os.environ[key.strip()] = value.strip()
def load_executor():
spec = importlib.util.spec_from_file_location("firebird_write_executor", ROOT / "scripts" / "firebird_write_executor.py")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def main():
code = sys.argv[1] if len(sys.argv) > 1 else "000026"
load_dotenv(ROOT / ".env")
executor = load_executor()
database_path = os.environ["FIREBIRD_DATABASE_PATH"]
executor.assert_authorized_write_path(database_path)
executor.assert_allowed_field("C000025", "IPI")
import fdb
conn = fdb.connect(
database=database_path,
user=os.environ["FB_USER"],
password=os.environ["FB_PASSWORD"],
fb_library_name=os.environ.get("FIREBIRD_CLIENT_LIBRARY"),
)
try:
cur = conn.cursor()
executor.assert_schema_ok(cur, "C000025", "IPI", None)
cur.execute("SELECT IPI FROM C000025 WHERE CODIGO = ?", (code,))
row = cur.fetchone()
before = row[0] if row else None
cur.execute("UPDATE C000025 SET IPI = NULL WHERE CODIGO = ? AND IPI = ?", (code, before))
rowcount = cur.rowcount
if rowcount != 1:
conn.rollback()
raise SystemExit(f"expected rowcount 1, got {rowcount}")
conn.commit()
cur.execute("SELECT IPI FROM C000025 WHERE CODIGO = ?", (code,))
after = cur.fetchone()[0]
print(json.dumps({"product_code": code, "before": before, "after": after, "rowcount": rowcount}, indent=2))
finally:
conn.close()
if __name__ == "__main__":
main()
+40
View File
@@ -0,0 +1,40 @@
"""Read-only operational profile of emitted documents (trusted corpus + document keys). No writes."""
import json, os, sys
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src"))
from gti_fiscal_agent.firebird_reader import _connect
for l in (ROOT / ".env").read_text(encoding="utf-8").splitlines():
if "=" in l and not l.startswith("#"):
k, _, v = l.partition("="); os.environ.setdefault(k.strip(), v.strip())
s = lambda v: None if v is None else str(v).strip()
trusted = json.loads((ROOT / "docs" / "runs" / "trusted_historical_items.json").read_text(encoding="utf-8"))
cur = _connect(os.environ).cursor()
cur.execute("select CFOP, NATUREZA, TIPO from C000030"); nat = {}
for cfop, n, t in cur.fetchall(): nat.setdefault(s(cfop), []).append((s(n), t))
cur.execute("select CHAVE from NFCE where CHAVE is not null and trim(CHAVE) <> ''"); keys = [s(r[0]) for r in cur.fetchall()]
model = Counter(k[20:22] for k in keys if len(k) == 44); uf = Counter(k[:2] for k in keys if len(k) == 44)
cur.execute("select CFOP, count(*) from NFCE_ITENS group by CFOP"); all_cfop = {s(a): b for a, b in cur.fetchall()}
cur.execute("select CFOP, CST, CSOSN, count(*) from C000062 group by CFOP, CST, CSOSN"); nf_rows = [(s(a), s(b), s(c), n) for a, b, c, n in cur.fetchall()]
cf = Counter(t["cfop"] for t in trusted); cs = Counter(t["csosn"] for t in trusted); x = Counter((t["cfop"], t["csosn"], t["cst"]) for t in trusted)
cest_by_cfop = Counter((t["cfop"], bool((t["cest"] or "").strip())) for t in trusted)
def tipo(c):
ts = {t for _, t in nat.get(c, [])}; return {"in_C000030": c in nat, "tipo_values": sorted(str(t) for t in ts), "meaning": {"1": "entrada", "2": "saída"}.get(str(next(iter(ts))) if len(ts) == 1 else "", "ambiguous/absent")}
doc = {"artifact": "ERP_OPERATION_PROFILES_V1", "firebird_writes": 0, "trusted_items": len(trusted),
"nfce_document_keys": {"analyzed": len(keys), "model_positions_20_22": dict(model), "uf_code_positions_0_2": dict(uf),
"reading": "chave de acesso: cUF (29=BA) + modelo (65=NFC-e, official: consumidor final, operação interna) — proven for the keyed subset"},
"trusted_cfop": dict(cf), "trusted_csosn": dict(cs), "trusted_cfop_csosn_cst": {"|".join(map(str, k)): v for k, v in x.most_common()},
"trusted_cest_present_by_cfop": {f"{k[0]}|cest_present={k[1]}": v for k, v in cest_by_cfop.items()},
"cfop_natureza_C000030": {c: tipo(c) for c in sorted(x for x in (set(cf) | set(all_cfop) | {r[0] for r in nf_rows}) if x)},
"all_nfce_items_cfop": all_cfop, "C000062_cfop_cst_csosn": [{"cfop": a, "cst": b, "csosn": c, "n": n} for a, b, c, n in nf_rows],
"context_stability": {
"direction": {"value": "saída", "stable": True, "evidence": "100% of NFC-e item CFOPs start with 5/6/7 (saída); C000025.CFOP is a saída default", "available_to_agent": True},
"company_uf": {"value": "BA", "stable": True, "evidence": "C000004.UF + chave cUF=29", "available_to_agent": True},
"destination_uf_nfce": {"value": "BA", "stable": True, "evidence": "NFC-e (mod 65) is intra-state consumer sale", "available_to_agent": "derivable (document model), not per product"},
"consumer_final": {"value": True, "stable": True, "evidence": "NFC-e model 65", "available_to_agent": "derivable (document model)"},
"customer_taxpayer_status": {"stable": False, "evidence": "not present on NFC-e items; NF-e items (C000062) have no customer join in this analysis", "available_to_agent": False},
"st_retention_by_supplier": {"stable": False, "evidence": "no structured field", "available_to_agent": False}},
"note": "Frequency is descriptive only; no rule is derived from it."}
(ROOT / "docs" / "ERP_OPERATION_PROFILES_V1.json").write_text(json.dumps(doc, ensure_ascii=False, indent=1), encoding="utf-8")
print(json.dumps({"model": doc["nfce_document_keys"], "cfop": cf, "csosn": cs, "x": x.most_common(6)}, ensure_ascii=True))
+95
View File
@@ -0,0 +1,95 @@
"""Zero-write proof for a Firebird database (READ-ONLY tool).
python scripts/prove_zero_write.py snapshot <FDB> <out.json> # before the run
python scripts/prove_zero_write.py compare <before.json> <after.json>
A snapshot records: (1) database-level DML counters (MON$RECORD_STATS inserts/updates/deletes), (2) a SHA-256 over every row/column of
C000025 (and C000004), (3) row counts of C000025/C000004/C000030/NFCE/NFCE_ITENS, (4) the file SHA-256 and size. `compare` reports whether
the DML counters, table digests and counts are identical. Only SELECT statements are executed. The file SHA-256 is informational only: any attach
(even an idle read-only one) touches header/page metadata, so it is not part of the verdict.
"""
import hashlib
import json
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def load_env():
f = ROOT / ".env"
if f.exists():
for line in f.read_text(encoding="utf-8").splitlines():
if "=" in line and not line.startswith("#"):
k, _, v = line.partition("=")
os.environ.setdefault(k.strip(), v.strip())
def table_digest(cur, table):
cur.execute(f"SELECT * FROM {table} ORDER BY 1")
h = hashlib.sha256()
n = 0
for row in cur.fetchall():
h.update(repr([v.read() if hasattr(v, "read") else v for v in row]).encode("utf-8", "replace"))
n += 1
return {"rows": n, "sha256": h.hexdigest()}
def snapshot(path):
import fdb
conn = 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)
try:
cur = conn.cursor()
stats = None
try:
cur.execute("SELECT r.MON$RECORD_INSERTS, r.MON$RECORD_UPDATES, r.MON$RECORD_DELETES FROM MON$RECORD_STATS r JOIN MON$DATABASE d ON d.MON$STAT_ID = r.MON$STAT_ID")
row = cur.fetchone()
stats = {"inserts": row[0], "updates": row[1], "deletes": row[2]}
except Exception as e: # noqa: BLE001
stats = {"unavailable": type(e).__name__}
digests = {t: table_digest(cur, t) for t in ("C000025", "C000004")}
counts = {}
for t in ("C000030", "NFCE", "NFCE_ITENS", "C000061", "C000032"):
try:
cur.execute(f"SELECT COUNT(*) FROM {t}")
counts[t] = cur.fetchone()[0]
except Exception: # noqa: BLE001
counts[t] = None
finally:
conn.close()
h = hashlib.sha256()
with open(path, "rb") as f:
for block in iter(lambda: f.read(8 * 1024 * 1024), b""):
h.update(block)
return {"database": Path(path).name, "dml_counters": stats, "table_digests": digests, "row_counts": counts, "file_sha256": h.hexdigest(), "file_size": Path(path).stat().st_size}
def compare(before, after):
res = {
"dml_counters_identical": before["dml_counters"] == after["dml_counters"],
"table_digests_identical": before["table_digests"] == after["table_digests"],
"row_counts_identical": before["row_counts"] == after["row_counts"],
"file_sha256_identical": before["file_sha256"] == after["file_sha256"],
"dml_counters": {"before": before["dml_counters"], "after": after["dml_counters"]},
}
res["ZERO_WRITE_PROVEN"] = res["table_digests_identical"] and res["row_counts_identical"] and res["dml_counters_identical"]
return res
def main(argv):
load_env()
if argv[0] == "snapshot":
Path(argv[2]).write_text(json.dumps(snapshot(argv[1]), indent=1), encoding="utf-8")
print("snapshot written")
elif argv[0] == "compare":
r = compare(json.loads(Path(argv[1]).read_text(encoding="utf-8")), json.loads(Path(argv[2]).read_text(encoding="utf-8")))
print(json.dumps(r, indent=1))
raise SystemExit(0 if r["ZERO_WRITE_PROVEN"] else 1)
else:
raise SystemExit(__doc__)
if __name__ == "__main__":
main(sys.argv[1:])
+119
View File
@@ -0,0 +1,119 @@
"""Read-only NCM historical reconciliation over NFCE_ITENS vs C000025.
Explains every NFC-e item whose emitted NCM differs from the product's current NCM and builds the
TRUSTED_HISTORICAL_ITEMS corpus (identity-proven items outside systemic bad-join windows).
Outputs: docs/NCM_HISTORICAL_RECONCILIATION_V1.json, docs/runs/trusted_historical_items.json
"""
import json, os, sys, urllib.request
from collections import Counter, defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src"))
from gti_fiscal_agent.firebird_reader import _connect # noqa: E402
for l in (ROOT / ".env").read_text(encoding="utf-8").splitlines():
if "=" in l and not l.startswith("#"):
k, _, v = l.partition("="); os.environ.setdefault(k.strip(), v.strip())
WINDOW_RATE = 0.5 # month is a systemic bad-join window when >= 50% of its NCM-bearing items disagree
s = lambda v: None if v is None else str(v).strip()
dg = lambda v: "".join(ch for ch in str(v or "") if ch.isdigit())
def ncm_status(tok, ncms):
out = {}
ncms = sorted(n for n in ncms if n)
for i in range(0, len(ncms), 100):
chunk = ncms[i:i + 100]
req = urllib.request.Request("http://localhost:8000/v1/classify/batch", data=json.dumps({"products": [{"current_ncm": n, "description": ""} for n in chunk]}).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + tok})
for n, r in zip(chunk, json.load(urllib.request.urlopen(req, timeout=600))["results"]):
out[n] = r.get("status")
return out
def main():
tok = [l.split("=", 1)[1].strip() for l in (ROOT / ".env").read_text(encoding="utf-8").splitlines() if l.startswith("AGENT_DEV_TOKEN=")][0]
cur = _connect(os.environ).cursor()
cur.execute("select CODIGO, PRODUTO, CLASSIFICACAO_FISCAL, DATA_CADASTRO, ULTIMA_ALTERACAO, PRECOVENDA from C000025")
P = {s(r[0]): {"desc": s(r[1]), "ncm": s(r[2]) or "", "cad": r[3], "alt": r[4], "price": r[5]} for r in cur.fetchall()}
by_ncm = defaultdict(list)
for k, p in P.items():
by_ncm[p["ncm"]].append((k, p["price"]))
cur.execute("select ID, CODPRODUTO, NCM, DATA, VALOR, CFOP, CSOSN, CST, CEST, GRUPO_ICMS, CST_PIS_COFINS, NUMERO_NOTA from NFCE_ITENS where NCM is not null")
items = [dict(zip(("id", "cod", "ncm", "date", "val", "cfop", "csosn", "cst", "cest", "gicms", "pis", "nota"), r)) for r in cur.fetchall()]
for it in items:
it["cod"] = s(it["cod"]); it["ncm"] = s(it["ncm"]) or ""
joined = [it for it in items if it["cod"] in P]
orphans = [it for it in items if it["cod"] not in P]
month = lambda it: str(it["date"])[:7]
mstat = defaultdict(lambda: [0, 0])
for it in joined:
mstat[month(it)][0 if it["ncm"] == P[it["cod"]]["ncm"] else 1] += 1
windows = {m for m, (ok, bad) in mstat.items() if bad and bad / (ok + bad) >= WINDOW_RATE}
status = ncm_status(tok, {it["ncm"] for it in joined} | {p["ncm"] for p in P.values()})
# per-product timeline outside systemic windows for change detection
timeline = defaultdict(list)
for it in joined:
if month(it) not in windows:
timeline[it["cod"]].append((it["date"], it["ncm"]))
def changed_after(cod, cur_ncm):
seq = sorted(timeline[cod])
vals = [n for _, n in seq]
if cur_ncm not in vals or vals[0] == cur_ncm:
return False
first_cur = vals.index(cur_ncm)
return all(v != cur_ncm for v in vals[:first_cur]) and all(v == cur_ncm for v in vals[first_cur:])
cls = Counter(); reasons = Counter(); by_month = Counter(); examples = defaultdict(list); trusted = []
for it in joined:
p = P[it["cod"]]
if it["ncm"] == p["ncm"]:
if month(it) not in windows and len(dg(it["ncm"])) == 8:
trusted.append(it)
continue
if dg(it["ncm"]).lstrip("0") == dg(p["ncm"]).lstrip("0") and dg(it["ncm"]):
c, r = "NORMALIZATION_BUG", "DIGITS_EQUAL_AFTER_NORMALIZATION"
elif status.get(it["ncm"]) == "LEGACY_NCM" or status.get(p["ncm"]) == "LEGACY_NCM":
c, r = "LEGACY_NCM", "ITEM_OR_PRODUCT_NCM_IS_LEGACY"
elif month(it) in windows:
c, r = "BAD_JOIN", f"SYSTEMIC_WINDOW_{month(it)}"
elif any(k != it["cod"] and pr is not None and it["val"] is not None and abs(float(pr) - float(it["val"])) < 0.005 for k, pr in by_ncm.get(it["ncm"], [])):
c, r = "BAD_JOIN", "ALT_PRODUCT_WITH_SAME_NCM_AND_PRICE"
elif p["alt"] is not None and str(p["alt"])[:10] > str(it["date"])[:10] and changed_after(it["cod"], p["ncm"]):
c, r = "PRODUCT_CHANGED_AFTER_DOCUMENT", "MONOTONIC_SWITCH_TO_CURRENT_NCM_AND_PRODUCT_MODIFIED_LATER"
elif status.get(it["ncm"]) == "EXACT_NCM":
vals = [n for _, n in timeline[it["cod"]]]
if vals and vals.count(p["ncm"]) / len(vals) > 0.5:
c, r = "TRUE_HISTORICAL_DIFFERENCE", "ISOLATED_DIFFERENCE_PRODUCT_OTHERWISE_CONSISTENT_UNPROVEN_CAUSE"
else:
c, r = "UNKNOWN", "NO_CONSISTENT_PRODUCT_HISTORY"
else:
c, r = "UNKNOWN", "ITEM_NCM_NOT_VALID_IN_NCM_DATASET"
cls[c] += 1; reasons[(c, r)] += 1; by_month[month(it)] += 1
if len(examples[c]) < 4:
examples[c].append({"nfce_item_id": it["id"], "product": it["cod"], "product_desc": p["desc"], "product_ncm": p["ncm"], "item_ncm": it["ncm"], "date": str(it["date"])[:10], "reason": r})
n_mis = sum(cls.values())
trusted_ids = [t["id"] for t in trusted]
doc = {"artifact": "NCM_HISTORICAL_RECONCILIATION_V1", "firebird_writes": 0,
"items_with_ncm": len(items), "items_joined_to_product": len(joined), "orphan_items_no_product": len(orphans),
"mismatches_total": n_mis, "mismatches_in_initial_sample_5000": 1671,
"classification": dict(cls), "reasons": {f"{c}|{r}": n for (c, r), n in reasons.most_common()},
"systemic_bad_join_windows": {m: {"matches": mstat[m][0], "mismatches": mstat[m][1]} for m in sorted(windows)},
"window_rule": f"month with >= {int(WINDOW_RATE*100)}% NCM disagreement is treated as a systemic bad-join window",
"mismatches_by_month": dict(sorted(by_month.items())), "examples": examples,
"technical_bugs_found": {"normalization": cls["NORMALIZATION_BUG"], "note": "0 => no code bug; the mismatches are data-identity problems (CODPRODUTO does not reliably identify the product in 2022-12 and a few other windows)."},
"trusted_corpus": {"definition": "NFC-e item with NCM present, product exists, item NCM == product NCM (identity proof), 8-digit NCM, month not a systemic window",
"TRUSTED_HISTORICAL_ITEMS": len(trusted), "excluded_matches_in_windows": sum(mstat[m][0] for m in windows),
"items_without_ncm_not_trusted": 34946 - len(items)}}
(ROOT / "docs" / "NCM_HISTORICAL_RECONCILIATION_V1.json").write_text(json.dumps(doc, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
(ROOT / "docs" / "runs" / "trusted_historical_items.json").write_text(json.dumps([{k: (str(v) if k == "date" else v) for k, v in t.items()} for t in trusted], ensure_ascii=False, default=str), encoding="utf-8")
print(json.dumps({k: doc[k] for k in ("items_with_ncm", "items_joined_to_product", "orphan_items_no_product", "mismatches_total", "classification", "systemic_bad_join_windows")}, ensure_ascii=True), len(trusted))
main()
+52
View File
@@ -0,0 +1,52 @@
"""Write release/RELEASE_MANIFEST.json: one coherent record of what RC1 is made of (no secrets).
python scripts/release_manifest.py <AGENT_TOKEN> [HUB_URL]
Registers together: release version, agent/hub versions, fiscal engine + decision versions, engine module versions, ACTIVE dataset versions
(kind, provider, sha256, record_count) read from the running Hub, alembic head (from the migration files) and the applied revision.
"""
import json
import re
import subprocess
import sys
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
def get(url, token):
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)
def main():
token = sys.argv[1]
hub = sys.argv[2] if len(sys.argv) > 2 else "http://localhost:8000"
from central.app.classification import DECISION_VERSION, ENGINE_VERSION
from central.app.fiscal_decision import DECISION_ENGINE_VERSION
from central.app.icms_context import ICMS_CONTEXT_ENGINE_VERSION
from central.app.product_defaults import PRODUCT_DEFAULTS_ENGINE_VERSION
version = (ROOT / "release" / "VERSION").read_text(encoding="utf-8").strip()
heads = sorted(p.name.split("_")[0] for p in (ROOT / "central" / "alembic" / "versions").glob("00*.py"))
try:
applied = subprocess.run(["docker", "exec", "projeto-fiscal-hub-1", "sh", "-c", "cd /app && alembic -c central/alembic.ini current"], capture_output=True, text=True, timeout=60).stdout
applied = re.findall(r"^(\d{4})", applied, re.M)[-1:] or ["unknown"]
except Exception: # noqa: BLE001
applied = ["unknown"]
datasets = sorted(({"kind": d["kind"], "sha256": d["sha256"][:16], "record_count": d["record_count"]} for d in get(f"{hub}/v1/datasets/status", token) if d["status"] == "ACTIVE"), key=lambda x: x["kind"])
manifest = {"artifact": "RELEASE_MANIFEST", "GTI_FISCAL_VERSION": version, "generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"hub": get(f"{hub}/version", token), "agent_version": version,
"fiscal_engine": {"ENGINE_VERSION": ENGINE_VERSION, "DECISION_VERSION": DECISION_VERSION, "DECISION_ENGINE_VERSION": DECISION_ENGINE_VERSION,
"PRODUCT_DEFAULTS_ENGINE_VERSION": PRODUCT_DEFAULTS_ENGINE_VERSION, "ICMS_CONTEXT_ENGINE_VERSION": ICMS_CONTEXT_ENGINE_VERSION},
"migrations": {"alembic_head_in_repo": heads[-1], "applied_revision": applied[0], "count": len(heads)},
"datasets_active": datasets, "note": "A release bump does not change fiscal semantics; engine/decision versions are versioned independently."}
(ROOT / "release" / "RELEASE_MANIFEST.json").write_text(json.dumps(manifest, indent=1), encoding="utf-8")
print(json.dumps({"version": version, "engine": ENGINE_VERSION, "migrations": manifest["migrations"], "datasets": len(datasets)}))
if __name__ == "__main__":
main()
+39
View File
@@ -0,0 +1,39 @@
"""Guarded rollback of one field to the BEFORE values recorded in the local-completion write audit.
usage: python scripts/rollback_field.py FIELD [--db NAME ...] Writes a rollback audit next to the original audit and verifies 100% against the BEFORE snapshot."""
import argparse, gzip, json, pickle, sys
from datetime import datetime, timezone
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from dotenv import load_dotenv
import local_completion as lc
ap = argparse.ArgumentParser(); ap.add_argument("field"); ap.add_argument("--db", nargs="*"); ap.add_argument("--plan", action="store_true", help="only the codes listed in rollback_plan.json"); a = ap.parse_args()
plan = json.loads((Path(__file__).resolve().parents[1] / "docs" / "runs" / "local_completion" / "rollback_plan.json").read_text()) if a.plan else None
load_dotenv(lc.ROOT / ".env")
ex = lc.load_executor(); al = lc.allowlist()
out = {}
for name in a.db or list(al):
path = al[name]; todo = []
for line in gzip.open(lc.STATE / f"{name}_write_audit.ndjson.gz", "rt", encoding="utf-8"):
r = json.loads(line)
if r.get("outcome") == "SUCCEEDED" and a.field in r["fields"] and (plan is None or r["product_code"] in plan.get(name, {}).get(a.field, [])):
todo.append((r["product_code"], r["fields"][a.field]["before"], r["fields"][a.field]["after"]))
conn = lc.connect(path, rw=True); ok = fail = 0; audit = []
for code, before, after in todo:
try:
res = ex.execute_guarded_product_update(conn, database_path=path, product_code=code, changes={a.field: (after, before)}, now=datetime.now(timezone.utc).isoformat(), restore_before=True)
audit.append({**res, "outcome": "ROLLED_BACK"}); ok += 1
except Exception as e: # noqa: BLE001
audit.append({"product_code": code, "outcome": "FAILED", "error": str(e)}); fail += 1
conn.close()
with gzip.open(lc.STATE / f"{name}_rollback_{a.field}{"_plan" if plan else ""}.ndjson.gz", "wt", encoding="utf-8") as f:
for r in audit: f.write(json.dumps(r, default=str) + "\n")
# independent verification against the BEFORE snapshot
snap = pickle.load(gzip.open(lc.STATE / f"{name}_before.pkl.gz", "rb")); cols = snap["columns"]; i = cols.index(a.field)
c2 = lc.connect(path); cur = c2.cursor(); cur.execute(f"select CODIGO, {a.field} from C000025"); live = {}
for k, v in cur.fetchall(): live.setdefault(k, []).append(v)
n = lambda v: None if v is None else str(v).strip() # noqa: E731
mism = sum(1 for k, r in snap["rows"].items() if (plan is None or k in plan.get(name, {}).get(a.field, [])) and k in live and any(n(v) != n(r[i]) for v in live[k]))
out[name] = {"rollback_planned": len(todo), "rolled_back": ok, "failed": fail, "column_mismatch_vs_before_snapshot": mism, "rows_snapshot": len(snap["rows"])}
print(name, out[name], flush=True)
json.dump(out, open(lc.STATE / f"rollback_{a.field}{"_plan" if plan else ""}_summary.json", "w"), indent=1)
+661
View File
@@ -0,0 +1,661 @@
"""Run the narrowly scoped, read-only Mission 00 Firebird probe."""
from __future__ import annotations
import argparse
import ctypes
import importlib.metadata
import json
import os
import platform
import re
import struct
import sys
from pathlib import Path
from typing import Any
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
from gti_discovery.firebird_connection import (
AUTHORIZED_DATABASE_PATH,
_FirebirdAdapter,
_ADAPTER_CREATION_TOKEN,
_create_firebird_adapter,
connect_authorized_read_only,
get_database_identity,
validate_authorized_database_path,
validate_authorized_database_identity,
)
from gti_discovery.collector import _safe_sample_rows, collect_metadata
from gti_discovery.fingerprint import (
SCHEMA_CHANGED,
SCHEMA_SUPPORTED,
SCHEMA_UNKNOWN,
build_fingerprint,
classify_schema,
)
from gti_discovery.mapping import build_schema_mapping, identify_product_candidates
from gti_discovery.provenance import (
ProvenanceError,
validate_fdb_provenance,
validate_imported_fdb_provenance,
)
DEFAULT_TARGET = AUTHORIZED_DATABASE_PATH
CLIENT_LIBRARY = Path(r"C:\Program Files\Firebird\Firebird_2_5\bin\fbclient.dll")
_BLOCKER_CATEGORIES = {
ValueError: "target_rejected",
ImportError: "driver_unavailable",
FileNotFoundError: "client_library_unavailable",
PermissionError: "connection_failed",
RuntimeError: "connection_failed",
}
class _FdbDriver:
version = "unknown"
def __init__(self, fdb_module: Any):
self._fdb = fdb_module
self.version = str(getattr(fdb_module, "__version__", "unknown"))
self._last_connection = None
def connect(self, **options):
if set(options) - {"database", "read_only", "client_library"}:
raise RuntimeError("unexpected connection settings")
if options.pop("read_only", None) is not True:
raise RuntimeError("read-only mode is required")
client_library = options.pop("client_library", None)
user = os.environ.get("FB_USER")
password = os.environ.get("FB_PASSWORD")
if not user or not password:
raise RuntimeError("Firebird credentials are unavailable in FB_USER/FB_PASSWORD")
isolation_level = _read_only_isolation_level(self._fdb)
raw = self._fdb.connect(
database=options["database"],
user=user,
password=password,
fb_library_name=client_library,
isolation_level=isolation_level,
)
self._last_connection = raw
observed = getattr(raw, "default_tpb", None)
if observed != isolation_level:
close = getattr(raw, "close", None)
if callable(close):
close()
raise RuntimeError("driver did not establish the fdb read-only TPB")
evidence = {
"accepted": True,
"driver_api": "fdb.Connection.default_tpb",
"configured_tpb": bytes(isolation_level),
"observed_default_tpb": bytes(observed),
}
return _create_firebird_adapter(raw, evidence, _ADAPTER_CREATION_TOKEN)
def facts(self) -> dict[str, Any]:
return {name: _fact(self._last_connection, name) for name in (
"engine_version", "database_sql_dialect", "ods_version",
"ods_minor_version", "charset", "page_size",
)}
def compatibility_evidence(self, client_version: str) -> dict[str, Any]:
isolation_level = _read_only_isolation_level(self._fdb)
return {
"fdb_version": self.version,
"client_version": client_version,
"firebird_version": ".".join(str(client_version).split(".")[:3]),
"read_only_isolation": _read_only_isolation_level(self._fdb) is not None,
"read_only_tpb": bytes(isolation_level),
"read_only_api": "fdb.Connection.default_tpb",
}
def _read_only_isolation_level(fdb: Any) -> Any:
"""Use fdb 2.0.4's actual read-only TPB constant (including its spelling)."""
value = getattr(fdb, "ISOLATION_LEVEL_READ_COMMITED_RO", None)
if isinstance(value, bytes) and value == b"\x03\x08\x06\x0f\x11":
return value
raise RuntimeError("fdb read-only isolation API is unavailable")
def _fact(connection: Any, name: str) -> Any:
try:
value = getattr(connection, name)
except Exception:
return None
if isinstance(value, (str, int, float, bool)) or value is None:
return value
return str(value)
def _read_file_version(path: Path) -> str | None:
"""Read Windows file-version metadata without loading the client DLL."""
if sys.platform != "win32":
return None
try:
version = ctypes.windll.version
size = version.GetFileVersionInfoSizeW(str(path), None)
if not size:
return None
buffer = ctypes.create_string_buffer(size)
if not version.GetFileVersionInfoW(str(path), 0, size, buffer):
return None
pointer = ctypes.c_void_p()
length = ctypes.c_uint()
if not version.VerQueryValueW(buffer, "\\", ctypes.byref(pointer), ctypes.byref(length)):
return None
class FixedFileInfo(ctypes.Structure):
_fields_ = [("signature", ctypes.c_uint32), ("struct_version", ctypes.c_uint32),
("file_version_ms", ctypes.c_uint32), ("file_version_ls", ctypes.c_uint32)]
info = ctypes.cast(pointer, ctypes.POINTER(FixedFileInfo)).contents
return ".".join((str(info.file_version_ms >> 16), str(info.file_version_ms & 0xffff),
str(info.file_version_ls >> 16), str(info.file_version_ls & 0xffff)))
except Exception:
return None
def _client_metadata(path: Path) -> dict[str, Any]:
exists = path.is_file()
return {"path": str(path), "exists": exists, "version": _read_file_version(path) if exists else None}
def _read_client_architecture(path: Path) -> str:
"""Read PE architecture metadata without loading or executing the DLL."""
try:
with path.open("rb") as handle:
if handle.read(2) != b"MZ":
return "UNKNOWN"
handle.seek(0x3C)
offset = struct.unpack("<I", handle.read(4))[0]
handle.seek(offset)
if handle.read(4) != b"PE\x00\x00":
return "UNKNOWN"
machine = struct.unpack("<H", handle.read(2))[0]
except (OSError, struct.error):
return "UNKNOWN"
return {0x014C: "x86", 0x8664: "x64", 0xAA64: "arm64"}.get(machine, "UNKNOWN")
def _python_architecture() -> str:
return "x64" if struct.calcsize("P") == 8 else "x86" if struct.calcsize("P") == 4 else "UNKNOWN"
def validate_firebird_runtime(*, fdb_module: Any = None) -> dict[str, Any]:
"""Return deterministic dependency evidence; never infer unknown facts."""
try:
provenance = validate_fdb_provenance()
except ProvenanceError as error:
return {"status": "UNKNOWN", "blocker": "compatibility_unproven", "provenance": error.evidence}
client = _client_metadata(CLIENT_LIBRARY)
client = dict(client)
client["architecture"] = client.get("architecture", _read_client_architecture(CLIENT_LIBRARY))
distribution_version = None
try:
distribution_version = importlib.metadata.version("fdb")
except (importlib.metadata.PackageNotFoundError, ImportError):
pass
fdb_version = getattr(fdb_module, "__version__", distribution_version)
python_architecture = _python_architecture()
read_only_api = (
fdb_module is not None
and isinstance(getattr(fdb_module, "ISOLATION_LEVEL_READ_COMMITED_RO", None), bytes)
and getattr(fdb_module, "ISOLATION_LEVEL_READ_COMMITED_RO") == b"\x03\x08\x06\x0f\x11"
)
version_match = fdb_version == "2.0.4" and distribution_version == "2.0.4"
architecture_match = client["architecture"] in {"x86", "x64", "arm64"} and (
(python_architecture == "x64" and client["architecture"] == "x64")
or (python_architecture == "x86" and client["architecture"] == "x86")
)
valid = (
provenance.get("status") == "VALID"
and client["exists"] is True
and client["version"] == "2.5.9.27139"
and client["architecture"] != "UNKNOWN"
and architecture_match
and version_match
and fdb_module is not None
and read_only_api
)
compatibility = {
"status": "VALID" if valid else "UNKNOWN",
"python_client_architecture_match": architecture_match,
"fdb_client_version_match": version_match,
"read_only_api": read_only_api,
}
return {
"status": "VALID" if valid else "UNKNOWN",
"python_runtime": {"version": platform.python_version(), "architecture": python_architecture},
"fdb": {"version": fdb_version, "distribution_version": distribution_version},
"client": client,
"provenance": provenance,
"compatibility": compatibility,
**({} if valid else {"blocker": "compatibility_unproven"}),
}
def _blocker_category(error: Exception) -> str:
if type(error).__name__ == "DatabaseError" and (
"-902" in str(error) or "user name and password are not defined" in str(error).lower()
):
return "credentials_rejected"
if isinstance(error, ValueError) and "read-only" in str(error).lower():
return "read_only_unproven"
if isinstance(error, RuntimeError) and "credentials" in str(error).lower():
return "credentials_unavailable"
if isinstance(error, RuntimeError) and "compatib" in str(error).lower():
return "compatibility_unproven"
if isinstance(error, RuntimeError) and "provenance" in str(error).lower():
return "compatibility_unproven"
return next((category for error_type, category in _BLOCKER_CATEGORIES.items()
if isinstance(error, error_type)), "unexpected_failure")
def _firebird_259(version: Any) -> bool:
match = re.match(r"^(\d+)\.(\d+)\.(\d+)", str(version or ""))
return bool(match and tuple(int(item) for item in match.groups()) == (2, 5, 9))
def _validate_runtime_compatibility(driver: Any) -> dict[str, Any]:
"""Prove the installed 2.5.9 client and usable driver before connect."""
client = _client_metadata(CLIENT_LIBRARY)
if not client["exists"] or not _firebird_259(client["version"]):
raise RuntimeError("Firebird 2.5.9 client compatibility is unproven")
if type(driver) is not _FdbDriver:
raise RuntimeError("Firebird driver/client compatibility is unproven")
try:
import fdb as installed_fdb
installed_version = importlib.metadata.version("fdb")
except (ImportError, importlib.metadata.PackageNotFoundError) as error:
raise RuntimeError("Firebird driver/client compatibility is unproven") from error
if (
driver._fdb is not installed_fdb
or installed_version != "2.0.4"
or getattr(installed_fdb, "__version__", None) != "2.0.4"
or _read_only_isolation_level(installed_fdb) != b"\x03\x08\x06\x0f\x11"
):
raise RuntimeError("Firebird driver/client compatibility is unproven")
return client
def _validate_preimport_compatibility() -> dict[str, Any]:
provenance = validate_fdb_provenance()
client = _client_metadata(CLIENT_LIBRARY)
client = dict(client)
client["architecture"] = client.get("architecture", _read_client_architecture(CLIENT_LIBRARY))
try:
fdb_version = importlib.metadata.version("fdb")
except importlib.metadata.PackageNotFoundError as error:
raise RuntimeError("fdb 2.0.4 compatibility is unproven") from error
if (
not client["exists"]
or not _firebird_259(client["version"])
or client["architecture"] == "UNKNOWN"
or client["architecture"] != _python_architecture()
or fdb_version != "2.0.4"
):
raise RuntimeError("Firebird 2.5.9/fdb 2.0.4 compatibility is unproven")
if not _preimport_read_only_api_proven():
raise RuntimeError("Firebird read-only isolation API compatibility is unproven")
return {"client": client, "provenance": provenance}
def _preimport_read_only_api_proven() -> bool:
"""Prove the pinned driver's read-only API from installed source, without import."""
try:
spec = __import__("importlib.util", fromlist=["find_spec"]).find_spec("fdb")
locations = list(getattr(spec, "submodule_search_locations", ()) or ())
if not locations:
return False
package = Path(locations[0])
init_source = (package / "__init__.py").read_text(encoding="utf-8")
core_source = (package / "fbcore.py").read_text(encoding="utf-8")
except (OSError, UnicodeError, ImportError, AttributeError, TypeError):
return False
return (
"ISOLATION_LEVEL_READ_COMMITED_RO" in init_source
and "ISOLATION_LEVEL_READ_COMMITED_RO = bs([" in core_source
and all(token in core_source for token in (
"isc_tpb_version3", "isc_tpb_read", "isc_tpb_wait",
"isc_tpb_read_committed", "isc_tpb_rec_version",
))
)
def _security_gate(path: os.PathLike[str] | str, driver: Any) -> dict[str, Any]:
validate_authorized_database_path(path)
if not os.environ.get("FB_USER") or not os.environ.get("FB_PASSWORD"):
raise RuntimeError("Firebird credentials are unavailable in FB_USER/FB_PASSWORD")
evidence = validate_firebird_runtime(fdb_module=getattr(driver, "_fdb", None))
if evidence["status"] != "VALID":
raise RuntimeError("Firebird driver/client compatibility is unproven")
return {"client": evidence["client"], "compatibility_evidence": evidence}
_SAMPLE_DENYLIST = (
"CUSTOMER", "CLIENT", "EMAIL", "PHONE", "MOBILE", "CONTACT", "ADDRESS",
"PRICE", "COST", "AMOUNT", "TOTAL", "TAX", "CRED", "PASSWORD",
"SECRET", "TOKEN", "BLOB", "SOURCE", "DEFAULT", "SALARY", "MARGIN",
"DISCOUNT", "PROFIT", "IMAGE", "PHOTO", "FILE", "PATH", "URL",
)
_SAMPLE_ALLOWLIST = (
"ID", "KEY", "CODE", "CODIGO", "SKU", "ITEM", "PRODUCT", "DESCRIPTION",
"DESCRICAO", "NAME", "NOME", "BARCODE", "EAN", "GTIN", "REFERENCE",
"REFERENCIA", "REF", "UNIT", "UNIDADE", "CATEGORY", "GROUP", "GRUPO",
"BRAND", "MARCA", "PRODUTO", "ESTOQUE", "STATUS", "ACTIVE", "ATIVO",
)
def _permitted_sample_plan(metadata: dict[str, Any]) -> list[tuple[str, list[str]]]:
fields_by_relation: dict[str, list[dict[str, Any]]] = {}
for field in metadata.get("fields", []):
if not isinstance(field, dict):
continue
relation = str(field.get("RELATION_NAME", "")).strip()
if relation:
fields_by_relation.setdefault(relation.upper(), []).append(field)
candidates = identify_product_candidates(metadata, {})
plan = []
for candidate in candidates:
relation = str(candidate.get("relation", "")).strip()
if not relation or candidate.get("classification") == "UNKNOWN":
continue
columns = []
for field in fields_by_relation.get(relation.upper(), []):
name = str(field.get("FIELD_NAME", "")).strip()
type_name = str(field.get("TYPE_NAME", field.get("FIELD_TYPE", ""))).upper()
upper_name = name.upper()
if not name or any(word in upper_name for word in _SAMPLE_DENYLIST):
continue
if "BLOB" in type_name or not any(word in upper_name for word in _SAMPLE_ALLOWLIST):
continue
columns.append(name)
if columns:
plan.append((relation, columns[:8]))
return plan
def _collect_samples(reader: Any, metadata: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
samples = {}
plan = _permitted_sample_plan(metadata)
if not isinstance(plan, list) or any(
not isinstance(item, tuple)
or len(item) != 2
or not isinstance(item[0], str)
or not isinstance(item[1], list)
or not item[1]
or any(
not isinstance(column, str)
or any(word in column.upper() for word in _SAMPLE_DENYLIST)
or not any(word in column.upper() for word in _SAMPLE_ALLOWLIST)
for column in item[1]
)
for item in plan
):
raise RuntimeError("sample plan is not permitted")
for relation, columns in plan:
rows = reader.sample_relation_rows(relation, columns, 5)
if not isinstance(rows, list) or any(not isinstance(row, dict) for row in rows):
raise RuntimeError("sample evidence is malformed")
samples[relation] = _safe_sample_rows(rows)
return samples
def _blocked(
blocker: str,
*,
connection_attempts: int = 0,
other_databases_accessed: int | str = 0,
) -> dict[str, Any]:
return {
"status": "MISSION_00_BLOCKED",
"blocker": blocker,
"application_writes": 0,
"other_databases_accessed": other_databases_accessed,
"connection_attempts": connection_attempts,
}
def _valid_metadata(metadata: Any) -> bool:
if not isinstance(metadata, dict):
return False
if not all(isinstance(metadata.get(name), list) for name in ("relations", "fields", "errors", "unavailable")):
return False
return (
all(isinstance(row, dict) for name in ("relations", "fields", "errors") for row in metadata[name])
and all(isinstance(name, str) and bool(name) for name in metadata["unavailable"])
)
def _valid_mapping(mapping: Any) -> bool:
required = {"product", "fields", "fiscal", "stock", "media", "incremental", "triggers"}
if type(mapping) is not dict or set(mapping) != required:
return False
allowed = {"CONFIRMED", "INFERRED", "UNKNOWN"}
def strings(value: Any) -> bool:
return type(value) is list and all(type(item) is str and bool(item) for item in value)
def classified(value: Any, keys: set[str]) -> bool:
return type(value) is dict and set(value) == keys and value["classification"] in allowed
def field_record(value: Any) -> bool:
if type(value) is not dict or not {"classification", "sample_present", "type"} <= set(value):
return False
if set(value) - {"classification", "sample_present", "type", "subtype", "value"}:
return False
return value["classification"] in allowed and type(value["sample_present"]) is bool and type(value["type"]) is str and bool(value["type"]) and all(
type(value[key]) in (str, int, float, bool) for key in ("subtype", "value") if key in value
)
def field_map(value: Any) -> bool:
return type(value) is dict and all(type(key) is str and bool(key) and field_record(item) for key, item in value.items())
def relation_record(value: Any) -> bool:
if type(value) is not dict or not {"classification", "fields", "relationships"} <= set(value):
return False
if set(value) - {"classification", "fields", "relationships", "evidence"}:
return False
relationships = value["relationships"]
return value["classification"] in allowed and field_map(value["fields"]) and type(relationships) is list and all(
type(item) is dict and set(item) == {"child_relation", "child_field", "referenced_relation", "referenced_field"}
and all(type(item[key]) is str and bool(item[key]) for key in item)
for item in relationships
) and ("evidence" not in value or type(value["evidence"]) is str)
def relation_map(value: Any) -> bool:
return type(value) is dict and all(type(key) is str and bool(key) and relation_record(item) for key, item in value.items())
product = mapping["product"]
product_keys = {"classification", "relation", "evidence", "candidates"}
candidate_keys = {"classification", "relation", "evidence"}
evidence_keys = {"primary_key", "field_count", "semantic_fields", "sample_fields", "semantic_sample_fields", "approximate_count"}
if not classified(product, product_keys) or (product["relation"] is not None and (type(product["relation"]) is not str or not product["relation"])) or type(product["evidence"]) is not dict or set(product["evidence"]) - evidence_keys:
return False
candidates = product.get("candidates")
if not isinstance(candidates, list) or not all(
classified(item, candidate_keys) and type(item["relation"]) is str and bool(item["relation"])
and type(item["evidence"]) is dict and set(item["evidence"]) - evidence_keys == set()
and strings(item["evidence"].get("primary_key"))
and type(item["evidence"].get("field_count")) is int
and strings(item["evidence"].get("semantic_fields")) for item in candidates
):
return False
for evidence in (product["evidence"], *(item["evidence"] for item in candidates)):
if not evidence:
continue
if not strings(evidence.get("primary_key")) or type(evidence.get("field_count")) is not int or evidence["field_count"] < 0 or not strings(evidence.get("semantic_fields")):
return False
if "sample_fields" in evidence and not strings(evidence["sample_fields"]):
return False
if "semantic_sample_fields" in evidence and not strings(evidence["semantic_sample_fields"]):
return False
if "approximate_count" in evidence and type(evidence["approximate_count"]) not in (str, int, float, bool, type(None)):
return False
if not field_map(mapping["fields"]):
return False
for section in ("fiscal", "media"):
value = mapping[section]
if not classified(value, {"classification", "fields", "relations"}) or not field_map(value["fields"]) or not relation_map(value["relations"]):
return False
if not classified(mapping["stock"], {"classification", "relations"}) or not relation_map(mapping["stock"]["relations"]):
return False
incremental = mapping["incremental"]
evidence = incremental.get("evidence")
if not classified(incremental, {"classification", "evidence"}) or type(evidence) is not dict or set(evidence) != {"timestamps", "triggers", "generators"}:
return False
if not strings(evidence["timestamps"]) or type(evidence["triggers"]) is not int or evidence["triggers"] < 0 or type(evidence["generators"]) is not int or evidence["generators"] < 0:
return False
triggers = mapping["triggers"]
return type(triggers) is list and all(
classified(trigger, {"classification", "relation", "trigger", "timing", "event", "trigger_type", "dependencies", "source"})
and all(type(trigger[key]) is str and bool(trigger[key]) for key in ("relation", "trigger", "timing", "event", "source"))
and type(trigger["trigger_type"]) in (str, int) and bool(str(trigger["trigger_type"]))
and strings(trigger.get("dependencies"))
for trigger in triggers
)
def _valid_fingerprint(value: Any) -> bool:
return isinstance(value, str) and bool(re.fullmatch(r"[0-9a-f]{64}", value))
def _public_compatibility(evidence: Any) -> dict[str, str]:
"""Expose compatibility categories only; never serialize internal evidence."""
if not isinstance(evidence, dict):
return {"status": "UNKNOWN"}
return {
"status": evidence.get("status", "UNKNOWN") if evidence.get("status") in {"VALID", "UNKNOWN"} else "UNKNOWN",
"architecture": "MATCH" if evidence.get("compatibility", {}).get("python_client_architecture_match") is True else "UNKNOWN",
"fdb_client": "MATCH" if evidence.get("compatibility", {}).get("fdb_client_version_match") is True else "UNKNOWN",
"read_only_api": "PROVEN" if evidence.get("compatibility", {}).get("read_only_api") is True else "UNKNOWN",
}
def _public_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
"""Remove database identity rows, which may contain an absolute FDB path."""
return {key: value for key, value in metadata.items() if key != "database"}
def run_discovery(path: os.PathLike[str] | str, *, driver: Any) -> dict[str, Any]:
"""Run the gated, structured, sanitized discovery workflow."""
try:
security = _security_gate(path, driver)
client = security.get("client", security)
except RuntimeError as error:
return _blocked(_blocker_category(error))
try:
connection = connect_authorized_read_only(
path, driver, client_library=str(CLIENT_LIBRARY)
)
except Exception as error:
return _blocked(_blocker_category(error), connection_attempts=1)
try:
try:
database_name = get_database_identity(connection)
validate_authorized_database_identity(database_name, path)
except Exception:
return _blocked(
"identity_mismatch",
connection_attempts=1,
other_databases_accessed="UNKNOWN",
)
try:
facts = driver.facts() if callable(getattr(driver, "facts", None)) else {}
metadata = collect_metadata(connection)
if not _valid_metadata(metadata):
return _blocked("discovery_invalid", connection_attempts=1)
samples = _collect_samples(connection, metadata)
mapping = build_schema_mapping(metadata, samples)
if not _valid_mapping(mapping):
return _blocked("discovery_invalid", connection_attempts=1)
unavailable = set(metadata.get("unavailable", []))
optional_catalog_gaps = {"events", "packages", "procedure_parameters"}
blocking_errors = [
error for error in metadata.get("errors", [])
if not (
error.get("kind") == "optional_unsupported"
or error.get("query") == "monitoring"
)
]
complete = (
not blocking_errors
and unavailable <= optional_catalog_gaps
and (not metadata.get("incomplete") or bool(unavailable) and unavailable <= optional_catalog_gaps)
and bool(metadata.get("relations"))
and bool(samples)
)
fingerprint = None
classification = SCHEMA_UNKNOWN
if complete:
evidence = {"metadata": metadata, "samples": samples, "mapping": mapping}
fingerprint = build_fingerprint(evidence)
if not _valid_fingerprint(fingerprint):
return _blocked("discovery_invalid", connection_attempts=1)
expected = getattr(driver, "expected_fingerprints", ())
classification = classify_schema(
metadata,
expected,
mapping_ambiguous=mapping.get("product", {}).get("classification") == "UNKNOWN",
)
if classification not in {SCHEMA_SUPPORTED, SCHEMA_CHANGED, SCHEMA_UNKNOWN}:
return _blocked("discovery_invalid", connection_attempts=1)
return {
"status": "CONNECTED" if complete else SCHEMA_UNKNOWN,
"driver_version": str(getattr(driver, "version", "unknown")),
"architecture": platform.architecture()[0],
"engine_version": facts.get("engine_version"),
"dialect": facts.get("database_sql_dialect"),
"ods": {
"major": facts.get("ods_version"),
"minor": facts.get("ods_minor_version"),
},
"charset": facts.get("charset"),
"page_size": facts.get("page_size"),
"metadata": _public_metadata(metadata),
"samples": samples,
"schema_mapping": mapping,
"fingerprint": fingerprint,
"schema_classification": classification,
"compatibility": _public_compatibility(security.get("compatibility_evidence")),
"application_writes": 0,
"other_databases_accessed": 0,
"connection_attempts": 1,
}
except Exception:
return _blocked("discovery_failed", connection_attempts=1)
finally:
connection.close()
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.parse_args(argv)
try:
if not os.environ.get("FB_USER") or not os.environ.get("FB_PASSWORD"):
raise RuntimeError("credentials unavailable")
compatibility = _validate_preimport_compatibility()
import fdb
provenance = validate_imported_fdb_provenance(fdb)
runtime_compatibility = validate_firebird_runtime(fdb_module=fdb)
if runtime_compatibility["status"] != "VALID":
raise RuntimeError("Firebird driver/client compatibility is unproven")
result = run_discovery(DEFAULT_TARGET, driver=_FdbDriver(fdb))
result["compatibility"] = _public_compatibility(runtime_compatibility)
except Exception as error:
result = {
"status": "MISSION_00_BLOCKED",
"blocker": _blocker_category(error),
"application_writes": 0,
"other_databases_accessed": 0,
"connection_attempts": 0,
}
print(json.dumps(result, ensure_ascii=True, sort_keys=True))
return 0 if result["status"] == "CONNECTED" else 2
if __name__ == "__main__":
sys.exit(main())
+113
View File
@@ -0,0 +1,113 @@
"""Run the MVP's first real, guarded Firebird write (test DB only) and verify it.
Loads a single write_eligible IPI proposal from a DRY_RUN_PLAN artifact,
re-reads the live row to check for drift, performs the guarded UPDATE via
firebird_write_executor, verifies persistence, and writes an audit log with
no secrets. Only ever targets TESTESOFICIAIS.FDB (enforced by the executor).
"""
import argparse
import importlib.util
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def load_executor():
spec = importlib.util.spec_from_file_location("firebird_write_executor", ROOT / "scripts" / "firebird_write_executor.py")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def load_dotenv(path: Path) -> None:
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
os.environ[key.strip()] = value.strip()
def pick_proposal(plan: dict, product_code: str | None) -> dict:
eligible = [p for p in plan["field_proposals"] if p["field"] == "IPI" and p["write_eligible"]]
if not eligible:
raise SystemExit("no write_eligible IPI proposal available")
if product_code:
chosen = next((p for p in eligible if p["product_code"] == product_code), None)
if chosen is None:
raise SystemExit(f"product {product_code} is not write_eligible")
return chosen
return eligible[0]
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--plan", default=str(ROOT / "docs" / "DRY_RUN_PLAN_V3.json"))
parser.add_argument("--product-code", default=None)
parser.add_argument("--audit-log", default=str(ROOT / "docs" / "runs" / "first_write_audit.json"))
args = parser.parse_args()
load_dotenv(ROOT / ".env")
executor = load_executor()
plan = json.loads(Path(args.plan).read_text(encoding="utf-8"))
proposal = pick_proposal(plan, args.product_code)
import fdb
database_path = os.environ["FIREBIRD_DATABASE_PATH"]
executor.assert_authorized_write_path(database_path)
connection = fdb.connect(
database=database_path,
user=os.environ["FB_USER"],
password=os.environ["FB_PASSWORD"],
fb_library_name=os.environ.get("FIREBIRD_CLIENT_LIBRARY"),
)
try:
cursor = connection.cursor()
cursor.execute(
"SELECT CODIGO, CLASSIFICACAO_FISCAL, IPI FROM C000025 WHERE CODIGO = ?",
(proposal["product_code"],),
)
row = cursor.fetchone()
if row is None:
raise SystemExit(f"product {proposal['product_code']} not found")
codigo, ncm, live_ipi = row
if live_ipi != proposal["current_value"]:
raise SystemExit(
f"drift before write: plan expected {proposal['current_value']!r}, "
f"live is {live_ipi!r} — refusing to write, choose another eligible product"
)
cursor.close()
now = datetime.now(timezone.utc).isoformat()
record = executor.execute_guarded_update(
connection,
database_path=database_path,
proposal=proposal,
now=now,
)
audit = {
**record.as_dict(),
"ncm": ncm,
"database": str(executor.AUTHORIZED_WRITE_DATABASE_PATH),
"table": "C000025",
}
audit_path = Path(args.audit_log)
audit_path.parent.mkdir(parents=True, exist_ok=True)
existing = json.loads(audit_path.read_text(encoding="utf-8")) if audit_path.exists() else []
existing.append(audit)
audit_path.write_text(json.dumps(existing, indent=2, ensure_ascii=False), encoding="utf-8")
print(json.dumps(audit, indent=2, ensure_ascii=False))
finally:
connection.close()
if __name__ == "__main__":
main()
+221
View File
@@ -0,0 +1,221 @@
"""Fiscal Decision Engine V1 runner.
Reads all C000025 products (read-only), classifies via the Hub, applies
central.app.fiscal_decision per domain, writes the matrix/coverage/priorities
artifacts, and with --apply executes ONLY AUTO_APPLY_SAFE proposals against
TESTESOFICIAIS.FDB (path guard enforced by firebird_write_executor), followed
by an independent re-read.
"""
import argparse
import importlib.util
import json
import os
import sys
import urllib.request
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "agent" / "src"))
from central.app.fiscal_decision import DECISION_ENGINE_VERSION, ERP_FIELD_MAP, NATURE, decide_product # noqa: E402
from gti_fiscal_agent.firebird_reader import _connect, parse_company_context # noqa: E402
DOMAIN_FIELDS = ("NCM", "IPI", "EX_TIPI", "CEST", "MVA", "CFOP", "CSOSN", "CST_ICMS", "ORIGEM", "PIS_CST", "COFINS_CST",
"PIS_COFINS_RATE", "FCP", "RTC_CCLASSTRIB", "RTC_CBS_RATE", "RTC_CCREDPRES")
NEXT_ACTION = {
"NCM": "Review LEGACY/INVALID NCMs manually (no unambiguous successor); never auto-change.",
"IPI": "Keep auto-apply; NON_NUMERIC/blank TIPI rows need a business decision on how ERP represents NT.",
"EX_TIPI": "Propose only when a single concrete EX applies; needs product-description evidence.",
"CEST": "Improve description evidence per candidate group (do not lower threshold); manual review queue for candidates.",
"MVA": "Confirm CEST first; then identify the ERP destination field for MVA (none proven).",
"CFOP": "Requires operation context (direction, destination UF, ST, purpose) — capture it at document level.",
"CSOSN": "Requires sale scenario/ST retention context; consider per-product default review for ST-annex NCMs.",
"CST_ICMS": "Not used under CRT=1; only relevant if the client changes regime.",
"ORIGEM": "Obtain objective product origin (supplier/import flag) before any write; ERP CST prefix is not proof.",
"PIS_CST": "Needs regime+incidence rule table (deferred 4.3.x) — measure need from client data first.",
"COFINS_CST": "Same as PIS_CST.",
"PIS_COFINS_RATE": "Needs source tables (deferred); never assume 0%.",
"FCP": "Needs destination UF + operation + BA FCP item list per NCM.",
"RTC_CCLASSTRIB": "Needs official NCM->annex/cClassTrib mapping (LC 214 annexes) — not in acquired datasets.",
"RTC_CBS_RATE": "Blocked by cClassTrib; apply 0.009 only where the official rule applies.",
"RTC_CCREDPRES": "Official conditions not derivable from product data.",
}
def load_dotenv(path):
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, _, v = line.partition("=")
os.environ.setdefault(k.strip(), v.strip())
def http(url, token, payload=None):
data = json.dumps(payload, ensure_ascii=True).encode() if payload is not None else None
req = urllib.request.Request(url, data=data, headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, method="POST" if payload is not None else "GET")
with urllib.request.urlopen(req, timeout=600) as r:
return json.load(r)
def read_all(env):
conn = _connect(env)
try:
cur = conn.cursor()
cur.execute("""SELECT CODIGO,PRODUTO,CLASSIFICACAO_FISCAL,CFOP,CST,CSOSN,CEST,IPI,EX_TIPI,ORIGEM,COD_SAIDA_PIS,COD_SAIDA_COFINS,ALIQ_PIS FROM C000025 ORDER BY CODIGO""")
names = [c[0].strip() for c in cur.description]
rows = [dict(zip(names, r)) for r in cur.fetchall()]
cur.execute("SELECT UF, CRT FROM C000004")
return rows, parse_company_context(cur.fetchall())
finally:
conn.close()
def build_refs(hub, token, products, company_uf):
refs = {}
for key, dom in (("csosn", "CSOSN"), ("cst_icms", "CST_ICMS"), ("cst_pis", "CST_PIS"), ("cst_cofins", "CST_COFINS"), ("origem", "ORIGEM_MERCADORIA")):
refs[key] = set(http(f"{hub}/v1/reference/fiscal_codes/{dom}", token)["codes"])
valid = set()
for code in {str(p["CFOP"]).strip() for p in products if p.get("CFOP")}:
if http(f"{hub}/v1/reference/cfop/{code}", token)["found"]:
valid.add(code)
refs["cfop_valid"] = valid
refs["fcp_uf"] = {}
r = http(f"{hub}/v1/reference/fcp/{company_uf}", token)
if r["found"]:
refs["fcp_uf"][company_uf] = r
refs["cbs_rate_2026"] = http(f"{hub}/v1/reference/rtc_cbs_rate/2026", token).get("rate")
status = http(f"{hub}/v1/datasets/status", token)
refs["cclasstrib_count"] = sum(d["record_count"] for d in status if d["kind"] == "RTC_CCLASSTRIB" and d["status"] == "ACTIVE")
return refs
def load_executor():
spec = importlib.util.spec_from_file_location("firebird_write_executor", ROOT / "scripts" / "firebird_write_executor.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def summarize(matrix):
cov = {}
for dom in DOMAIN_FIELDS:
rows = [m["fields"][dom] for m in matrix]
c = Counter(r["status"] for r in rows)
act = Counter(r["action_class"] for r in rows)
cov[dom] = {
"nature": NATURE[dom],
"DATA_AVAILABLE": sum(r["data_available"] for r in rows),
"MATCH_FOUND": sum(r["match_found"] for r in rows),
"RULE_RESOLVED": c["RULE_RESOLVED"], "RULE_CANDIDATE": c["RULE_CANDIDATE"], "RULE_UNRESOLVED": c["RULE_UNRESOLVED"],
"OPERATION_CONTEXT_REQUIRED": c["OPERATION_CONTEXT_REQUIRED"],
"CHANGE_REQUIRED": sum(1 for r in rows if r["write_eligible"]),
"action_class": dict(act),
"reason_codes": dict(Counter(r["reason_code"] for r in rows).most_common(6)),
}
return cov
def priorities_md(cov):
def key(d):
c = cov[d]
return (-c["action_class"].get("AUTO_APPLY_SAFE", 0), -c["CHANGE_REQUIRED"], -c["RULE_RESOLVED"], -c["RULE_CANDIDATE"], d)
lines = ["# NEXT_FISCAL_AUTOMATION_PRIORITIES_V1", "",
"Generated by `scripts/run_fiscal_decision_v1.py` from `docs/FISCAL_DECISION_COVERAGE_V1.json` (521 products).",
"Ordering is mechanical: AUTO_APPLY_SAFE desc, CHANGE_REQUIRED desc, RESOLVED_PRODUCTS desc, candidates desc.", "",
"| FIELD | RESOLVED_PRODUCTS | CHANGE_REQUIRED | AUTO_APPLY_SAFE | CANDIDATES | OP_CONTEXT_REQUIRED | BLOCKER | NEXT_ACTION |", "|---|---|---|---|---|---|---|---|"]
for d in sorted(cov, key=key):
c = cov[d]
blocker = next(iter(c["reason_codes"]), "-")
lines.append(f"| {d} | {c['RULE_RESOLVED']} | {c['CHANGE_REQUIRED']} | {c['action_class'].get('AUTO_APPLY_SAFE', 0)} | {c['RULE_CANDIDATE']} | {c['OPERATION_CONTEXT_REQUIRED']} | {blocker} | {NEXT_ACTION[d]} |")
return "\n".join(lines) + "\n"
def apply_writes(matrix, products_by_code, executor):
"""Only AUTO_APPLY_SAFE + write_eligible; executor enforces path/allowlist/compare-before-write."""
import fdb
database_path = os.environ["FIREBIRD_DATABASE_PATH"]
executor.assert_authorized_write_path(database_path)
per_field = defaultdict(lambda: Counter())
audit = []
conn = fdb.connect(database=database_path, user=os.environ["FB_USER"], password=os.environ["FB_PASSWORD"], fb_library_name=os.environ.get("FIREBIRD_CLIENT_LIBRARY"))
try:
for m in matrix:
for dom, rec in m["fields"].items():
if not (rec["write_eligible"] and rec["action_class"] == "AUTO_APPLY_SAFE"):
continue
field = {"IPI": "IPI", "EX_TIPI": "EX_TIPI", "CEST": "CEST"}[dom]
per_field[field]["ATTEMPTED"] += 1
proposal = {"product_code": m["CODIGO"], "field": field, "current_value": rec["current_value"], "proposed_value": rec["proposed_value"],
"status": "CHANGE_REQUIRED", "write_eligible": True, "source_dataset": rec["provenance"]["dataset"], "source_version": rec["provenance"]["version"]}
try:
r = executor.execute_guarded_update(conn, database_path=database_path, proposal=proposal, now=datetime.now(timezone.utc).isoformat())
per_field[field]["SUCCEEDED"] += 1
audit.append({**r.as_dict(), "outcome": "SUCCEEDED"})
except executor.WriteRefused as e:
per_field[field]["FAILED"] += 1
audit.append({"product_code": m["CODIGO"], "field": field, "proposed_value": rec["proposed_value"], "outcome": "FAILED", "error": str(e)})
finally:
conn.close()
ro = _connect(os.environ)
try:
cur = ro.cursor()
for a in audit:
if a["outcome"] != "SUCCEEDED":
continue
cur.execute(f"SELECT {a['field']} FROM C000025 WHERE CODIGO = ?", (a["product_code"],))
live = cur.fetchone()[0]
if str(live).strip() == str(a["proposed_value"]).strip():
per_field[a["field"]]["POST_WRITE_VERIFIED"] += 1
finally:
ro.close()
return {f: dict(c) for f, c in per_field.items()}, audit
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--token", required=True)
ap.add_argument("--hub-url", default="http://localhost:8000")
ap.add_argument("--apply", action="store_true")
args = ap.parse_args()
load_dotenv(ROOT / ".env")
products, company = read_all(os.environ)
refs = build_refs(args.hub_url, args.token, products, company['uf'])
decisions = []
for i in range(0, len(products), 100):
chunk = products[i:i + 100]
decisions += http(f"{args.hub_url}/v1/classify/batch", args.token, {"products": [{"current_ncm": p["CLASSIFICACAO_FISCAL"], "description": p["PRODUTO"] or ""} for p in chunk]})["results"]
matrix = []
for p, d in zip(products, decisions):
matrix.append({"CODIGO": p["CODIGO"], "DESCRIPTION": p["PRODUTO"], "NCM": p["CLASSIFICACAO_FISCAL"], "fields": decide_product(p, d, company, refs)})
cov = summarize(matrix)
totals = Counter()
for m in matrix:
for rec in m["fields"].values():
totals[rec["action_class"]] += 1
(ROOT / "docs" / "FISCAL_DECISION_MATRIX_V1.json").write_text(json.dumps({
"artifact": "FISCAL_DECISION_MATRIX_V1", "engine": DECISION_ENGINE_VERSION, "company_context": company,
"total_products": len(matrix), "erp_field_map": ERP_FIELD_MAP, "products": matrix}, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
coverage = {"artifact": "FISCAL_DECISION_COVERAGE_V1", "engine": DECISION_ENGINE_VERSION, "total_products": len(matrix), "company_context": company,
"erp_fields_mapped": sum(1 for f in ERP_FIELD_MAP if f["MAPPING"] == "CONFIRMED"),
"erp_fields_unconfirmed": [f["ERP_FIELD"] for f in ERP_FIELD_MAP if f["MAPPING"] != "CONFIRMED"],
"action_class_totals": dict(totals), "domains": cov, "reference_cbs_rate_2026": refs["cbs_rate_2026"]}
write_result = None
if args.apply:
write_result, audit = apply_writes(matrix, {p["CODIGO"]: p for p in products}, load_executor())
coverage["writes"] = write_result
(ROOT / "docs" / "runs").mkdir(exist_ok=True)
(ROOT / "docs" / "runs" / "fiscal_decision_v1_write_audit.json").write_text(json.dumps(audit, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
(ROOT / "docs" / "FISCAL_DECISION_COVERAGE_V1.json").write_text(json.dumps(coverage, ensure_ascii=False, indent=1), encoding="utf-8")
(ROOT / "docs" / "NEXT_FISCAL_AUTOMATION_PRIORITIES_V1.md").write_text(priorities_md(cov), encoding="utf-8")
print(json.dumps({"action_class_totals": dict(totals), "writes": write_result,
"domains": {d: {k: v for k, v in c.items() if k in ("RULE_RESOLVED", "RULE_CANDIDATE", "RULE_UNRESOLVED", "OPERATION_CONTEXT_REQUIRED", "CHANGE_REQUIRED", "MATCH_FOUND")} for d, c in cov.items()}}, indent=1))
if __name__ == "__main__":
main()
+262
View File
@@ -0,0 +1,262 @@
"""MVP first real batch: classify all of C000025, dry-run the IPI plan, then
execute every write_eligible IPI proposal against TESTESOFICIAIS.FDB only.
Phases:
1. Read all products from C000025 (read-only).
2. Classify each via the running fiscal-hub /v1/classify/batch.
3. Build field proposals (dry-run) with central.app.dry_run_planner.
4. Execute every write_eligible IPI proposal via firebird_write_executor,
sequentially, with compare-before-write and rowcount==1 per product.
5. Re-read every written CODIGO independently and verify persistence.
Never touches CEST/EX_TIPI writes. Never touches any database other than
TESTESOFICIAIS.FDB (enforced by firebird_write_executor's path guard).
"""
import argparse
import importlib.util
import json
import os
import sys
import urllib.request
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "central"))
from app.dry_run_planner import build_field_proposals, split_by_eligibility # noqa: E402
def load_dotenv(path: Path) -> None:
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
os.environ[key.strip()] = value.strip()
def load_executor():
spec = importlib.util.spec_from_file_location("firebird_write_executor", ROOT / "scripts" / "firebird_write_executor.py")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def read_all_products(env):
import fdb
conn = fdb.connect(
database=env["FIREBIRD_DATABASE_PATH"],
user=env["FB_USER"],
password=env["FB_PASSWORD"],
fb_library_name=env.get("FIREBIRD_CLIENT_LIBRARY"),
isolation_level=fdb.ISOLATION_LEVEL_READ_COMMITED_RO,
)
try:
cur = conn.cursor()
cur.execute("SELECT CODIGO, PRODUTO, CLASSIFICACAO_FISCAL, IPI, EX_TIPI, CEST FROM C000025 ORDER BY CODIGO")
names = [c[0].strip() for c in cur.description]
return [dict(zip(names, row)) for row in cur.fetchall()]
finally:
conn.close()
def classify_batch(hub_url, token, products, chunk_size=100):
results = []
for start in range(0, len(products), chunk_size):
chunk = products[start:start + chunk_size]
payload = {"products": [{"current_ncm": p["CLASSIFICACAO_FISCAL"], "description": p["PRODUTO"] or ""} for p in chunk]}
request = urllib.request.Request(
f"{hub_url}/v1/classify/batch",
data=json.dumps(payload, ensure_ascii=True).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
method="POST",
)
with urllib.request.urlopen(request, timeout=60) as response:
results.extend(json.load(response)["results"])
return results
def classify_ipi_bucket(decision):
ipi_status = decision.get("ipi_status")
if ipi_status == "LEGACY_NCM":
return None # tracked under NCM_LEGACY, not an IPI bucket
if ipi_status == "NO_TIPI_MAPPING_FOUND":
return "NO_MATCH"
if ipi_status == "IPI_CANDIDATES":
return "AMBIGUOUS"
if ipi_status == "IPI_RESOLVED":
rate_kind = (decision.get("classification", {}).get("ipi") or {}).get("rate_kind")
return {"NUMERIC": "NUMERIC", "NON_NUMERIC": "NON_NUMERIC", "BLANK": "SOURCE_BLANK"}.get(rate_kind, "NO_MATCH")
return "NO_MATCH"
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--hub-url", default="http://localhost:8000")
parser.add_argument("--token", required=True)
parser.add_argument("--dry-run-output", default=str(ROOT / "docs" / "FULL_DATABASE_DRY_RUN_V1.json"))
parser.add_argument("--audit-output", default=str(ROOT / "docs" / "runs" / "full_database_ipi_write_audit_v1.json"))
parser.add_argument("--no-write", action="store_true", help="Only produce the dry-run artifact; skip Phase 4/5.")
args = parser.parse_args()
load_dotenv(ROOT / ".env")
executor = load_executor()
database_path = os.environ["FIREBIRD_DATABASE_PATH"]
executor.assert_authorized_write_path(database_path)
products = read_all_products(os.environ)
total = len(products)
ncm_missing = sum(1 for p in products if not (p["CLASSIFICACAO_FISCAL"] or "").strip())
decisions = classify_batch(args.hub_url, args.token, products)
ncm_status_counts = Counter(d.get("status") for d in decisions)
ncm_valid = ncm_status_counts.get("EXACT_NCM", 0)
ncm_legacy = ncm_status_counts.get("LEGACY_NCM", 0)
# invalid = has a CLASSIFICACAO_FISCAL but it isn't a recognized/legacy NCM
ncm_invalid = sum(
1 for p, d in zip(products, decisions)
if (p["CLASSIFICACAO_FISCAL"] or "").strip() and d.get("status") not in ("EXACT_NCM", "LEGACY_NCM")
)
ipi_buckets = Counter()
field_proposals = []
for product, decision in zip(products, decisions):
code = product["CODIGO"]
current = {"ipi": product.get("IPI"), "cest": product.get("CEST"), "ex_tipi": product.get("EX_TIPI")}
field_proposals.extend(build_field_proposals(code, decision, current))
bucket = classify_ipi_bucket(decision)
if bucket:
ipi_buckets[bucket] += 1
ipi_rows = [p for p in field_proposals if p["field"] == "IPI"]
cest_rows = [p for p in field_proposals if p["field"] == "CEST"]
split = split_by_eligibility(ipi_rows)
ipi_already_correct = sum(1 for p in ipi_rows if p["status"] == "SAME_VALUE")
ipi_change_required = sum(1 for p in ipi_rows if p["status"] == "CHANGE_REQUIRED")
ipi_write_eligible_rows = split["eligible"]
cest_db_match = sum(1 for p in cest_rows if p["status"] not in ("UNRESOLVED",) or p["reason_code"] != "NO_CEST_MAPPING_FOUND_FOR_NCM")
cest_confirmed = sum(1 for p in cest_rows if p["status"] in ("SAME_VALUE", "CHANGE_REQUIRED", "CURRENT_UNKNOWN"))
dry_run_artifact = {
"artifact": "FULL_DATABASE_DRY_RUN_V1",
"total_products": total,
"ncm_valid": ncm_valid,
"ncm_legacy": ncm_legacy,
"ncm_invalid": ncm_invalid,
"ncm_missing": ncm_missing,
"ipi_buckets": dict(ipi_buckets),
"ipi_already_correct": ipi_already_correct,
"ipi_change_required": ipi_change_required,
"ipi_write_eligible": len(ipi_write_eligible_rows),
"cest_db_match_products": cest_db_match,
"cest_confirmed": cest_confirmed,
"field_proposals": field_proposals,
}
Path(args.dry_run_output).parent.mkdir(parents=True, exist_ok=True)
Path(args.dry_run_output).write_text(json.dumps(dry_run_artifact, indent=2, ensure_ascii=False), encoding="utf-8")
summary = {
"total_products": total,
"ncm_valid": ncm_valid,
"ncm_legacy": ncm_legacy,
"ncm_invalid": ncm_invalid,
"ncm_missing": ncm_missing,
"ipi_buckets": dict(ipi_buckets),
"ipi_already_correct": ipi_already_correct,
"ipi_change_required": ipi_change_required,
"ipi_write_eligible": len(ipi_write_eligible_rows),
}
if args.no_write or not ipi_write_eligible_rows:
summary["writes_attempted"] = 0
print(json.dumps(summary, indent=2))
return
import fdb
connection = fdb.connect(
database=database_path,
user=os.environ["FB_USER"],
password=os.environ["FB_PASSWORD"],
fb_library_name=os.environ.get("FIREBIRD_CLIENT_LIBRARY"),
)
audit_records = []
succeeded = failed = 0
try:
for proposal in ipi_write_eligible_rows:
now = datetime.now(timezone.utc).isoformat()
try:
record = executor.execute_guarded_update(
connection,
database_path=database_path,
proposal=proposal,
now=now,
)
audit_records.append({**record.as_dict(), "outcome": "SUCCEEDED"})
succeeded += 1
except executor.WriteRefused as error:
audit_records.append({
"product_code": proposal["product_code"],
"field": proposal["field"],
"before_value": proposal["current_value"],
"proposed_value": proposal["proposed_value"],
"after_value": None,
"rowcount": 0,
"source_dataset": proposal.get("source_dataset"),
"source_version": proposal.get("source_version"),
"timestamp": now,
"outcome": "FAILED",
"error": str(error),
})
failed += 1
finally:
connection.close()
Path(args.audit_output).parent.mkdir(parents=True, exist_ok=True)
Path(args.audit_output).write_text(json.dumps(audit_records, indent=2, ensure_ascii=False), encoding="utf-8")
# Phase 5: independent re-read + verification of every written CODIGO.
written_codes = [r["product_code"] for r in audit_records if r["outcome"] == "SUCCEEDED"]
verify_conn = fdb.connect(
database=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,
)
verified = 0
mismatches = []
try:
cur = verify_conn.cursor()
for record in audit_records:
if record["outcome"] != "SUCCEEDED":
continue
cur.execute("SELECT IPI FROM C000025 WHERE CODIGO = ?", (record["product_code"],))
row = cur.fetchone()
live_value = row[0] if row else None
if live_value == record["proposed_value"]:
verified += 1
else:
mismatches.append({"product_code": record["product_code"], "expected": record["proposed_value"], "found": live_value})
finally:
verify_conn.close()
summary.update({
"writes_attempted": len(ipi_write_eligible_rows),
"writes_succeeded": succeeded,
"writes_failed": failed,
"post_write_verified": verified,
"post_write_mismatches": mismatches,
"written_codes": written_codes,
})
print(json.dumps(summary, indent=2))
if __name__ == "__main__":
main()
+155
View File
@@ -0,0 +1,155 @@
"""ICMS Context Engine V1 runner (READ-ONLY): ST applicability, ST withholding, product role, CSOSN V2 dry-run.
Outputs: docs/ICMS_PRODUCT_CONTEXT_V1.json, ICMS_CONTEXT_COVERAGE_V1.json, CSOSN_DRY_RUN_V2.json,
FISCAL_CONTEXT_REQUIREMENTS_V3.json, NEXT_FISCAL_FIELD_IMPLEMENTATION_V3.md. Never writes to Firebird.
"""
import json
import os
import subprocess
import sys
from collections import Counter, defaultdict
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
from central.app.icms_context import ICMS_CONTEXT_ENGINE_VERSION, csosn_v2, product_role, st_conflict_class, st_status, withheld_status # noqa: E402
from central.app.product_defaults import validate_against_history # noqa: E402
from gti_fiscal_agent.firebird_reader import _connect, parse_company_context # noqa: E402
s = lambda v: None if v is None else str(v).strip()
dg = lambda v: "".join(c for c in str(v or "") if c.isdigit())
def main():
token = sys.argv[1]; hub = "http://localhost:8000"
base.load_dotenv(ROOT / ".env")
cur = _connect(os.environ).cursor()
cur.execute("select CODIGO, PRODUTO, CLASSIFICACAO_FISCAL, CEST, CSOSN, CST, CFOP, TIPO, PROD_COZINHA, PRODUTO_SERVICO from C000025 order by CODIGO")
products = [dict(zip(("CODIGO", "PRODUTO", "CLASSIFICACAO_FISCAL", "CEST", "CSOSN", "CST", "CFOP", "TIPO", "PROD_COZINHA", "PRODUTO_SERVICO"), r)) for r in cur.fetchall()]
cur.execute("select ICMS_CSOSN from C000004"); company_default = s(cur.fetchone()[0])
cur.execute("select UF, CRT from C000004"); company = parse_company_context(cur.fetchall()); uf, crt = company["uf"], company["crt"]
cur.execute("select CODRECEITA from RECEITA_INGREDIENTES where CODRECEITA is not null"); recipes = {str(r[0]).zfill(6) for r in cur.fetchall()}
cur.execute("select distinct CODPRODUTO from C000032 where TIPO = 'COMPOSICAO'"); composed = {s(r[0]) for r in cur.fetchall()}
cur.execute("select CODPRODUTO, ICMS_RETIDO, ICMS_VALORRETIDO, SUB_VALOR, CFOP from C000088"); purchases = defaultdict(list)
for cod, ret, vr, sv, cf in cur.fetchall():
purchases[s(cod)].append({"icms_retido": s(ret), "valor_retido": vr, "sub_valor": sv, "cfop": s(cf)})
cur.execute("select CODPRODUTO, ULTIMA_ENTRADA, NOTA_FISCAL from C000100"); stock_entry = {s(r[0]): (r[1] is not None or (s(r[2]) not in (None, "", "000000"))) for r in cur.fetchall()}
cur.execute("select CODPRODUTO, CST, ICMS_SUB from C000062"); nf_out = defaultdict(list)
for cod, cst, sub in cur.fetchall(): nf_out[s(cod)].append((s(cst), float(sub or 0)))
cur.connection.close()
sh = defaultdict(list)
out = subprocess.run(["docker", "exec", "projeto-postgres-1", "psql", "-U", "gti", "-d", "gti_fiscal", "-At", "-F|", "-c",
"select e.item, n.ncm_sh from ba_st_mva_entries e join ba_st_mva_ncm_sh n on n.entry_id=e.id join datasets d on d.id=e.dataset_id where d.status='ACTIVE'"], capture_output=True, text=True, check=True).stdout
for line in out.splitlines():
it, n = line.split("|"); sh[it].append(dg(n))
refs = base.build_refs(hub, token, products, uf)
decisions = []
for i in range(0, len(products), 100):
chunk = products[i:i + 100]
decisions += base.http(f"{hub}/v1/classify/batch", token, {"products": [{"current_ncm": p["CLASSIFICACAO_FISCAL"], "description": p["PRODUTO"] or ""} for p in chunk]})["results"]
trusted = json.loads((ROOT / "docs" / "runs" / "trusted_historical_items.json").read_text(encoding="utf-8"))
hist = defaultdict(list)
for t in trusted:
hist[s(t["cod"])].append(s(t.get("csosn")) or "")
hist_cfop = defaultdict(list)
for t in trusted:
hist_cfop[s(t["cod"])].append(s(t.get("cfop")) or "")
rows = []
for p, d in zip(products, decisions):
code = s(p["CODIGO"]); cls = d.get("classification") or {}
ncm_status = d.get("status"); mva = d.get("ba_st_mva_status"); ceststat = d.get("cest_status")
ba = cls.get("ba_st_mva") or {}
ba_item, ba_cest = ba.get("item"), ba.get("cest")
ncm8 = dg(p["CLASSIFICACAO_FISCAL"])
sh_len = max((len(x) for x in sh.get(ba_item, []) if x and ncm8.startswith(x)), default=None) if ba_item else None
st, st_reason = st_status(ncm_status, mva, ceststat, p["CEST"], ba_cest, sh_len)
wh, wh_reason = withheld_status(purchases.get(code, []))
purchase_ev = bool(purchases.get(code)) or stock_entry.get(code, False)
role, role_ev = product_role(p["TIPO"], p["PROD_COZINHA"] == 1, code in recipes, code in composed, bool(s(p["PRODUTO_SERVICO"])), purchase_ev)
cs = csosn_v2(p["CSOSN"], company_default, ncm_status, st, wh, role, refs["csosn"])
h = hist.get(code, [])
hv = validate_against_history(cs["proposed_csosn"], [x for x in h if x])
share = (sum(1 for x in h if x == cs["current_value"]) / len(h)) if h else None
rows.append({
"CODIGO": code, "DESCRIPTION": p["PRODUTO"], "NCM": p["CLASSIFICACAO_FISCAL"], "CEST": s(p["CEST"]) or None,
"NCM_STATUS": ncm_status, "ST_STATUS": st, "ST_REASON": st_reason, "ST_WITHHELD_STATUS": wh, "ST_WITHHELD_REASON": wh_reason,
"PRODUCT_ROLE": role, "PRODUCT_ROLE_EVIDENCE": role_ev, "CURRENT_CSOSN": cs["current_value"], "COMPANY_CRT": crt, "COMPANY_DEFAULT_CSOSN": company_default,
"HISTORICAL_CSOSN": dict(Counter(x for x in h if x)), "HISTORICAL_CONSISTENCY": share,
"ERP_EMITTED_ST_TREATMENT_NF_OUT": {"cst060_items": sum(1 for c, _ in nf_out.get(code, []) if c == "060"), "icms_sub_items": sum(1 for _, v in nf_out.get(code, []) if v > 0)},
"CSOSN_V2": cs, "CSOSN_HISTORY_VALIDATION": hv,
"_conf": {"cest_status": ceststat, "mva": mva, "sh_len": sh_len, "erp_cest": p["CEST"], "ba_cest": ba_cest, "cfop": s(p["CFOP"]),
"hist_cfop_current": bool(hist_cfop.get(code)) and all(x == s(p["CFOP"]) for x in hist_cfop[code])},
})
# ST conflict audit (the CFOP-conflict population: ST-subject with non-ST CFOP, or 5405 without ST)
conflicts = []
for r in rows:
c = r["_conf"]
st_subject = c["mva"] == "MVA_RESOLVED"
cfop_conflict = r["NCM_STATUS"] == "EXACT_NCM" and ((st_subject and c["cfop"] == "5102") or (c["mva"] == "NO_MVA_MAPPING_FOUND" and c["cfop"] == "5405"))
if cfop_conflict:
primary, flags = st_conflict_class(c["cest_status"], c["mva"], c["sh_len"], c["erp_cest"], c["ba_cest"], c["cfop"], c["hist_cfop_current"], c["mva"] == "NO_MVA_MAPPING_FOUND" and c["cfop"] == "5405")
conflicts.append({"CODIGO": r["CODIGO"], "DESCRIPTION": r["DESCRIPTION"], "NCM": r["NCM"], "erp_cfop": c["cfop"], "primary": primary, "flags": flags, "st_status": r["ST_STATUS"]})
for r in rows:
r.pop("_conf")
C = lambda key: Counter(r[key] for r in rows)
st_c, wh_c, role_c = C("ST_STATUS"), C("ST_WITHHELD_STATUS"), C("PRODUCT_ROLE")
cs_c = Counter(r["CSOSN_V2"]["status"] for r in rows)
act_c = Counter(r["CSOSN_V2"]["action_class"] for r in rows)
hv_c = Counter(r["CSOSN_HISTORY_VALIDATION"] for r in rows if r["CSOSN_V2"]["proposed_csosn"] is not None)
ncm_blocked = sum(1 for r in rows if r["NCM_STATUS"] != "EXACT_NCM")
ncm_only_blocked = ncm_blocked # every NCM-invalid product is UNRESOLVED solely because of NCM (no other context is evaluated)
already = sum(1 for r in rows if r["CSOSN_V2"]["status"] == "CSOSN_RESOLVED" and r["CSOSN_V2"]["proposed_csosn"] == r["CURRENT_CSOSN"])
change = sum(1 for r in rows if r["CSOSN_V2"]["status"] == "CSOSN_RESOLVED" and r["CSOSN_V2"]["proposed_csosn"] != r["CURRENT_CSOSN"])
auto_safe = 0 # none may be write-eligible in this mission; readiness below
ready = sum(1 for r in rows if r["CSOSN_V2"]["status"] == "CSOSN_RESOLVED" and r["CSOSN_HISTORY_VALIDATION"] == "MATCH")
conf_primary = Counter(c["primary"] for c in conflicts)
conf_flags = Counter(f for c in conflicts for f in c["flags"])
cov = {"artifact": "ICMS_CONTEXT_COVERAGE_V1", "firebird_writes": 0, "TOTAL_PRODUCTS": len(rows), "engine": ICMS_CONTEXT_ENGINE_VERSION,
"ST": {k: st_c.get(k, 0) for k in ("ST_CONFIRMED", "ST_NOT_APPLICABLE", "ST_CANDIDATE", "ST_UNKNOWN")},
"ST_reasons": dict(C("ST_REASON")),
"ST_WITHHELD": {"ST_ALREADY_WITHHELD_CONFIRMED": wh_c.get("ST_ALREADY_WITHHELD_CONFIRMED", 0), "ST_ALREADY_WITHHELD_NOT_CONFIRMED": wh_c.get("ST_ALREADY_WITHHELD_NOT_CONFIRMED", 0), "ST_ALREADY_WITHHELD_UNKNOWN": wh_c.get("ST_WITHHOLDING_UNKNOWN", 0)},
"PRODUCT_ROLE": {k: role_c.get(k, 0) for k in ("RESALE", "OWN_PRODUCTION", "INPUT", "SERVICE", "UNKNOWN")},
"CSOSN": {"RESOLVED": cs_c.get("CSOSN_RESOLVED", 0), "ALREADY_CORRECT": already, "CHANGE_REQUIRED": change, "CANDIDATES": cs_c.get("CSOSN_CANDIDATE", 0),
"CONTEXT_REQUIRED": cs_c.get("CSOSN_CONTEXT_REQUIRED", 0), "CONFLICT": cs_c.get("CSOSN_CONFLICT", 0), "UNRESOLVED": cs_c.get("CSOSN_UNRESOLVED", 0),
"AUTO_APPLY_SAFE": auto_safe, "AUTO_APPLY_READY_IF_ENABLED": ready, "action_class": dict(act_c),
"reasons": dict(Counter(r["CSOSN_V2"]["reason_code"] for r in rows).most_common(10))},
"CSOSN_HISTORY_of_proposals": {"MATCH": hv_c.get("MATCH", 0), "MISMATCH": hv_c.get("MISMATCH", 0), "UNVALIDATED": hv_c.get("UNVALIDATED", 0) + sum(1 for r in rows if r["CSOSN_V2"]["proposed_csosn"] is None)},
"CSOSN_HISTORY_of_proposals_note": "MATCH/MISMATCH/UNVALIDATED counted on products WITH a proposed value; UNVALIDATED additionally counts products with no proposal (nothing to validate).",
"NCM_BLOCKED_PRODUCTS": ncm_blocked, "NCM_BLOCKED_ONLY_BY_NCM": ncm_only_blocked,
"ST_CONFLICT_AUDIT": {"population": len(conflicts), "primary_class": dict(conf_primary), "any_flag": dict(conf_flags)}}
(ROOT / "docs" / "ICMS_CONTEXT_COVERAGE_V1.json").write_text(json.dumps(cov, ensure_ascii=False, indent=1), encoding="utf-8")
(ROOT / "docs" / "ICMS_PRODUCT_CONTEXT_V1.json").write_text(json.dumps({"artifact": "ICMS_PRODUCT_CONTEXT_V1", "engine": ICMS_CONTEXT_ENGINE_VERSION, "company": {"crt": crt, "uf": uf, "default_csosn": company_default},
"products": rows, "st_conflict_audit": conflicts}, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
dry = {"artifact": "CSOSN_DRY_RUN_V2", "firebird_writes": 0, "CSOSN_AUTO_APPLY_SAFE": auto_safe, "CSOSN_ALREADY_CORRECT": already, "CSOSN_CHANGE_REQUIRED": change,
"would_change": [], "states": dict(cs_c), "note": "No CSOSN proposal is write-eligible; readiness requires proven ST, withholding, role and ICMS treatment."}
(ROOT / "docs" / "CSOSN_DRY_RUN_V2.json").write_text(json.dumps(dry, ensure_ascii=False, indent=1), encoding="utf-8")
n = lambda key, val: sum(1 for r in rows if r[key] == val)
req = [
{"INPUT": "valid_ncm", "SOURCE_EXPECTED": "product master", "AVAILABLE": "PARTIAL", "AFFECTED_PRODUCTS": ncm_blocked, "AFFECTED_FIELD": "CSOSN", "NEXT_ACTION": "Manual NCM review (3 legacy + 23 invalid); not auto-corrected."},
{"INPUT": "st_confirmed_per_product (CEST corroboration)", "SOURCE_EXPECTED": "ERP CEST field filled and equal to the BA Anexo 1 item CEST", "AVAILABLE": "PARTIAL", "AFFECTED_PRODUCTS": st_c.get("ST_CANDIDATE", 0) + st_c.get("ST_UNKNOWN", 0) - ncm_blocked, "AFFECTED_FIELD": "CSOSN", "NEXT_ACTION": "Fill/confirm CEST for ST candidates (description review) — 490 of 521 ERP CEST values are blank."},
{"INPUT": "st_withheld_by_supplier (purchase evidence)", "SOURCE_EXPECTED": "purchase items with ICMS_RETIDO/ST values (C000088)", "AVAILABLE": "NO (12 purchase items, all ICMS_RETIDO='N', ST values 0)", "AFFECTED_PRODUCTS": st_c.get("ST_CONFIRMED", 0) + st_c.get("ST_CANDIDATE", 0), "AFFECTED_FIELD": "CSOSN(500 vs other)", "NEXT_ACTION": "Import supplier NF-e XML (ICMS ST/CSOSN 500 on entrada) or obtain vendor confirmation of how retention is recorded."},
{"INPUT": "product_role (resale/own production)", "SOURCE_EXPECTED": "C000025.TIPO/PROD_COZINHA + recipe + purchase docs", "AVAILABLE": "PARTIAL", "AFFECTED_PRODUCTS": role_c.get("UNKNOWN", 0), "AFFECTED_FIELD": "CSOSN,CFOP", "NEXT_ACTION": "TIPO_ITEM/PRODUTO_SERVICO are unused; only 7 INPUT and ~13 own-production products are objectively evidenced."},
{"INPUT": "product_icms_treatment (taxed vs exempt/not taxed)", "SOURCE_EXPECTED": "official per-NCM ICMS treatment source (none acquired)", "AVAILABLE": False, "AFFECTED_PRODUCTS": st_c.get("ST_NOT_APPLICABLE", 0), "AFFECTED_FIELD": "CSOSN(102 vs 400/300/103)", "NEXT_ACTION": "Decide with accountant/vendor: ERP CST=040 with CSOSN=102 is the emitted practice; needs a documented rule."},
]
(ROOT / "docs" / "FISCAL_CONTEXT_REQUIREMENTS_V3.json").write_text(json.dumps({"artifact": "FISCAL_CONTEXT_REQUIREMENTS_V3", "requirements": req}, ensure_ascii=False, indent=1), encoding="utf-8")
md = ["# NEXT_FISCAL_FIELD_IMPLEMENTATION_V3", "",
"Mechanical ordering: mapping confirmed, rule available, context available, products affected, history validation, auto-apply possible.", "",
"| FIELD | ERP_MAPPING | CONTEXT_AVAILABLE | RULE_AVAILABLE | POTENTIAL_PRODUCTS | HISTORICAL_VALIDATION | NEXT_ACTION |", "|---|---|---|---|---|---|---|",
f"| CSOSN | CONFIRMED (HIGH) | NO (retention + ICMS treatment missing) | PARTIAL (ST→500 rule needs withholding proof) | {cs_c.get('CSOSN_CANDIDATE',0)+cs_c.get('CSOSN_CONTEXT_REQUIRED',0)+cs_c.get('CSOSN_CONFLICT',0)} | trusted 1146 items | Acquire supplier ST retention evidence; get accountant decision on 102 vs 400 |",
"| CEST | CONFIRMED (HIGH) | PARTIAL | PARTIAL | " + str(st_c.get("ST_CANDIDATE", 0)) + " | weak | CEST fill/confirm drives ST_CONFIRMED |",
"| CFOP | CONFIRMED (HIGH) | PARTIAL | YES (ST family) | 29 conflicts | trusted history | Depends on ST_CONFIRMED + retention |",
"| IPI | CONFIRMED (HIGH) | YES | YES | 107 | n/a | Maintained |", "",
"Blocking inputs with counts: `docs/FISCAL_CONTEXT_REQUIREMENTS_V3.json`."]
(ROOT / "docs" / "NEXT_FISCAL_FIELD_IMPLEMENTATION_V3.md").write_text("\n".join(md) + "\n", encoding="utf-8")
print(json.dumps({k: cov[k] for k in ("ST", "ST_reasons", "ST_WITHHELD", "PRODUCT_ROLE", "CSOSN", "CSOSN_HISTORY_of_proposals", "NCM_BLOCKED_PRODUCTS", "ST_CONFLICT_AUDIT")}, ensure_ascii=True, indent=1))
main()
+115
View File
@@ -0,0 +1,115 @@
"""Fiscal Engine V1 coverage over every database in ./Banco — READ-ONLY (no Firebird writes, no new rules).
python scripts/run_multi_db_coverage.py <AGENT_TOKEN> [DIR]
Reuses the existing pure engines: fiscal_decision (NCM/IPI/EX/CEST/MVA), product_defaults (CFOP), icms_context (ST, role,
withholding, CSOSN V2). Only GUARDS are added for company context: the BA Anexo 1 dataset is applied to UF=BA companies only, and
CSOSN logic only under CRT=1; otherwise the engines are told the context is unavailable (no fiscal rule invented).
"""
import json
import os
import sys
from collections import Counter, defaultdict
from concurrent.futures import ThreadPoolExecutor
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 fdb # noqa: E402
from central.app.fiscal_decision import decide_product # noqa: E402
from central.app.icms_context import csosn_v2, product_role, st_status, withheld_status # noqa: E402
from central.app.product_defaults import cfop_default # noqa: E402
from gti_fiscal_agent.firebird_reader import parse_company_context # noqa: E402
HUB = "http://localhost:8000"
s = lambda v: None if v is None else str(v).strip() # noqa: E731
dg = lambda v: "".join(c for c in str(v or "") if c.isdigit()) # noqa: E731
def connect(path):
return fdb.connect(database=str(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 table_cols(cur):
cur.execute("SELECT TRIM(RDB$RELATION_NAME), TRIM(RDB$FIELD_NAME) FROM RDB$RELATION_FIELDS")
m = defaultdict(set)
for t, f in cur.fetchall():
m[t].add(f)
return m
def classify_all(token, products):
"""Classify every distinct (ncm, description) with the production Classifier inside the hub container (read-only on PostgreSQL,
memoized dataset lookups; identical code path to /v1/classify, verified against the HTTP API in tests/verification)."""
import subprocess
import tempfile
pairs = sorted({(s(p["CLASSIFICACAO_FISCAL"]) or "", s(p["PRODUTO"]) or "") for p in products})
with tempfile.TemporaryDirectory() as tmp:
fin, fout = Path(tmp) / "in.json", Path(tmp) / "out.json"
fin.write_text(json.dumps(pairs, ensure_ascii=False), encoding="utf-8")
subprocess.run(["docker", "cp", str(ROOT / "scripts" / "bulk_classify_in_container.py"), "projeto-fiscal-hub-1:/tmp/bulk.py"], check=True)
subprocess.run(["docker", "cp", str(fin), "projeto-fiscal-hub-1:/tmp/in.json"], check=True)
subprocess.run(["docker", "exec", "projeto-fiscal-hub-1", "sh", "-c", "cd /app && python /tmp/bulk.py /tmp/in.json /tmp/out.json"], check=True, capture_output=True)
subprocess.run(["docker", "cp", "projeto-fiscal-hub-1:/tmp/out.json", str(fout)], check=True)
results = json.loads(fout.read_text(encoding="utf-8"))
print(f" classify: {len(pairs)} distinct (ncm, description) pairs for {len(products)} products", flush=True)
return dict(zip(pairs, results))
def analyze(path, token):
conn = connect(path); cur = conn.cursor()
tc = table_cols(cur)
have = lambda t, *cols: t in tc and all(c in tc[t] for c in cols) # noqa: E731
c25 = tc["C000025"]
cols = [c for c in ("CODIGO", "PRODUTO", "CLASSIFICACAO_FISCAL", "CFOP", "CSOSN", "CST", "CEST", "IPI", "EX_TIPI", "TIPO", "PROD_COZINHA", "PRODUTO_SERVICO", "SITUACAO_TRIBUTARIA") if c in c25]
cur.execute(f"SELECT {','.join(cols)} FROM C000025")
products = [dict(zip(cols, r)) for r in cur.fetchall()]
for p in products:
for c in ("CFOP", "CSOSN", "CST", "CEST", "IPI", "EX_TIPI", "TIPO", "PROD_COZINHA", "PRODUTO_SERVICO", "SITUACAO_TRIBUTARIA", "PRODUTO", "CLASSIFICACAO_FISCAL"):
p.setdefault(c, None)
# company (fail closed; guards below)
try:
cur.execute("SELECT UF, CRT FROM C000004"); company = {"status": "OK", **parse_company_context(cur.fetchall())}
except Exception as e: # noqa: BLE001
company = {"status": "UNAVAILABLE", "error": str(e)[:120], "uf": None, "crt": None}
company_default = None
if have("C000004", "ICMS_CSOSN"):
cur.execute("SELECT FIRST 1 ICMS_CSOSN FROM C000004 ORDER BY CODIGO"); company_default = s(cur.fetchone()[0])
# context evidence (only where the tables exist in this database)
recipes = set(); composed = set(); purchases = defaultdict(list); stock_entry = {}
if have("RECEITA_INGREDIENTES", "CODRECEITA"):
cur.execute("SELECT CODRECEITA FROM RECEITA_INGREDIENTES WHERE CODRECEITA IS NOT NULL"); recipes = {str(r[0]).zfill(6) for r in cur.fetchall()}
if have("C000032", "CODPRODUTO", "TIPO"):
cur.execute("SELECT DISTINCT CODPRODUTO FROM C000032 WHERE TIPO = 'COMPOSICAO'"); composed = {s(r[0]) for r in cur.fetchall()}
if have("C000088", "CODPRODUTO", "ICMS_RETIDO", "ICMS_VALORRETIDO", "SUB_VALOR"):
cur.execute("SELECT CODPRODUTO, ICMS_RETIDO, ICMS_VALORRETIDO, SUB_VALOR FROM C000088")
for cod, ret, vr, sv in cur.fetchall():
purchases[s(cod)].append({"icms_retido": s(ret), "valor_retido": vr, "sub_valor": sv})
if have("C000100", "CODPRODUTO", "ULTIMA_ENTRADA", "NOTA_FISCAL"):
cur.execute("SELECT CODPRODUTO, ULTIMA_ENTRADA, NOTA_FISCAL FROM C000100"); stock_entry = {s(r[0]): (r[1] is not None or s(r[2]) not in (None, "", "000000")) for r in cur.fetchall()}
conn.close()
refs = base.build_refs(HUB, token, products, company["uf"] or "BA")
decisions = classify_all(token, products)
from gti_fiscal_agent.coverage import compute_coverage
evidence = {"recipes": recipes, "composed": composed, "purchases": dict(purchases), "stock_entry": stock_entry}
report = compute_coverage(products, [decisions[(s(p["CLASSIFICACAO_FISCAL"]) or "", s(p["PRODUTO"]) or "")] for p in products], company, company_default, refs, evidence)
return {"file": path.name, "company": {k: company.get(k) for k in ("status", "uf", "crt", "error")}, **report}
def main():
base.load_dotenv(ROOT / ".env")
token = sys.argv[1]
directory = Path(sys.argv[2]) if len(sys.argv) > 2 else ROOT / "Banco"
out = []
for p in sorted(directory.glob("*.FDB")):
print("analyzing", p.name, flush=True)
out.append(analyze(p, token))
(ROOT / "docs" / "runs" / "multi_db" / f"{p.stem}_coverage.json").write_text(json.dumps(out[-1], ensure_ascii=False, indent=1, default=str), encoding="utf-8")
print(json.dumps({k: out[-1][k] for k in ("file", "TOTAL_PRODUCTS", "company")}, ensure_ascii=False), flush=True)
(ROOT / "docs" / "MULTI_DATABASE_FISCAL_COVERAGE_V1.json").write_text(json.dumps({"artifact": "MULTI_DATABASE_FISCAL_COVERAGE_V1", "firebird_writes": 0, "databases": out}, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
main()
+136
View File
@@ -0,0 +1,136 @@
"""Product Fiscal Defaults V1 runner: CFOP/CSOSN/CST over all C000025 products (read-only).
Writes docs/PRODUCT_FISCAL_DEFAULTS_MATRIX_V1.json, ..._DRY_RUN_V1.json, FISCAL_CONTEXT_REQUIREMENTS_V2.json,
NEXT_FISCAL_FIELD_IMPLEMENTATION_V2.md. No Firebird writes: the engine can only emit write_eligible=False
(see central/app/product_defaults.py); this runner refuses to write anything.
"""
import json
import os
import sys
from collections import Counter, defaultdict
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
from central.app.product_defaults import PRODUCT_DEFAULTS_ENGINE_VERSION, cfop_default, csosn_default, cst_status, validate_against_history # noqa: E402
from gti_fiscal_agent.firebird_reader import _connect # noqa: E402
def main():
token = sys.argv[1]
hub = "http://localhost:8000"
base.load_dotenv(ROOT / ".env")
conn = _connect(os.environ); cur = conn.cursor()
cur.execute("select CODIGO, PRODUTO, CLASSIFICACAO_FISCAL, CFOP, CSOSN, CST from C000025 order by CODIGO")
products = [dict(zip(("CODIGO", "PRODUTO", "CLASSIFICACAO_FISCAL", "CFOP", "CSOSN", "CST"), r)) for r in cur.fetchall()]
cur.execute("select ICMS_CSOSN, CRT, UF from C000004"); company_default, crt, uf = [str(x).strip() for x in cur.fetchone()]
conn.close()
refs = base.build_refs(hub, token, products, uf)
decisions = []
for i in range(0, len(products), 100):
chunk = products[i:i + 100]
decisions += base.http(f"{hub}/v1/classify/batch", token, {"products": [{"current_ncm": p["CLASSIFICACAO_FISCAL"], "description": p["PRODUTO"] or ""} for p in chunk]})["results"]
trusted = json.loads((ROOT / "docs" / "runs" / "trusted_historical_items.json").read_text(encoding="utf-8"))
hist = defaultdict(lambda: {"cfop": [], "csosn": []})
for t in trusted:
h = hist[str(t["cod"]).strip()]
if (t.get("cfop") or "").strip(): h["cfop"].append(t["cfop"].strip())
if (t.get("csosn") is not None) and str(t["csosn"]).strip(): h["csosn"].append(str(t["csosn"]).strip())
rows = []
for p, d in zip(products, decisions):
code = str(p["CODIGO"]).strip()
ncm_status = d.get("status"); mva = d.get("ba_st_mva_status")
cf = cfop_default(p, ncm_status, mva, refs["cfop_valid"])
cs = csosn_default(p, ncm_status, mva, refs["csosn"], company_default)
ct = cst_status(p, refs["cst_icms"], refs["origem"])
h = hist.get(code, {"cfop": [], "csosn": []})
cf["historical_validation"] = validate_against_history(cf["proposed_value"], h["cfop"])
cs["historical_validation"] = validate_against_history(cs["proposed_value"], h["csosn"])
cf_cur_share = (sum(1 for v in h["cfop"] if v == cf["current_value"]) / len(h["cfop"])) if h["cfop"] else None
cs_cur_share = (sum(1 for v in h["csosn"] if v == cs["current_value"]) / len(h["csosn"])) if h["csosn"] else None
rows.append({
"CODIGO": code, "DESCRIPTION": p["PRODUTO"], "NCM": p["CLASSIFICACAO_FISCAL"],
"CFOP_CURRENT": cf["current_value"], "CFOP_PROPOSED": cf["proposed_value"], "CFOP_STATUS": cf["status"], "CFOP_REASON": cf["reason_code"], "CFOP_WRITE_ELIGIBLE": cf["write_eligible"],
"CSOSN_CURRENT": cs["current_value"], "CSOSN_PROPOSED": cs["proposed_value"], "CSOSN_STATUS": cs["status"], "CSOSN_REASON": cs["reason_code"], "CSOSN_WRITE_ELIGIBLE": cs["write_eligible"],
"CST_CURRENT": ct["current_value"], "CST_STATUS": ct["status"],
"HISTORICAL_OCCURRENCES": {"trusted_cfop_items": len(h["cfop"]), "trusted_csosn_items": len(h["csosn"])},
"HISTORICAL_CONSISTENCY": {"cfop_current_equals_emitted_share": cf_cur_share, "csosn_current_equals_emitted_share": cs_cur_share,
"cfop_validation": cf["historical_validation"], "csosn_validation": cs["historical_validation"]},
"PROVENANCE": {"engine": PRODUCT_DEFAULTS_ENGINE_VERSION, "cfop_dataset": d.get("dataset_versions", {}).get("cfop"), "moc": d.get("dataset_versions", {}).get("nfe_moc_tables"),
"ba_st_mva": d.get("dataset_versions", {}).get("ba_st_mva"), "company": {"crt": crt, "uf": uf, "icms_csosn": company_default}},
"_cfop": cf, "_csosn": cs,
})
def count(field, key):
c = Counter(r[key] for r in rows)
return c
cf_states = Counter(r["CFOP_STATUS"] for r in rows); cs_states = Counter(r["CSOSN_STATUS"] for r in rows)
cf_act = Counter(r["_cfop"]["action_class"] for r in rows); cs_act = Counter(r["_csosn"]["action_class"] for r in rows)
cfv = Counter(r["HISTORICAL_CONSISTENCY"]["cfop_validation"] for r in rows if r["CFOP_PROPOSED"] is not None)
csv_ = Counter(r["HISTORICAL_CONSISTENCY"]["csosn_validation"] for r in rows if r["CSOSN_PROPOSED"] is not None)
cf_conflicts = [r for r in rows if r["CFOP_STATUS"] == "CFOP_DEFAULT_CANDIDATE"]
conflict_hist = Counter()
for r in cf_conflicts:
share = r["HISTORICAL_CONSISTENCY"]["cfop_current_equals_emitted_share"]
conflict_hist["no_trusted_history" if share is None else ("history_emitted_current_value" if share == 1.0 else "history_differs_from_current")] += 1
summary = {
"TOTAL_PRODUCTS": len(rows), "company": {"crt": crt, "uf": uf, "icms_csosn": company_default},
"CFOP": {"RESOLVED": cf_states["CFOP_DEFAULT_RESOLVED"], "ALREADY_CORRECT": cf_act["ALREADY_CORRECT"], "CHANGE_REQUIRED": 0,
"REVIEW": cf_act["REVIEW_REQUIRED"], "CONTEXT_REQUIRED": cf_states["CFOP_OPERATION_CONTEXT_REQUIRED"], "UNRESOLVED": cf_states["CFOP_UNRESOLVED"],
"CANDIDATE": cf_states["CFOP_DEFAULT_CANDIDATE"], "AUTO_APPLY_SAFE": 0, "states": dict(cf_states), "historical_validation_of_proposals": dict(cfv),
"conflict_candidates_vs_history": dict(conflict_hist)},
"CSOSN": {"RESOLVED": cs_states["CSOSN_DEFAULT_RESOLVED"], "ALREADY_CORRECT": 0, "CHANGE_REQUIRED": 0, "REVIEW": cs_act["REVIEW_REQUIRED"],
"CONTEXT_REQUIRED": cs_states["CSOSN_OPERATION_CONTEXT_REQUIRED"], "UNRESOLVED": cs_states["CSOSN_UNRESOLVED"], "CANDIDATE": cs_states["CSOSN_DEFAULT_CANDIDATE"],
"AUTO_APPLY_SAFE": 0, "states": dict(cs_states), "historical_validation_of_proposals": dict(csv_),
"st_conflicts": dict(Counter(r["_csosn"].get("consistency_flag") for r in rows if r["_csosn"].get("consistency_flag")))},
"CST": dict(Counter(r["CST_STATUS"] for r in rows)),
}
for r in rows:
r.pop("_cfop"); r.pop("_csosn")
dm = {"artifact": "PRODUCT_FISCAL_DEFAULTS_MATRIX_V1", "engine": PRODUCT_DEFAULTS_ENGINE_VERSION, "products": rows}
(ROOT / "docs" / "PRODUCT_FISCAL_DEFAULTS_MATRIX_V1.json").write_text(json.dumps(dm, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
dry = {"artifact": "PRODUCT_FISCAL_DEFAULTS_DRY_RUN_V1", "firebird_writes": 0, "write_candidates": [], **summary,
"note": "No proposal is write-eligible: CFOP conflicts need ST-retention/resale context; CSOSN needs product ICMS-treatment and retention context."}
(ROOT / "docs" / "PRODUCT_FISCAL_DEFAULTS_DRY_RUN_V1.json").write_text(json.dumps(dry, ensure_ascii=False, indent=1), encoding="utf-8")
# context requirements V2 (exact inputs still blocking)
st_unknown = sum(1 for r in rows if r["CFOP_REASON"] == "ST_APPLICABILITY_UNCONFIRMED_NEEDS_CEST_CONFIRMATION")
st_subject = sum(1 for r in rows if r["CSOSN_REASON"] == "ST_RETENTION_BY_SUPPLIER_UNKNOWN")
ncm_bad = sum(1 for r in rows if r["CFOP_REASON"] == "NCM_NOT_VALID_CANNOT_ASSESS_ST")
csosn_notst = sum(1 for r in rows if r["CSOSN_STATUS"] == "CSOSN_DEFAULT_CANDIDATE")
blank_cfop = sum(1 for r in rows if r["CFOP_REASON"] == "NO_DECLARED_RESALE_OR_PRODUCTION_FAMILY")
other_family = sum(1 for r in rows if r["CFOP_REASON"] in ("NO_RULE_FOR_DECLARED_CFOP_FAMILY", "DIRECTION_OR_UF_SCOPE_NOT_INTRA_UF_SAIDA"))
conflicts = len(cf_conflicts)
req = [
{"INPUT": "valid_ncm", "SOURCE_EXPECTED": "product master (C000025.CLASSIFICACAO_FISCAL)", "AVAILABLE": "PARTIAL (495/521 valid)", "AFFECTED_PRODUCTS": ncm_bad, "AFFECTED_FIELD": "CFOP,CSOSN", "NEXT_ACTION": "Fix/review invalid+legacy NCMs (never auto-change)."},
{"INPUT": "st_applicability_confirmed (CEST confirmation)", "SOURCE_EXPECTED": "CEST dataset + BA Anexo 1 item", "AVAILABLE": "PARTIAL (31 resolved, 237 candidates)", "AFFECTED_PRODUCTS": st_unknown, "AFFECTED_FIELD": "CFOP,CSOSN", "NEXT_ACTION": "Confirm CEST per product (description evidence/manual review) to settle ST applicability."},
{"INPUT": "st_retention_by_supplier", "SOURCE_EXPECTED": "purchase document per product (entrada) or supplier flag", "AVAILABLE": False, "AFFECTED_PRODUCTS": st_subject, "AFFECTED_FIELD": "CSOSN(500 vs 102),CFOP(5405 vs 5102)", "NEXT_ACTION": "Locate structured retention evidence in purchase items (C000032/C000062 CST 060/CSOSN 500 history) or ask vendor."},
{"INPUT": "product_icms_treatment (taxed / exempt / not taxed)", "SOURCE_EXPECTED": "state ICMS rules per NCM (no acquired dataset)", "AVAILABLE": False, "AFFECTED_PRODUCTS": csosn_notst, "AFFECTED_FIELD": "CSOSN(102 vs 400/300/103)", "NEXT_ACTION": "Needs an official per-NCM ICMS treatment source; ERP CST=040 with CSOSN=102 coexists, so ERP alone is ambiguous."},
{"INPUT": "declared_resale_vs_production", "SOURCE_EXPECTED": "product nature/type (C000025.TIPO_ITEM unused) or company activity", "AVAILABLE": False, "AFFECTED_PRODUCTS": blank_cfop + other_family, "AFFECTED_FIELD": "CFOP", "NEXT_ACTION": "Confirm meaning of FOOD_ATRIBUICAO_VALORES/TIPO_ITEM with vendor before deriving 5101 vs 5102."},
{"INPUT": "st_conflict_resolution (which of ERP CFOP or BA Anexo 1 is wrong)", "SOURCE_EXPECTED": "human/fiscal review of conflicting products", "AVAILABLE": False, "AFFECTED_PRODUCTS": conflicts, "AFFECTED_FIELD": "CFOP", "NEXT_ACTION": "Review queue; do not auto-apply."},
{"INPUT": "destination_uf / consumer_final / direction", "SOURCE_EXPECTED": "NFC-e document model 65 + chave cUF (proven 1499/1499) and C000004.UF", "AVAILABLE": True, "AFFECTED_PRODUCTS": 0, "AFFECTED_FIELD": "CFOP", "NEXT_ACTION": "Already satisfied for NFC-e scope; NF-e needs customer data."},
]
(ROOT / "docs" / "FISCAL_CONTEXT_REQUIREMENTS_V2.json").write_text(json.dumps({"artifact": "FISCAL_CONTEXT_REQUIREMENTS_V2", "requirements": req}, ensure_ascii=False, indent=1), encoding="utf-8")
# priorities V2 (mechanical)
prio = [
("CFOP", "CONFIRMED (HIGH)", "PARTIAL", "YES (ST family)", summary["CFOP"]["RESOLVED"] + conflicts, "trusted history available", "AUTO_APPLY_SAFE=0; review the ST conflicts."),
("CSOSN", "CONFIRMED (HIGH)", "NO", "NO (treatment/retention sources missing)", summary["CSOSN"]["CANDIDATE"] + summary["CSOSN"]["CONTEXT_REQUIRED"], "trusted history available", "Acquire retention/treatment evidence."),
("CST", "CONFIRMED (MEDIUM)", "YES", "n/a (reference only, CRT=1)", 521, "trusted history 100%", "No write needed."),
("CEST", "CONFIRMED (HIGH)", "YES", "PARTIAL", 15, "weak (1/15 supported)", "Description-evidence review of 15 candidates."),
("IPI", "CONFIRMED (HIGH)", "YES", "YES", 107, "n/a", "Maintained; already reconciled."),
]
md = ["# NEXT_FISCAL_FIELD_IMPLEMENTATION_V2", "",
"Mechanical ordering: mapping confirmed, rule available, context available, products affected, history validation, auto-apply possible (post PRODUCT DEFAULTS V1).", "",
"| FIELD | ERP_MAPPING | CONTEXT_AVAILABLE | RULE_AVAILABLE | POTENTIAL_PRODUCTS | HISTORICAL_VALIDATION | NEXT_ACTION |", "|---|---|---|---|---|---|---|"]
md += [f"| {a} | {b} | {c} | {d_} | {e} | {f} | {g} |" for a, b, c, d_, e, f, g in prio]
md += ["", "Blocking inputs and affected counts: `docs/FISCAL_CONTEXT_REQUIREMENTS_V2.json`."]
(ROOT / "docs" / "NEXT_FISCAL_FIELD_IMPLEMENTATION_V2.md").write_text("\n".join(md) + "\n", encoding="utf-8")
print(json.dumps(summary, ensure_ascii=True, indent=1))
main()
+9
View File
@@ -0,0 +1,9 @@
"""Re-export of gti_fiscal_agent.capabilities (the Agent ships the module; scripts keep their historical import path)."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "agent" / "src"))
from gti_fiscal_agent.capabilities import * # noqa: F401,F403,E402
from gti_fiscal_agent.capabilities import ( # noqa: F401,E402
ACCEPT, CAPABILITIES, DEFAULT_ACCEPT, FAMILY, NAME_KEYWORDS, ORIGIN_FIELD, WRITE_TARGETS, classify_schema, detect_capabilities,
field_compatibility, infer_segment, type_ok)
+100
View File
@@ -0,0 +1,100 @@
"""SGV UI oracle: snapshot/diff of ONE C000025 row (READ-ONLY; never writes to Firebird).
python scripts/sgv_product_snapshot.py before <CODIGO>
-> reads the whole row and saves docs/runs/snapshots/<CODIGO>_before.json
(operator changes exactly ONE field in the SGV product screen and saves)
python scripts/sgv_product_snapshot.py diff <CODIGO> [--ui-label "CFOP saída"]
-> re-reads, prints FIELD/BEFORE/AFTER for every changed column, and (with --ui-label) records the
result in docs/runs/sgv_ui_diff_ledger.json. A label is CONFIRMED only when exactly ONE
non-technical column changed.
"""
import argparse
import json
import os
import sys
from datetime import date, datetime
from decimal import Decimal
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agent" / "src"))
SNAP_DIR = ROOT / "docs" / "runs" / "snapshots"
LEDGER = ROOT / "docs" / "runs" / "sgv_ui_diff_ledger.json"
# Columns the ERP rewrites by itself on every save; ignored ONLY because they are clearly automatic.
TECHNICAL = {"ULTIMA_ALTERACAO", "DATA_ALTERACAO", "DATA_ULTIMA_ALTERACAO", "USUARIO_ALTERACAO"}
def norm(v):
if isinstance(v, (bytes, bytearray)):
import hashlib
return "blob:" + hashlib.sha256(bytes(v)).hexdigest()[:16]
if isinstance(v, (datetime, date)):
return v.isoformat()
if isinstance(v, Decimal):
return str(v)
return v.strip() if isinstance(v, str) else v
def read_row(env, code):
from gti_fiscal_agent.firebird_reader import _connect # read-only isolation
conn = _connect(env)
try:
cur = conn.cursor()
cur.execute("SELECT * FROM C000025 WHERE CODIGO = ?", (code,))
row = cur.fetchone()
if row is None:
raise SystemExit(f"product {code} not found")
names = [c[0].strip() for c in cur.description]
blob = {}
return {n: norm(v) for n, v in zip(names, row)}
finally:
conn.close()
def compute_diff(before, after):
out = []
for f in sorted(set(before) | set(after)):
if before.get(f) != after.get(f):
out.append({"FIELD": f, "BEFORE": before.get(f), "AFTER": after.get(f), "technical": f in TECHNICAL})
return out
def load_env():
for line in (ROOT / ".env").read_text(encoding="utf-8").splitlines():
if "=" in line and not line.startswith("#"):
k, _, v = line.partition("="); os.environ.setdefault(k.strip(), v.strip())
def record(label, code, changes):
real = [c for c in changes if not c["technical"]]
ledger = json.loads(LEDGER.read_text(encoding="utf-8")) if LEDGER.exists() else []
entry = {"ui_label": label, "product": code, "recorded": datetime.now().isoformat(timespec="seconds"), "changed_columns": [c["FIELD"] for c in real],
"before": {c["FIELD"]: c["BEFORE"] for c in real}, "after": {c["FIELD"]: c["AFTER"] for c in real},
"confirmed": len(real) == 1, "note": "" if len(real) == 1 else ("no column changed" if not real else "more than one column changed; repeat changing a single field")}
ledger.append(entry)
LEDGER.write_text(json.dumps(ledger, ensure_ascii=False, indent=1), encoding="utf-8")
return entry
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("action", choices=["before", "diff"]); ap.add_argument("codigo"); ap.add_argument("--ui-label")
a = ap.parse_args(argv)
load_env()
SNAP_DIR.mkdir(parents=True, exist_ok=True)
path = SNAP_DIR / f"{a.codigo}_before.json"
if a.action == "before":
path.write_text(json.dumps(read_row(os.environ, a.codigo), ensure_ascii=False, indent=1, default=str), encoding="utf-8")
print(f"snapshot saved: {path}"); return
if not path.exists():
raise SystemExit("run 'before' first")
changes = compute_diff(json.loads(path.read_text(encoding="utf-8")), read_row(os.environ, a.codigo))
print(f"{'FIELD':<28}{'BEFORE':<28}AFTER")
for c in changes:
print(f"{c['FIELD']:<28}{str(c['BEFORE']):<28}{c['AFTER']}{' (technical, ignored)' if c['technical'] else ''}")
if a.ui_label:
print(json.dumps(record(a.ui_label, a.codigo, changes), ensure_ascii=False))
if __name__ == "__main__":
main()