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