fix: product_role never infers OWN_PRODUCTION from a single COMPOSICAO occurrence
Missão 4 / Bloco A.1 (prioridade máxima): central.app.field_contract.infer_role treated ANY C000032.TIPO=COMPOSICAO row for a product as proof of own production, with no ratio check - a beer sold 19 times and used once as a recipe component (Agent 2, real data) was classified OWN_PRODUCTION from that single occurrence, proposing to change its CFOP and IPPT. New deterministic threshold in field_contract.py: COMPOSICAO must be a majority (>=70%) of that product's own C000032 movement history, with at least 5 total events, to count as own-production evidence. Below 30% the evidence is discarded entirely (falls through to other role evidence, e.g. TIPO=VENDA -> RESALE). Between 30% and 70% (with enough events to be meaningful) is genuinely conflicting evidence -> UNKNOWN, never guessed either way. RECEITA_INGREDIENTES/PROD_COZINHA-based evidence (a direct recipe-table linkage, not a statistical ratio) is untouched. agent/src/gti_fiscal_agent/firebird_reader.py now counts COMPOSICAO vs other C000032 rows per product (read_product_evidence: single indexed query; read_evidence: one grouped query for the whole catalog) into a new ev["composed_ratio"] key, alongside the existing "composed" set (kept for other, non-role consumers like coverage.py). central/app/enrichment/api.py's sanitize_product_evidence and fiscal_completion.py's empty-ev fallback both carry the new key through. New tests (central/tests/test_field_contract.py) cover the 5 real cases named in the mission: the beer (1 vs 19, 5% - discarded), a second beer (3 vs 4, 42.9% - MIXED/UNKNOWN), the liqueur (34 vs 22, 60.7% - still MIXED/UNKNOWN, below the 70% bar), the juice with PROD_COZINHA=1+RECEITA_INGREDIENTES (unaffected, stays OWN_PRODUCTION), and the tilapia/corvina fish (no composed evidence at all, stays RESALE) - plus a strong 90% majority case and an insufficient-sample (<5 events) case. A new end-to-end test in agent/tests/test_cfop_ippt_auto_apply.py (committed separately, see the feature/cfop-ippt-write branch) proves the weak-evidence beer never reaches select_candidates as a write candidate, independent of any write flag. Re-running the dry-run over Agent 2's real database (GTI.FDB, 527 products) after this fix: AUTO_FILL_SAFE CFOP/IPPT cells drop from 12 to 3 (the two fish CFOP fills + the juice's IPPT, all with strong, non-statistical evidence) - docs/runs/GTI_dryrun_universe_summary_after_A1.json. This corrects the Missão 3 report's 12/9(downgraded)/4(sustained) manual judgment call with a deterministic, tested engine rule. No schema/migration change. Zero Firebird writes (read-only analysis only). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -221,6 +221,47 @@ def test_role_inference_is_evidence_based_and_conflicts_stay_unknown():
|
||||
assert infer_role(product(PROD_COZINHA=1), {**EV, "recipes": {"000001"}}, Context([]), "000001", "1")[0] == "OWN_PRODUCTION"
|
||||
|
||||
|
||||
def test_composed_evidence_needs_a_dominant_majority_never_a_single_occurrence():
|
||||
"""Missão 4 / Bloco A.1: C000032.TIPO=COMPOSICAO must be a deterministic MAJORITY of that
|
||||
product's own movement history to prove OWN_PRODUCTION - real cases audited from Agent 2 (2026-09-28)."""
|
||||
def ev_with(composed, other):
|
||||
return {**EV, "composed_ratio": {"000001": {"composed": composed, "other": other}}}
|
||||
|
||||
# A cerveja: 1 composição vs 19 vendas (5%) - minoria clara, evidência descartada -> cai para o
|
||||
# papel padrão (aqui sem TIPO=VENDA/docs no fixture, então UNKNOWN por falta de outra evidência;
|
||||
# o ponto do teste é que NUNCA vira OWN_PRODUCTION).
|
||||
assert infer_role(product(), ev_with(1, 19), Context([]), "000001", "1")[0] != "OWN_PRODUCTION"
|
||||
r = infer_role(product(TIPO="VENDA"), {**ev_with(1, 19), "stock_in": {"000001": True}}, Context([]), "000001", "1")
|
||||
assert r[0] == "RESALE" # a cerveja, com evidência de venda, é revenda - nunca produção própria
|
||||
|
||||
# O refrigerante/2ª cerveja: 3 composições vs 4 vendas (42,9%) - amostra pequena mas na zona
|
||||
# ambígua -> evidência CONFLITANTE, nunca adivinhada para nenhum dos dois lados.
|
||||
r = infer_role(product(TIPO="VENDA"), {**ev_with(3, 4), "stock_in": {"000001": True}}, Context([]), "000001", "1")
|
||||
assert r[0] == "UNKNOWN" and "MIXED" in r[1][0]
|
||||
|
||||
# O licor: 34 composições vs 22 vendas (60,7%) - maioria real, mas abaixo do limiar dominante
|
||||
# (70%) -> também CONFLITANTE, nunca OWN_PRODUCTION por uma maioria fraca.
|
||||
r = infer_role(product(TIPO="VENDA"), {**ev_with(34, 22), "stock_in": {"000001": True}}, Context([]), "000001", "1")
|
||||
assert r[0] == "UNKNOWN" and "MIXED" in r[1][0]
|
||||
|
||||
# Suco com PROD_COZINHA=1 + RECEITA_INGREDIENTES: evidência forte e DIRETA (não é composição
|
||||
# estatística) - continua OWN_PRODUCTION, o limiar de composição não se aplica a ela.
|
||||
assert infer_role(product(PROD_COZINHA=1), {**EV, "recipes": {"000001"}}, Context([]), "000001", "1")[0] == "OWN_PRODUCTION"
|
||||
|
||||
# Uma maioria REAL e forte (90%, amostra suficiente) ainda prova produção própria.
|
||||
r = infer_role(product(), ev_with(90, 10), Context([]), "000001", "1")
|
||||
assert r[0] == "OWN_PRODUCTION" and r[1] == ["C000032.TIPO=COMPOSICAO"]
|
||||
|
||||
# Amostra insuficiente (< 5 eventos), mesmo 100% composição: não confia num histórico tão raso.
|
||||
r = infer_role(product(TIPO="VENDA"), {**ev_with(2, 0), "stock_in": {"000001": True}}, Context([]), "000001", "1")
|
||||
assert r[0] == "RESALE" # a evidência de composição é ignorada, cai para a evidência de venda real
|
||||
|
||||
# Tilápia/corvina: sem NENHUMA evidência de composição - continua revenda normalmente (o fix
|
||||
# não pode quebrar o caso que já funcionava).
|
||||
r = infer_role(product(TIPO="VENDA", CFOP=None), {**EV, "purchases": {"000001": [{}]}}, Context([]), "000001", "1")
|
||||
assert r[0] == "RESALE"
|
||||
|
||||
|
||||
# ---------------- executor guards ----------------
|
||||
def load_executor():
|
||||
spec = importlib.util.spec_from_file_location("fwe_local", ROOT / "scripts" / "firebird_write_executor.py")
|
||||
|
||||
Reference in New Issue
Block a user