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