feat: deterministic cClassTrib resolver + gated CCLASSTRIB auto-apply (default OFF), RTC panel summary; 1.1.9

- resolver (rules RTC-R0..R4/A1..A3/G1..G2) over the official cClassTrib table 2026-06-22 and LC 214/2025 annexes; local vs official catalog comparison
- ONE root-cause RTC blocker per product (missing/unresolved/ambiguous/conflict/official mismatch/catalog outdated/evidence missing)
- auto-apply: C000025.CCLASSTRIB only, three switches (FIREBIRD_WRITES + RTC_AUTO_APPLY_ENABLED + Hub authorization), per-product re-read and re-decision, never overwrites, rowcount 1, audit table rtc_write_audits (migration 0027)
- copy-only write E2E (280/280 applied, only CCLASSTRIB changed, 2nd run 0 writes, stale decision 0 writes); real databases untouched
- docs: cclasstrib_resolution, rtc_auto_apply, unified_fiscal_completeness

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-24 21:18:34 -03:00
co-authored by Claude Sonnet 5
parent 88f4318072
commit 9f08998800
51 changed files with 16461 additions and 47 deletions
+102
View File
@@ -0,0 +1,102 @@
"""Build central/app/rtc_reference_data.py: the OFFICIAL cClassTrib catalog + the LC 214/2025 goods annexes as structured, hash-pinned reference data.
python scripts/build_rtc_reference_data.py
Inputs (all official, already downloaded/hashed):
sources/raw/bootstrap_v1/cClassTrib 2026-06-22.xlsx Portal Nacional NF-e cClassTrib table (parsed with the Hub's own parser)
sources_cache/wb_lc214.bin LC 214/2025 compiled (Planalto, through LC 227/2026), extracted to text
Nothing here decides anything: it only turns the published texts into data with provenance. Item texts that carry a condition/exception are
marked `clean=False` so the resolver never treats them as a deterministic match.
"""
import hashlib
import html
import json
import re
import sys
from datetime import date
from pathlib import Path
from pprint import pformat
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from central.app.datasets.bootstrap_v1_extra import parse_rtc_cclasstrib # noqa: E402
XLSX = ROOT / "sources" / "raw" / "bootstrap_v1" / "cClassTrib 2026-06-22.xlsx"
LC214 = ROOT / "sources_cache" / "wb_lc214.bin"
ROMAN = {"I": 1, "II": 2, "III": 3, "IV": 4, "V": 5, "VI": 6, "VII": 7, "VIII": 8, "IX": 9, "X": 10, "XI": 11, "XII": 12, "XIII": 13, "XIV": 14, "XV": 15}
RESTRICTION = re.compile(r"exceto|ressalv|desde que|em conformidade|requisitos|somente|exclusiv|quando|observad|salvo|apenas|conforme|vedad|\ba\)|\bb\)|\bex\b|não", re.I)
CODE = re.compile(r"\b(\d{2}\.\d{2}(?:\.\d{1,2}){0,2}|\d{4}(?:\.\d{1,2}){1,3})\b")
def lc214_text():
raw = LC214.read_bytes()
text = raw.decode("latin-1", errors="replace")
text = re.sub(r"<script.*?</script>|<style.*?</style>", "", text, flags=re.S)
text = html.unescape(re.sub(r"<[^>]+>", " ", text))
return raw, re.sub(r"[ \t\r\f\v]+", " ", text)
def parse_annexes(text):
heads = [(m.start(), m.group(1)) for m in re.finditer(r"\n[ \xa0]*ANEXO ([IVXL]+)[ \xa0]*\n", text)]
blocks, seen = {}, set()
for i, (pos, roman) in enumerate(heads):
if roman in seen or roman not in ROMAN:
continue
# the goods annexes I..XV are the first occurrence of each numeral, before the Simples annexes (which repeat I..VII)
seen.add(roman)
end = heads[i + 1][0] if i + 1 < len(heads) else len(text)
blocks[roman] = text[pos:end]
out = {}
for roman, block in blocks.items():
head = " ".join(block[:600].split())
items, current, expected = [], None, 1
lines = [ln.strip() for ln in block.split("\n")]
for idx, ln in enumerate(lines):
if ln == str(expected):
nxt = next((x for x in lines[idx + 1:idx + 8] if x), "")
if nxt and not nxt.isdigit():
current = {"item": expected, "text": []}
items.append(current)
expected += 1
continue
if current is not None and ln:
current["text"].append(ln)
parsed = []
for it in items:
desc = " ".join(" ".join(it["text"]).split())
prefixes = sorted({re.sub(r"\D", "", c) for c in CODE.findall(desc)})
clean = not RESTRICTION.search(desc) and bool(prefixes)
parsed.append({"item": it["item"], "description": desc[:400], "prefixes": prefixes, "clean": clean})
out[roman] = {"number": ROMAN[roman], "title": head[:200], "revoked": "Revogado" in head, "items": parsed}
return out
def main():
payload = parse_rtc_cclasstrib(XLSX.read_bytes())
official = []
for e in payload.entries:
ind = e["indicators"]
official.append({"code": e["cclasstrib"], "cst": e["cst"], "name": (e["name"] or "")[:160], "tipo_aliquota": e["tipo_aliquota"], "p_red_ibs": e["p_red_ibs"], "p_red_cbs": e["p_red_cbs"],
"start": (e["d_ini_vig"] or "")[:10] or None, "end": (e["d_fim_vig"] or "")[:10] or None, "anexo": e.get("anexo"), "ind_nfe": ind.get("indNFe") == "1", "ind_nfce": ind.get("indNFCe") == "1"})
raw, text = lc214_text()
annexes = parse_annexes(text)
counts = {k: (len(v["items"]), sum(1 for i in v["items"] if i["clean"])) for k, v in annexes.items()}
module = ROOT / "central" / "app" / "rtc_reference_data.py"
catalog_meta = {"file": XLSX.name, "sha256": hashlib.sha256(XLSX.read_bytes()).hexdigest(), "publication": "2026-06-22", "source": "Portal Nacional da NF-e (cClassTrib)", "parser": payload.parser_version}
lc_meta = {"document": "Lei Complementar 214/2025 compiled (through LC 227/2026)", "url": "https://www.planalto.gov.br/ccivil_03/leis/lcp/lcp214.htm", "sha256": hashlib.sha256(raw).hexdigest(),
"accessed_at": date.today().isoformat(), "fetched_via": "Internet Archive copy of the official URL"}
header = '"""GENERATED by scripts/build_rtc_reference_data.py - do not edit. Official cClassTrib catalog and LC 214/2025 goods annexes with provenance."""' + "\n\n"
body = "\n\n".join([
"OFFICIAL_CATALOG = " + pformat(catalog_meta, width=160),
"OFFICIAL_CCLASSTRIB = " + pformat(official, width=220),
"LC214_SOURCE = " + pformat(lc_meta, width=160),
"LC214_ANNEXES = " + pformat(annexes, width=220),
]) + "\n"
module.write_text(header + body, encoding="utf-8")
(ROOT / "docs" / "sources" / "RTC_REFERENCE_MANIFEST_V1.json").write_text(json.dumps({"official_catalog": catalog_meta, "official_classes": len(official), "lc214": lc_meta, "annex_items_total_and_clean": counts}, ensure_ascii=False, indent=1), encoding="utf-8")
print(len(official), "official classes;", "annex items (total, clean):", counts)
if __name__ == "__main__":
main()
+140
View File
@@ -0,0 +1,140 @@
"""COPY-ONLY write E2E for the RTC auto-apply (C000025.CCLASSTRIB).
python scripts/rtc_copy_write_e2e.py
Runs ONLY against scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB (a gbak restore of TESTESOFICIAIS). It refuses any other path, and refuses every real database.
Scenario (all through the Agent's own code path `run_agent_service._rtc_auto_apply`):
1. the copy's company is switched to regime normal (C000004.CRT='3'): a disposable experiment, so the rules can act (the real company is Simples => 0 candidates);
2. assess -> candidates; 3. apply with all three gates open; 4. re-read, FK/CST checks, coverage; 5. ONLY CCLASSTRIB changed (whole-table diff);
6. second run: 0 candidates, 0 writes; 7. stale-decision abort (RTC switched off on the copy between decision and write) writes nothing.
Writes go to the copy and nowhere else.
"""
import importlib.util
import json
import os
import sys
from datetime import date
from pathlib import Path
from types import SimpleNamespace
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"))
from dotenv import load_dotenv # noqa: E402
load_dotenv(ROOT / ".env")
import fdb # noqa: E402
import local_completion as lc # noqa: E402
from central.app.operational_state import classify_product_state, summarize_operational_states # noqa: E402
from gti_fiscal_agent import firebird_reader, unified_fiscal # noqa: E402
COPY = str((ROOT / "scratch_rtc_e2e" / "TESTESOFICIAIS_RTC_COPY.FDB").resolve())
NCM_OK = set((ROOT / "docs" / "runs" / "ncm_valid_codes.txt").read_text().split())
def guard(path):
canon = os.path.normcase(os.path.abspath(path))
if canon != os.path.normcase(COPY):
raise SystemExit(f"REFUSED: {path} is not the disposable copy")
real = {os.path.normcase(os.path.abspath(p)) for p in lc.allowlist().values()}
if canon in real or "scratch_rtc_e2e" not in canon:
raise SystemExit("REFUSED: real database")
def connect(rw):
kw = dict(database=COPY, 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 load_service():
spec = importlib.util.spec_from_file_location("run_agent_service", ROOT / "agent" / "scripts" / "run_agent_service.py")
mod = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = mod
spec.loader.exec_module(mod)
return mod
def digits(v):
return "".join(ch for ch in str(v or "") if ch.isdigit())
def snapshot_table(cur, table, key, skip_cols=()):
cur.execute(f"SELECT * FROM {table}")
cols = [d[0] for d in cur.description]
ki = cols.index(key)
return cols, {r[ki]: {c: v for c, v in zip(cols, r) if c not in skip_cols} for r in cur.fetchall()}
def coverage(env, company, ncm_valid, override=None):
conn = firebird_reader._connect(env)
try:
out = unified_fiscal.assess_from_connection(conn, company, ncm_valid=ncm_valid, rtc_override=override)
finally:
conn.close()
products = firebird_reader.read_products(env)
states = [classify_product_state(p, {"status": "EXACT_NCM"}, fiscal=out["findings"].get(str(p["CODIGO"]))) for p in products]
s = summarize_operational_states(states)
return out, {"total": s["TOTAL_PRODUCTS"], "complete": s["COMPLETE_PRODUCTS"], "blocked": s["SAFE_BLOCKED"], "unclassified": s["UNCLASSIFIED"], "coverage_percent": round(100 * s["COMPLETE_PRODUCTS"] / s["TOTAL_PRODUCTS"], 2)}
def main():
guard(COPY)
service = load_service()
env = {"FIREBIRD_DATABASE_PATH": COPY, "FB_USER": os.environ["FB_USER"], "FB_PASSWORD": os.environ["FB_PASSWORD"], "FIREBIRD_CLIENT_LIBRARY": os.environ.get("FIREBIRD_CLIENT_LIBRARY")}
report = {"database": "scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB (gbak restore of TESTESOFICIAIS)", "real_database_writes": 0, "real_database_write_attempts": 0}
# ---- 1. company -> regime normal on the COPY (documented experiment)
rw = connect(True); cur = rw.cursor()
cur.execute("SELECT DISTINCT TRIM(CRT) FROM C000004"); report["copy_original_crt"] = [r[0] for r in cur.fetchall()]
cur.execute("UPDATE C000004 SET CRT = '3'"); rw.commit()
cur.execute("SELECT DISTINCT TRIM(CRT) FROM C000004"); company = {"crt": cur.fetchone()[0]}
report["copy_crt_for_experiment"] = company["crt"]
_, before_c25 = snapshot_table(cur, "C000025", "CODIGO")
cur.execute("SELECT USAR_REFORMA_TRIBUTARIA FROM PARAMETROS"); report["copy_rtc_param"] = cur.fetchone()[0]
rw.commit(); rw.close()
ncm_valid = {str(c).strip(): digits(v["CLASSIFICACAO_FISCAL"]) in NCM_OK for c, v in before_c25.items()}
# ---- 2. assess (as found on the copy)
out, cov_before = coverage(env, company, ncm_valid)
report["coverage_before"] = cov_before
report["resolution_before"] = {k: v for k, v in out["context"]["rtc_resolution"].items() if k != "by_reason"}
products = firebird_reader.read_products(env)
config = SimpleNamespace(firebird_writes=True, rtc_auto_apply_enabled=True, rtc_auto_apply_max_per_job=500, database_path=COPY)
job = {"job_id": 424242, "authorizations": {"rtc_auto_apply": True}}
# ---- 7 (first, on purpose): stale decision - RTC switched off on the copy between decision and write => nothing is written
rw = connect(True); c = rw.cursor(); c.execute("UPDATE PARAMETROS SET USAR_REFORMA_TRIBUTARIA = 0"); rw.commit(); rw.close()
stale = service._rtc_auto_apply(env, config, job, products, out, company)
report["stale_decision_run"] = {"write_attempts": stale["write_attempts"], "write_successes": stale["write_successes"], "aborted_reasons": stale["aborted_reasons"]}
rw = connect(True); c = rw.cursor(); c.execute("UPDATE PARAMETROS SET USAR_REFORMA_TRIBUTARIA = 1"); rw.commit(); rw.close()
# ---- 3. apply
first = service._rtc_auto_apply(env, config, job, products, out, company)
report["first_run"] = {"mode": first["mode"], "ready_total": first["ready_total"], "write_attempts": first["write_attempts"], "write_successes": first["write_successes"], "aborted_reasons": first["aborted_reasons"],
"audit_sample": first["applied"][:3]}
# ---- 4/5. re-read, FK, whole-table diff
ro = connect(False); cur = ro.cursor()
cols, after_c25 = snapshot_table(cur, "C000025", "CODIGO")
changed = {code: [c for c in cols if before_c25[code][c] != row[c]] for code, row in after_c25.items() if any(before_c25[code][c] != row[c] for c in cols)}
report["rows_changed"] = len(changed)
report["columns_changed"] = sorted({c for v in changed.values() for c in v})
cur.execute("SELECT COUNT(*) FROM C000025 p WHERE p.CCLASSTRIB IS NOT NULL AND NOT EXISTS (SELECT 1 FROM RTC_CCLASSTRIB c WHERE c.CCLASSTRIB = p.CCLASSTRIB)"); report["fk_orphans"] = cur.fetchone()[0]
cur.execute("SELECT COUNT(*) FROM C000025 p JOIN RTC_CCLASSTRIB c ON c.CCLASSTRIB = p.CCLASSTRIB WHERE NOT EXISTS (SELECT 1 FROM RTC_CST s WHERE s.CSTIBSCBS = c.CSTIBSCBS)"); report["cst_orphans"] = cur.fetchone()[0]
cur.execute("SELECT CCLASSTRIB, COUNT(*) FROM C000025 WHERE CCLASSTRIB IS NOT NULL GROUP BY 1"); report["written_by_class"] = {r[0].strip(): r[1] for r in cur.fetchall()}
cur.execute("SELECT COUNT(*) FROM RTC_CCLASSTRIB"); report["catalog_rows_unchanged"] = cur.fetchone()[0]
ro.close()
out2, cov_after = coverage(env, company, ncm_valid)
report["coverage_after"] = cov_after
report["resolution_after"] = {k: v for k, v in out2["context"]["rtc_resolution"].items() if k != "by_reason"}
# ---- 6. second run
products2 = firebird_reader.read_products(env)
second = service._rtc_auto_apply(env, config, job, products2, out2, company)
report["second_run"] = {"ready_total": second["ready_total"], "write_attempts": second["write_attempts"], "write_successes": second["write_successes"]}
report["IDEMPOTENT"] = second["write_attempts"] == 0 and second["write_successes"] == 0
report["ONLY_CCLASSTRIB_CHANGED"] = report["columns_changed"] in ([], ["CCLASSTRIB"])
report["PASS"] = bool(report["IDEMPOTENT"] and report["ONLY_CCLASSTRIB_CHANGED"] and report["fk_orphans"] == 0 and report["cst_orphans"] == 0 and first["write_successes"] == first["write_attempts"] > 0
and report["stale_decision_run"]["write_successes"] == 0 and report["rows_changed"] == first["write_successes"])
(ROOT / "docs" / "runs" / "rtc_copy_write_e2e.json").write_text(json.dumps(report, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
print(json.dumps({k: report[k] for k in ("copy_original_crt", "copy_rtc_param", "coverage_before", "coverage_after", "first_run", "stale_decision_run", "rows_changed", "columns_changed", "fk_orphans", "cst_orphans", "second_run", "IDEMPOTENT", "ONLY_CCLASSTRIB_CHANGED", "PASS")},
ensure_ascii=False, default=str)[:3000])
if __name__ == "__main__":
main()
+79
View File
@@ -0,0 +1,79 @@
"""READ-ONLY dry-run of the deterministic cClassTrib resolver over real Firebird databases. Writes NOTHING to any database.
python scripts/rtc_resolution_dryrun.py --db TESTESOFICIAIS CONSTRUCAO ... [--out docs/runs/rtc_resolution_dryrun.json]
For each database, three passes over the same read-only snapshot:
AS_FOUND the ERP's own RTC switch and the company's real CRT (what the Agent would report today)
WHATIF_RTC_ON RTC forced ON (pure evaluator; PARAMETROS is never touched), real CRT
WHATIF_NORMAL_ON RTC forced ON and regime normal (CRT 3): what the rules WOULD resolve once an official orientation exists for Simples
NCM validity = the code exists in the active official NCM table exported from the Hub (docs/runs/ncm_valid_codes.txt).
"""
import argparse
import csv
import json
import sys
from collections import Counter
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"))
from dotenv import load_dotenv # noqa: E402
load_dotenv(ROOT / ".env")
import local_completion as lc # noqa: E402
from gti_fiscal_agent import firebird_reader, unified_fiscal # noqa: E402
from unified_coverage_e2e import resolved_crt # noqa: E402
NCM_OK = set((ROOT / "docs" / "runs" / "ncm_valid_codes.txt").read_text().split())
def digits(value):
return "".join(ch for ch in str(value or "") if ch.isdigit())
def one_pass(env, company, override, ncm_valid):
conn = firebird_reader._connect(env) # READ COMMITTED READ ONLY
try:
return unified_fiscal.assess_from_connection(conn, company, rtc_override=override, ncm_valid=ncm_valid)
finally:
conn.close()
def main():
ap = argparse.ArgumentParser(); ap.add_argument("--db", nargs="+", required=True); ap.add_argument("--out", default="docs/runs/rtc_resolution_dryrun.json"); ap.add_argument("--sample", type=int, default=100)
a = ap.parse_args()
report, sample = {}, []
for name in a.db:
path = lc.allowlist()[name]; env = lc.env_for(path)
crt = resolved_crt(name, path)
products = firebird_reader.read_products(env)
ncm_valid = {str(p["CODIGO"]): digits(p.get("CLASSIFICACAO_FISCAL")) in NCM_OK for p in products}
passes = {}
for label, override, company in (("AS_FOUND", None, {"crt": crt}), ("WHATIF_RTC_ON", True, {"crt": crt}), ("WHATIF_NORMAL_ON", True, {"crt": "3"})):
out = one_pass(env, company, override, ncm_valid)
summ = out["context"].get("rtc_resolution")
findings = out["findings"]
by_status = Counter((f.get("rtc_resolution") or {}).get("status", "NOT_EVALUATED") for f in findings.values())
conflicts = Counter(b["type"] for f in findings.values() for b in f["blockers"] if b["category"] == "RTC")
passes[label] = {"crt": company["crt"], "rtc_enabled": out["context"]["rtc_enabled"], "products": len(findings), "resolution_summary": summ, "by_status": dict(by_status), "rtc_blocker_types": dict(conflicts),
"AUTO_APPLY_CANDIDATES": sum(1 for f in findings.values() if (f.get("rtc_resolution") or {}).get("auto_apply_candidate") and not [b for b in f["blockers"] if b["type"] in ("CCLASSTRIB_CONFLICT",)])}
if label == "WHATIF_NORMAL_ON" and len(sample) < a.sample:
by_code = {str(p["CODIGO"]): p for p in products}
for code, f in findings.items():
res = f.get("rtc_resolution")
if res and len(sample) < a.sample and (res["auto_apply_candidate"] or res["status"] == "AMBIGUOUS") and (len([s for s in sample if s["database"] == name]) < max(1, a.sample // len(a.db))):
p = by_code.get(code, {})
sample.append({"database": name, "product": code, "ncm": digits(p.get("CLASSIFICACAO_FISCAL")), "current_cclasstrib": (p.get("CCLASSTRIB") or "").strip() if p.get("CCLASSTRIB") else "", "proposed_cclasstrib": res["resolved_cclasstrib"] or "",
"status": res["status"], "rule_id": res["rule_id"], "reason": res["reason"], "confidence_type": res["confidence_type"], "scenario": "WHATIF_NORMAL_ON (regime normal, RTC on)"})
passes[label]["context"] = {k: out["context"][k] for k in ("rtc_param", "rtc_source", "rtc_observed_at", "local_catalog_hash", "official_catalog", "catalog_integrity")}
report[name] = {"crt": crt, "total_products": len(products), "passes": passes}
print(name, "crt", crt, {k: (v["rtc_enabled"], v["by_status"], v["AUTO_APPLY_CANDIDATES"]) for k, v in passes.items()}, flush=True)
doc = {"mode": "READ_ONLY_DRY_RUN", "FIREBIRD_WRITES": 0, "WRITE_ATTEMPTS": 0, "WRITE_SUCCESSES": 0, "databases": report}
(ROOT / a.out).write_text(json.dumps(doc, ensure_ascii=False, indent=1, default=str), encoding="utf-8")
with open(ROOT / "docs" / "runs" / "rtc_resolution_audit_sample.csv", "w", newline="", encoding="utf-8-sig") as fh:
w = csv.DictWriter(fh, fieldnames=list(sample[0]) if sample else ["database"]); w.writeheader(); w.writerows(sample)
print("sample rows:", len(sample))
if __name__ == "__main__":
main()