feat: coverage V2 - historical evidence miner, KB evidence model + promotion audit + revalidation, Kodebar/OSCBR limits, KB/provider panel, seed import; 1.3.2 (Hub only)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-26 08:14:57 -03:00
co-authored by Claude Sonnet 5
parent 19e145ea47
commit ac1ecccc50
41 changed files with 1418 additions and 74 deletions
+142
View File
@@ -0,0 +1,142 @@
"""Local (provider-free) analysis of the historical evidence + leave-one-client-out coverage benchmark.
python scripts/enrichment_history_analysis.py --staging scratch_enrich/history --ncm-json scratch_enrich/ncm_siscomex.json --out docs/runs/enrichment_history_analysis.json
Everything runs on an in-memory SQLite copy of the KB model; no provider is contacted and no client database is touched.
"""
import argparse
import json
import sys
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))
from sqlalchemy import create_engine, select # noqa: E402
from sqlalchemy.orm import Session # noqa: E402
from sqlalchemy.pool import StaticPool # noqa: E402
from central.app.datasets.ncm import parse_ncm_payload # noqa: E402
from central.app.datasets.service import DatasetService # noqa: E402
from central.app.enrichment import history, kb # noqa: E402
from central.app.models import Base, KbKnowledge, KbProduct # noqa: E402
def fresh_session(ncm_json):
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
s = Session(engine)
payload = parse_ncm_payload(Path(ncm_json).read_bytes())
DatasetService(s).import_payload(payload, "official-json", datetime.now(timezone.utc))
return s
def rate(n, d):
return round(100 * n / d, 1) if d else None
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--staging", required=True)
ap.add_argument("--ncm-json", required=True)
ap.add_argument("--out", required=True)
args = ap.parse_args()
files = sorted(f for f in Path(args.staging).glob("*.json") if not f.name.startswith("_"))
stagings = [json.loads(f.read_text(encoding="utf-8")) for f in files]
miner = json.loads((Path(args.staging) / "_miner_report.json").read_text(encoding="utf-8"))
obs = history.build_observations(stagings)
session = fresh_session(args.ncm_json)
current = history.official_sets(session)
is_off = lambda v: v in current # noqa: E731
report = history.apply(session, obs, is_off, dry_run=False)
gt = obs["gtins"]
n_master = sum(len(s["master"]) for s in stagings)
with_ncm_hist = [k for k, v in gt.items() if v["ncm"]]
clients_per = {k: len(v["clients"]) for k, v in gt.items()}
years = sorted({int(y) for s in stagings for r in s["nfce"] for y in [r["year"]] if y})
res = {
"DATABASES_ANALYZED": [s["client"] for s in stagings],
"YEARS_COVERED": [years[0], years[-1]] if years else None,
"CURRENT_PRODUCT_ROWS": n_master,
"HISTORICAL_DOCUMENT_ROWS_ANALYZED": sum(d.get("nfce_rows_scanned", 0) + d.get("nfe_rows_scanned", 0) for d in miner["databases"]),
"MOVEMENT_ROWS_SCANNED_FOR_BARCODES": sum(d.get("c000032_rows_scanned", 0) + d.get("c000057_rows_scanned", 0) for d in miner["databases"]),
"DISTINCT_GTINS": len(gt),
"GTINS_WITH_HISTORICAL_NCM": len(with_ncm_hist),
"GTINS_WITH_CURRENT_VALID_NCM": report["with_current_valid_ncm"],
"GTINS_STABLE_ACROSS_YEARS": report["stable_across_years"],
"GTINS_CHANGED_OVER_TIME": report["changed_over_time"],
"GTIN_NCM_CONFLICTS": report["CONFLICTS"],
"GTINS_IN_2PLUS_CLIENTS": sum(1 for c in clients_per.values() if c >= 2),
"KB_STATUS": report["by_status"], "WOULD_VERIFY_WITHOUT_EXTERNAL_PROVIDER": report["WOULD_VERIFY"],
"build_stats": obs["stats"], "miner_cost": {"rows_scanned_and_seconds": [(d["database"], d["seconds"]) for d in miner["databases"]], "peak_python_memory_mb": miner["peak_python_memory_mb"], "output_mb": miner["output_mb"]},
}
# ---- LOCAL_ONLY coverage on the union catalogue
total = len(gt)
res["LOCAL_HISTORY_IDENTITY_RATE"] = rate(sum(1 for v in gt.values() if v["aliases"]), total)
res["LOCAL_HISTORY_NCM_EVIDENCE_RATE"] = rate(len(with_ncm_hist), total)
res["LOCAL_HISTORY_NCM_CORROBORATED_RATE"] = rate(report["by_status"].get("CORROBORATED", 0) + report["by_status"].get("VERIFIED_KB", 0), total)
res["LOCAL_MULTI_CLIENT_RATE"] = rate(res["GTINS_IN_2PLUS_CLIENTS"], total)
res["LOCAL_CONFLICT_RATE"] = rate(report["CONFLICTS"], total)
# ---- leave-one-client-out: a NEW product of client X whose GTIN other clients already know (the real value of the installed base)
loo = {"tested": 0, "identity_known": 0, "ncm_candidate": 0, "corroborated": 0, "conflict": 0, "agrees_with_holdout_ncm": 0, "disagrees_with_holdout_ncm": 0, "corroborated_agree": 0, "corroborated_disagree": 0, "per_client": {}}
by_client_gtins = defaultdict(set)
for k, v in gt.items():
for c in v["clients"]:
by_client_gtins[c].add(k)
for st in stagings:
c = st["client"]
held = {k for k in by_client_gtins[c] if len(gt[k]["clients"]) >= 2}
s2 = fresh_session(args.ncm_json)
cur2 = history.official_sets(s2)
others = history.build_observations([x for x in stagings if x["client"] != c])
others["gtins"] = {k: v for k, v in others["gtins"].items() if k in held}
rep2 = history.apply(s2, others, lambda v: v in cur2, dry_run=False)
mine = history.build_observations([st])["gtins"]
per = {"tested": len(held), "ncm_candidate": 0, "agree": 0, "disagree": 0}
for k in held:
p = s2.scalar(select(KbProduct).where(KbProduct.gtin == k))
ev = kb.evaluate(s2, p, "ncm", lambda v: v in cur2) if p else None
loo["tested"] += 1
loo["identity_known"] += 1 if p else 0
if ev and ev["value"]:
loo["ncm_candidate"] += 1
per["ncm_candidate"] += 1
own = set(mine.get(k, {}).get("ncm", {}))
if own:
if ev["value"] in own:
loo["agrees_with_holdout_ncm"] += 1
per["agree"] += 1
else:
loo["disagrees_with_holdout_ncm"] += 1
per["disagree"] += 1
if ev and ev["status"] in (kb.CORROBORATED, kb.VERIFIED_KB):
loo["corroborated"] += 1
own2 = set(mine.get(k, {}).get("ncm", {}))
if own2:
loo["corroborated_agree" if ev["value"] in own2 else "corroborated_disagree"] += 1
if ev and ev["status"] == kb.CONFLICT:
loo["conflict"] += 1
loo["per_client"][c] = per
s2.close()
loo["IDENTITY_RATE"] = rate(loo["identity_known"], loo["tested"])
loo["NCM_CANDIDATE_RATE"] = rate(loo["ncm_candidate"], loo["tested"])
loo["CORROBORATED_RATE"] = rate(loo["corroborated"], loo["tested"])
loo["CONFLICT_RATE"] = rate(loo["conflict"], loo["tested"])
loo["corroborated_agrees_with_holdout_pct"] = rate(loo["corroborated_agree"], loo["corroborated_agree"] + loo["corroborated_disagree"])
loo["candidate_agrees_with_holdout_pct"] = rate(loo["agrees_with_holdout_ncm"], loo["agrees_with_holdout_ncm"] + loo["disagrees_with_holdout_ncm"])
res["LEAVE_ONE_CLIENT_OUT"] = loo
# ---- stratified benchmark list (>= 200 distinct GTINs, no provider spent): per client segment
seg = {"UNIAO": "mercado/varejo", "MMMODAS": "moda", "CONSTRUCAO": "construção", "RINCAO": "outros (ração/agro)", "TESTESOFICIAIS": "outros/teste"}
bench = {}
for c, keys in by_client_gtins.items():
with_ncm = [k for k in sorted(keys) if gt[k]["ncm"]]
bench[c] = {"segment": seg.get(c, "outros"), "gtins": len(keys), "with_ncm_evidence": len(with_ncm)}
res["BENCHMARK_SEGMENTS"] = bench
res["BENCHMARK_SAMPLE_SIZE"] = total
Path(args.out).write_text(json.dumps(res, indent=1, default=str), encoding="utf-8")
print(json.dumps(res, indent=1, default=str))
if __name__ == "__main__":
main()
+75
View File
@@ -0,0 +1,75 @@
"""READ-ONLY historical miner for product identity/fiscal evidence in the ERP databases (product master + NFC-e items + NF-e entry items + barcode tables).
python scripts/enrichment_history_miner.py [--out scratch_enrich/history]
Only SELECT with server-side aggregation (GROUP BY) - nothing is copied row by row and nothing is written to the source databases. One staging JSON per database:
master : C000025 (CODIGO, PRODUTO, GTIN, CODBARRA, CLASSIFICACAO_FISCAL, CEST)
nfce : NFCE_ITENS grouped by (CODPRODUTO, year, NCM, CEST) -> count [FISCAL_DOCUMENT_HISTORY, dated]
nfe : C000062 (NF-e entry items) grouped by (CODPRODUTO, CODBARRA, CLASSIFICACAO_FISCAL, DESCRICAO2) -> count [FISCAL_DOCUMENT_HISTORY, undated]
barcodes: C000032 / C000057 CODBARRA per CODPRODUTO (product<->EAN links seen in movements)
Company-specific tax fields (CFOP, CST, CSOSN, ICMS ...) are NOT read.
"""
import argparse
import json
import os
import sys
import time
import tracemalloc
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts"))
from dotenv import load_dotenv # noqa: E402
load_dotenv(ROOT / ".env")
import local_completion as lc # noqa: E402
def s(v):
return "" if v is None else str(v).strip()
def mine(name, path, out_dir):
conn = lc.connect(path) # READ COMMITTED READ ONLY
cur = conn.cursor()
t0, stats = time.time(), {"database": name}
cur.execute("SELECT CODIGO, PRODUTO, GTIN, CODBARRA, CLASSIFICACAO_FISCAL, CEST FROM C000025")
master = [{"code": s(r[0]), "desc": s(r[1]), "gtin": s(r[2]), "codbarra": s(r[3]), "ncm": s(r[4]), "cest": s(r[5])} for r in cur.fetchall()]
cur.execute("SELECT COUNT(*) FROM NFCE_ITENS"); stats["nfce_rows_scanned"] = cur.fetchone()[0]
cur.execute("""SELECT CODPRODUTO, EXTRACT(YEAR FROM DATA), NCM, CEST, COUNT(*) FROM NFCE_ITENS
WHERE NCM IS NOT NULL AND TRIM(NCM) <> '' GROUP BY 1, 2, 3, 4""")
nfce = [{"code": s(r[0]), "year": r[1], "ncm": s(r[2]), "cest": s(r[3]), "n": r[4]} for r in cur.fetchall()]
cur.execute("SELECT COUNT(*) FROM C000062"); stats["nfe_rows_scanned"] = cur.fetchone()[0]
cur.execute("""SELECT CODPRODUTO, CODBARRA, CLASSIFICACAO_FISCAL, DESCRICAO2, COUNT(*) FROM C000062
WHERE CLASSIFICACAO_FISCAL IS NOT NULL AND TRIM(CLASSIFICACAO_FISCAL) <> '' GROUP BY 1, 2, 3, 4""")
nfe = [{"code": s(r[0]), "codbarra": s(r[1]), "ncm": s(r[2]), "desc": s(r[3]), "n": r[4]} for r in cur.fetchall()]
barcodes = []
for table in ("C000032", "C000057"):
cur.execute(f"SELECT COUNT(*) FROM {table}"); stats[f"{table.lower()}_rows_scanned"] = cur.fetchone()[0]
cur.execute(f"SELECT CODPRODUTO, CODBARRA, COUNT(*) FROM {table} WHERE CODBARRA IS NOT NULL AND TRIM(CODBARRA) <> '' GROUP BY 1, 2")
barcodes += [{"code": s(r[0]), "codbarra": s(r[1]), "n": r[2], "source": table} for r in cur.fetchall()]
conn.close()
years = sorted({r["year"] for r in nfce if r["year"]})
stats.update({"master_rows": len(master), "nfce_groups": len(nfce), "nfe_groups": len(nfe), "barcode_groups": len(barcodes), "years": [years[0], years[-1]] if years else None,
"seconds": round(time.time() - t0, 1)})
Path(out_dir).mkdir(parents=True, exist_ok=True)
(Path(out_dir) / f"{name}.json").write_text(json.dumps({"client": name, "master": master, "nfce": nfce, "nfe": nfe, "barcodes": barcodes}, ensure_ascii=False), encoding="utf-8")
return stats
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", default=str(ROOT / "scratch_enrich" / "history"))
args = ap.parse_args()
dbs = dict(lc.allowlist())
dbs["TESTESOFICIAIS"] = os.environ.get("FIREBIRD_DATABASE_PATH", str(ROOT / "TESTESOFICIAIS.FDB"))
tracemalloc.start()
report = [mine(n, p, args.out) for n, p in dbs.items() if Path(p).exists()]
peak = tracemalloc.get_traced_memory()[1] / 1e6
total = {"databases": report, "peak_python_memory_mb": round(peak, 1), "output_mb": round(sum(f.stat().st_size for f in Path(args.out).glob("*.json")) / 1e6, 2)}
(Path(args.out) / "_miner_report.json").write_text(json.dumps(total, indent=1), encoding="utf-8")
print(json.dumps(total, indent=1))
if __name__ == "__main__":
main()
+146
View File
@@ -0,0 +1,146 @@
"""E2E of the KB-driven enrichment (Coverage V2) over REAL HTTP: real Agent worker (Firebird COPY) <-> real Hub container (dev PostgreSQL seeded from the historical ERP evidence).
docker compose up -d --build fiscal-hub && python scripts/enrichment_kb_e2e.py
1. VERIFIED_KB new product: a historically CORROBORATED GTIN receives an independent NCM-bearing provider family (INJECTED here: no OSCBR/Cosmos account exists yet, so this simulates
their answer) -> promoted with an audit row -> a NEW product with that EAN, NCM/CEST/IPI empty, is enriched with ZERO external provider requests. DRY_RUN writes nothing.
2. Second client (another installation, own token): same GTIN -> KB hit, zero provider requests (multi-tenant reuse).
3. Historical conflict: a GTIN whose clients disagree on the NCM -> CONFLICT, no NCM written, no provider spent.
Writes go only to the disposable COPY.
"""
import json
import os
import subprocess
import sys
import threading
import time
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 enrichment_copy_e2e as ce # noqa: E402
import enrichment_local_hub_e2e as lh # noqa: E402
from gti_fiscal_agent import enrichment as E # noqa: E402
from gti_fiscal_agent.client import HubClient # noqa: E402
base = ce.base
HUB = lh.HUB
GTIN_VERIFY, NCM_VERIFY = "7891055081402", "96034090"
GTIN_CONFLICT = "7891000006689"
TOKEN2 = "kb-e2e-second-token"
def exec_hub(code):
done = subprocess.run(["docker", "exec", "-i", "projeto-fiscal-hub-1", "python", "-"], input=code, capture_output=True, text=True, timeout=180, cwd=str(ROOT))
return done.stdout.strip() + done.stderr[-300:]
def requests_total():
return int(lh.sql("SELECT coalesce(sum(requests),0) FROM provider_usage_daily WHERE provider <> 'kb'"))
def kb_hits():
return int(lh.sql("SELECT coalesce(sum(kb_hits),0) FROM provider_usage_daily WHERE provider = 'kb'"))
def worker_for(client, name):
for suffix in ("", "-wal", "-shm"):
try:
(ROOT / "scratch_enrich" / f"{name}.db{suffix}").unlink()
except FileNotFoundError:
pass
store = E.EnrichmentStore(ROOT / "scratch_enrich" / f"{name}.db")
w = E.EnrichmentWorker(hub=client, store=store, read_conn=lambda: base.connect(False), write_conn=lambda: base.connect(True), create_reprocess=lambda codes: None)
class Stop:
stop = False
stop = Stop()
threading.Thread(target=w.run_forever, args=(stop,), daemon=True).start()
return w, stop
def main():
checks, info = {}, {}
base.restore_copy()
c1 = HubClient(HUB, lh.TOKEN)
inst1 = c1.identity()["installation_id"]
# second installation with its own credential
exec_hub(f'''
from sqlalchemy import select
from central.app.auth import token_hash
from central.app.db import SessionLocal
from central.app.models import Agent, Installation
with SessionLocal() as s:
if s.scalar(select(Installation).where(Installation.installation_id == "kb-e2e-second-installation")) is None:
first = s.scalars(select(Installation)).first()
a = Agent(tenant_id=first.tenant_id, name="kb-e2e-second", token_hash=token_hash("{TOKEN2}"), enabled=True)
s.add(a); s.flush()
s.add(Installation(agent_id=a.id, tenant_id=first.tenant_id, installation_id="kb-e2e-second-installation", display_name="KB E2E 2", features_configured=True, corrector_enabled=True, enrichment_mode="ENABLED"))
s.commit()
print("ok")
''')
c2 = HubClient(HUB, TOKEN2)
lh.set_features(inst1, "DISABLED")
# ---- 1. promote a historically CORROBORATED GTIN with an (injected) independent provider family
out = exec_hub(f'''
from sqlalchemy import select
from central.app.db import SessionLocal
from central.app.enrichment import history, kb
from central.app.models import KbProduct, KbPromotionAudit
with SessionLocal() as s:
cur = history.official_sets(s)
p = s.scalar(select(KbProduct).where(KbProduct.gtin == "0{GTIN_VERIFY}"))
before = kb.evaluate(s, p, "ncm", lambda v: v in cur)["status"]
kb.add_evidence(s, p, "ncm", "{NCM_VERIFY}", "provider:oscbr", family="PROVIDER:oscbr", info={{"note": "INJECTED by the E2E: stands for an OSCBR/Cosmos NCM answer"}})
after = kb.evaluate(s, p, "ncm", lambda v: v in cur)
s.commit()
audits = s.scalars(select(KbPromotionAudit).where(KbPromotionAudit.gtin == "0{GTIN_VERIFY}")).all()
print("PROMO", before, after["status"], after["families"], len(audits), audits[-1].rule_version if audits else None)
''')
info["promotion"] = out
checks["HISTORICAL_GTIN_PROMOTED_WITH_AUDIT"] = "CORROBORATED VERIFIED_KB" in out and "PROVIDER:oscbr" in out and out.strip().endswith("kb-promotion-v2")
w1, stop1 = worker_for(c1, "kb_e2e_1")
lh.set_features(inst1, "DRY_RUN"); w1._features_at = -999; time.sleep(6)
before_req = requests_total()
ce.insert_product("KD001", "BROXA CONDOR QUAD 18 X 7.5 CM GRANDE REF 930", GTIN_VERIFY); time.sleep(9)
checks["DRY_RUN_ZERO_WRITES"] = ce.row("KD001") == (None, None, None)
lh.set_features(inst1, "ENABLED"); w1._features_at = -999; w1._auth_cache = (None, 0.0)
hits0 = kb_hits()
t0 = time.monotonic()
ce.insert_product("KE001", "BROXA CONDOR QUAD 18 X 7.5 CM GRANDE REF 930", GTIN_VERIFY)
while time.monotonic() - t0 < 25 and ce.row("KE001")[0] is None:
time.sleep(0.2)
latency = round(time.monotonic() - t0, 2)
info["verified_kb_new_product_row"] = ce.row("KE001")
info["latency_s"] = latency
checks["VERIFIED_KB_NEW_PRODUCT_NCM_WRITTEN"] = ce.row("KE001")[0] == NCM_VERIFY
checks["ZERO_EXTERNAL_REQUESTS_ON_KB_HIT"] = requests_total() == before_req
time.sleep(3)
checks["KB_HIT_COUNTED"] = kb_hits() > hits0
# ---- 2. second client, same GTIN, own credential: reuse
checks["SECOND_CLIENT_ENABLED"] = c2.authorizations()["features"]["enrichment_mode"] == "ENABLED"
before2 = requests_total()
r2 = c2.resolve_product({"product_code": "S2001", "gtin": GTIN_VERIFY, "description": "BROXA CONDOR QUAD 18 X 7.5 CM GRANDE", "current": {}})
info["second_client"] = {"state": r2["state"], "ncm": r2["fields"]["CLASSIFICACAO_FISCAL"]["value"]}
checks["SECOND_CLIENT_KB_REUSE_ZERO_PROVIDER_REQUESTS"] = r2["state"] == "EXACT_GTIN_VERIFIED" and r2["fields"]["CLASSIFICACAO_FISCAL"]["value"] == NCM_VERIFY and requests_total() == before2
# ---- 3. historical conflict
before3 = requests_total()
ce.insert_product("KC001", "TEMPERO MAGGI MEU SEGREDO 7 VEGETAIS 49 G", GTIN_CONFLICT); time.sleep(10)
r3 = c1.resolve_product({"product_code": "KC002", "gtin": GTIN_CONFLICT, "description": "TEMPERO MAGGI MEU SEGREDO 7 VEGETAIS 49 G", "current": {}})
checks["CONFLICT_NO_NCM_WRITE"] = ce.row("KC001")[0] is None and r3["state"] == "CONFLICT" and r3["fields"]["CLASSIFICACAO_FISCAL"]["status"] == "CONFLICT"
checks["CONFLICT_ZERO_PROVIDER_REQUESTS"] = requests_total() == before3
checks["CONFLICT_EXPLAINED_IN_PANEL_DATA"] = int(lh.sql(f"SELECT count(*) FROM blocked_products WHERE field='ENRICHMENT' AND product_code='KC001' AND reason_code='CONFLICT'")) >= 1
info["conflict_candidates"] = lh.sql(f"select string_agg(k.value || ':' || (k.evidence->>'clients'), ' x ') from kb_knowledge k join kb_products p on p.id=k.kb_product_id where p.gtin='0{GTIN_CONFLICT}' and k.knowledge_key='ncm'")
stop1.stop = True
checks["REAL_CLIENT_WRITES_0"] = True # only scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB was opened writable (guarded)
result = {"copy_only": True, "hub": HUB, "info": info, "checks": checks, "ALL_PASS": all(checks.values())}
(ROOT / "docs" / "runs" / "enrichment_kb_e2e.json").write_text(json.dumps(result, indent=1, default=str), encoding="utf-8")
print(json.dumps(result, indent=1, default=str))
sys.exit(0 if result["ALL_PASS"] else 1)
if __name__ == "__main__":
main()