From fbff392ec931f26e31e6f15b0ed47cb580055654 Mon Sep 17 00:00:00 2001 From: gti Date: Sun, 27 Sep 2026 10:33:45 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20KB=20dashboard=20search=20was=20applying?= =?UTF-8?q?=20the=20strict=20fiscal=20name-match=20rule=20(used=20to=20aut?= =?UTF-8?q?horize=20automatic=20writes)=20to=20human=20browsing,=20so=20a?= =?UTF-8?q?=20natural=20query=20(e.g.=20missing=20the=20unit=20word)=20fou?= =?UTF-8?q?nd=20nothing;=20now=20a=20separate,=20lenient,=20global,=20pagi?= =?UTF-8?q?nated=20server-side=20search=20=E2=80=94=20filter=20over=20the?= =?UTF-8?q?=20whole=20KB,=20then=20count/order/paginate.=201.3.6=20(Hub=20?= =?UTF-8?q?only)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 --- agent/pyproject.toml | 2 +- agent/scripts/run_agent_service.py | 6 +- agent/src/gti_fiscal_agent/preflight.py | 2 +- central/app/enrichment/observability.py | 50 +++++++--- central/app/version.py | 2 +- central/app/web.py | 4 +- central/pyproject.toml | 2 +- central/templates/dashboard.html | 6 +- central/tests/test_health.py | 2 +- central/tests/test_kb_search.py | 98 +++++++++++++++++++ deploy/agent-windows/agent.env.template | 2 +- deploy/agent-windows/gti-agent.ps1 | 2 +- .../pilot-construcao.env.example | 2 +- .../profiles/mmmodas.env.example | 2 +- .../agent-windows/profiles/rincao.env.example | 2 +- .../agent-windows/profiles/uniao.env.example | 2 +- deploy/hub/.env.example | 2 +- deploy/hub/docker-compose.prod.yml | 2 +- docs/runs/sandbox_1_3_5.json | 14 +++ release/RELEASE_MANIFEST.json | 8 +- release/VERSION | 2 +- tests/test_release_candidate.py | 4 +- 22 files changed, 180 insertions(+), 38 deletions(-) create mode 100644 central/tests/test_kb_search.py create mode 100644 docs/runs/sandbox_1_3_5.json diff --git a/agent/pyproject.toml b/agent/pyproject.toml index a529120..6d51e32 100644 --- a/agent/pyproject.toml +++ b/agent/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gti-fiscal-agent" -version = "1.3.5" +version = "1.3.6" requires-python = ">=3.10" dependencies = ["httpx>=0.27,<1"] diff --git a/agent/scripts/run_agent_service.py b/agent/scripts/run_agent_service.py index c09b4c0..c79a338 100644 --- a/agent/scripts/run_agent_service.py +++ b/agent/scripts/run_agent_service.py @@ -107,7 +107,7 @@ class Config: self.poll_interval_seconds = float(env.get("POLL_INTERVAL_SECONDS", "5")) # RTC auto-apply is a SEPARATE switch (default OFF): FIREBIRD_WRITES alone never lets the Agent fill cClassTrib. self.rtc_auto_apply_max_per_job = int(env.get("RTC_AUTO_APPLY_MAX_PER_JOB", str(_rtc_apply.DEFAULT_MAX_PER_JOB))) - self.agent_version = env.get("AGENT_VERSION", "1.3.5") + self.agent_version = env.get("AGENT_VERSION", "1.3.6") self.schema_fingerprint = env.get("SCHEMA_FINGERPRINT", REFERENCE_SCHEMA_V1) self.state_db = env.get("AGENT_STATE_DB", str(AGENT_ROOT / "agent_state.sqlite")) # Pilot write authorization: both must be explicitly set by the installer for @@ -349,7 +349,7 @@ def execute_job(job, config, executor, state, env): "fiscal_context": fiscal_context, "rtc_auto_apply": rtc_apply, "fiscal_mode": getattr(config, "fiscal_mode", None), - "release_version": env.get("AGENT_VERSION") or "1.3.5", + "release_version": env.get("AGENT_VERSION") or "1.3.6", "products_total": len(products), "products_changed": len(to_process), "products_processed": summary["processed"], @@ -520,7 +520,7 @@ def run(config, env, *, max_iterations=None, stopper=None): def enforce_startup_preflight(env): """Full read-only preflight at startup (Hub, auth, Firebird, capabilities, company context, guards). Fail closed.""" from gti_fiscal_agent.preflight import run_preflight - report = run_preflight(env, agent_version=env.get("AGENT_VERSION") or "1.3.5") + report = run_preflight(env, agent_version=env.get("AGENT_VERSION") or "1.3.6") logger.info("startup preflight: %s", json.dumps({k: report[k] for k in ("PREFLIGHT", "blockers", "fiscal_mode", "effective_firebird_writes")}, ensure_ascii=False)) print(json.dumps({"PREFLIGHT": report["PREFLIGHT"], "blockers": report["blockers"]}, ensure_ascii=False)) if report["PREFLIGHT"] != "PASS": diff --git a/agent/src/gti_fiscal_agent/preflight.py b/agent/src/gti_fiscal_agent/preflight.py index d3edba0..2d208e8 100644 --- a/agent/src/gti_fiscal_agent/preflight.py +++ b/agent/src/gti_fiscal_agent/preflight.py @@ -9,7 +9,7 @@ import os import sqlite3 from pathlib import Path -AGENT_VERSION_DEFAULT = "1.3.5" +AGENT_VERSION_DEFAULT = "1.3.6" REQUIRED_ENV = ("GTI_HUB_URL", "AGENT_TOKEN", "FIREBIRD_DATABASE_PATH", "FB_USER", "FB_PASSWORD") REQUIRED_CAPABILITIES = ("supports_ncm", "supports_ipi", "supports_cest") FISCAL_MODES = ("DRY_RUN", "AUTO_APPLY") diff --git a/central/app/enrichment/observability.py b/central/app/enrichment/observability.py index 0feedcd..039a45e 100644 --- a/central/app/enrichment/observability.py +++ b/central/app/enrichment/observability.py @@ -100,23 +100,50 @@ def _candidate_cards(session, products) -> list[dict]: return out -def kb_search(session, query: str, limit: int = 6) -> list[dict]: - from ..models import KbProduct - from . import kb as kbm +def kb_search_ids(session, query: str) -> list[int]: + """GLOBAL, server-side match over the whole KB (never limited to a rendered page): GTIN exact, or every normalized query word present somewhere among the + product's known names (any alias row OR its own normalized_description - words can come from different aliases of the same product). Deliberately lenient + (a human exploring the KB, e.g. "COCA COLA LATA 350" without the unit, must still find "COCA-COLA ORIGINAL LATA 350 ML"); this is DIFFERENT from + `kb.alias_match`, the strict rule that authorizes an automatic fiscal write, which stays untouched.""" + import re + + from ..models import KbKnowledge, KbProduct + from . import kb as kbm, textnorm q = (query or "").strip() if not q: return [] digits = "".join(ch for ch in q if ch.isdigit()) - products = [] - if len(digits) >= 8 and len(digits) == len(q.replace(" ", "")): + if len(digits) >= 8 and re.fullmatch(r"[\d\s.\-]+", q): p = kbm.find_by_gtin(session, digits.zfill(14)) - products = [p] if p else [] - else: - products = kbm.name_candidates(session, q)[:limit] - return _candidate_cards(session, products) + return [p.id] if p else [] + tokens = [t for t in textnorm.tokens(q) if t not in textnorm.MARKETING] + if not tokens: + return [] + ids: set[int] | None = None + for token in tokens: # SQL ILIKE narrows the candidates (portable: Postgres in production, SQLite in tests); the exact whole-word + like = f"%{token}%" # check happens in Python so "ML" never matches inside "SIMPLES", etc. + matched = {pid for pid, val in session.execute(select(KbKnowledge.kb_product_id, KbKnowledge.value).where(KbKnowledge.knowledge_key == "alias", KbKnowledge.value.ilike(like))) if token in (val or "").split()} + matched |= {pid for pid, val in session.execute(select(KbProduct.id, KbProduct.normalized_description).where(KbProduct.normalized_description.ilike(like))) if token in (val or "").split()} + ids = matched if ids is None else (ids & matched) + if not ids: + return [] + return sorted(ids) -def kb_summary(session, query: str | None = None) -> dict: +def kb_search(session, query: str, *, page: int = 1, page_size: int = 10) -> dict: + """query/filter GLOBAL over the whole KB -> count -> order -> pagination (never the other way round).""" + from ..models import KbProduct + ids = kb_search_ids(session, query) + total = len(ids) + page = max(1, page) + pages = max(1, -(-total // page_size)) + page = min(page, pages) + page_ids = ids[(page - 1) * page_size: page * page_size] + products = [session.get(KbProduct, i) for i in page_ids] + return {"results": _candidate_cards(session, products), "total": total, "page": page, "pages": pages, "page_size": page_size} + + +def kb_summary(session, query: str | None = None, page: int = 1) -> dict: """Compact Knowledge Base panel: known products, statuses of the NCM knowledge, hits today, and the latest conflicts with their evidence (no dashboard of its own).""" from ..models import KbKnowledge, KbProduct today = datetime.now(timezone.utc).strftime("%Y-%m-%d") @@ -134,4 +161,5 @@ def kb_summary(session, query: str | None = None) -> dict: "candidates": [{"ncm": i.value, "clients": sorted({c for c in (i.evidence or {}).get("clients", [])}), "documents": sum(int(l.get("docs", 0)) for l in ((i.evidence or {}).get("lineages") or {}).values()), "providers": sorted(f for f in ((i.evidence or {}).get("families") or {}) if f.startswith("PROVIDER:"))} for i in items]}) return {"products": products, "verified": by.get("VERIFIED_KB", 0), "corroborated": by.get("CORROBORATED", 0) + by.get("EXTERNAL_CORROBORATED", 0), "observed": by.get("OBSERVED", 0), - "conflicts": by.get("CONFLICT", 0), "stale": by.get("STALE", 0), "hits_today": hits, "recent_conflicts": conflicts, "search": kb_search(session, query) if query else []} + "conflicts": by.get("CONFLICT", 0), "stale": by.get("STALE", 0), "hits_today": hits, "recent_conflicts": conflicts, + "search": kb_search(session, query, page=page) if query else {"results": [], "total": 0, "page": 1, "pages": 1, "page_size": 10}} diff --git a/central/app/version.py b/central/app/version.py index 63781c1..abe7a7f 100644 --- a/central/app/version.py +++ b/central/app/version.py @@ -4,5 +4,5 @@ kept identical to this) and what an AgentRelease row's `version` column holds once published — never a second, independent version number. Fiscal semantics are versioned separately (ENGINE_VERSION/DECISION_VERSION in classification.py, DECISION_ENGINE_VERSION etc.) and are NOT changed by a release bump.""" -GTI_FISCAL_VERSION = "1.3.5" +GTI_FISCAL_VERSION = "1.3.6" API_COMPAT_MAJOR_MINOR = "1.0" # an Agent 1.0.x/1.1.x talks to any Hub 1.0.x diff --git a/central/app/web.py b/central/app/web.py index b6c29e2..f10f663 100644 --- a/central/app/web.py +++ b/central/app/web.py @@ -159,10 +159,10 @@ def router(session_factory): return {key: items[0] for key, items in parse_qs((await request.body()).decode()).items() if items} @routes.get("/", response_class=HTMLResponse) - def dashboard(request: Request, kb_q: str | None = None): + def dashboard(request: Request, kb_q: str | None = None, kb_page: int = 1): session, auth_data, redirect = page_auth(request) if redirect: return redirect - try: return render(request, "dashboard.html", {"summary": dashboard_summary(session), "auth": auth_data, "kb_q": kb_q or "", "is_admin": _is_privileged(auth_data[1]), "csrf_token": request.cookies.get("gti_csrf", ""), "sources": provider_sources(session), "official": official_sources(session), "enrich_metrics": coverage_metrics(session), "kb": kb_summary(session, kb_q)}) + try: return render(request, "dashboard.html", {"summary": dashboard_summary(session), "auth": auth_data, "kb_q": kb_q or "", "is_admin": _is_privileged(auth_data[1]), "csrf_token": request.cookies.get("gti_csrf", ""), "sources": provider_sources(session), "official": official_sources(session), "enrich_metrics": coverage_metrics(session), "kb": kb_summary(session, kb_q, kb_page)}) finally: session.close() @routes.get("/agents", response_class=HTMLResponse) diff --git a/central/pyproject.toml b/central/pyproject.toml index 24b8bc3..7eb38e0 100644 --- a/central/pyproject.toml +++ b/central/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gti-fiscal-hub" -version = "1.3.5" +version = "1.3.6" requires-python = ">=3.10" dependencies = [ "fastapi>=0.115,<1", diff --git a/central/templates/dashboard.html b/central/templates/dashboard.html index 9e85ea6..1742eab 100644 --- a/central/templates/dashboard.html +++ b/central/templates/dashboard.html @@ -30,10 +30,12 @@

Base de conhecimento de produtos

Evidência local, histórica e externa; nada vira verdade fiscal sem regra de promoção.

{{ kb.products }}Produtos conhecidos
{{ kb.verified }}GTINs verificados
{{ kb.corroborated }}Corroborados
{{ kb.conflicts }}Conflitos
{{ kb.hits_today }}Resolvidos pela base hoje
-
-{% for c in kb.search %}
{{ c.gtin }} {{ c.description }} · {{ c.status }} +
{% if kb_q %}{{ kb.search.total }} resultado(s) em toda a base{% if kb.search.pages > 1 %} · página {{ kb.search.page }}/{{ kb.search.pages }}{% endif %}{% endif %}
+{% for c in kb.search.results %}
{{ c.gtin }} {{ c.description }} · {{ c.status }} {% for x in c.candidates %}NCM {{ x.ncm }} — {{ x.official_text or "fora do dataset oficial vigente" }} ({{ x.clients|length }} cliente(s), {{ x.documents }} doc.{% if x.providers %}, {{ x.providers|join("/") }}{% endif %}{% if x.attested %}, decidido por {{ x.attested }}{% endif %}){% if is_admin and x.official_text and c.status != "VERIFIED_KB" %}
{% endif %}
{% endfor %}
{% endfor %} +{% if kb_q and kb.search.total == 0 %}

Nenhum produto encontrado para "{{ kb_q }}".

{% endif %} +{% if kb.search.pages > 1 %}{% endif %} {% for c in kb.recent_conflicts %}
{{ c.gtin }} {{ c.description }}NCMs divergentes, nada é preenchido: {% for x in c.candidates %}{{ x.ncm }} ({{ x.clients|length }} cliente(s), {{ x.documents }} doc.{% if x.providers %}, {{ x.providers|join("/") }}{% endif %}){% if not loop.last %} × {% endif %}{% endfor %}
{% endfor %}

Enriquecimento de produtos novos

Cobertura medida sobre os produtos novos acompanhados (o catálogo histórico não entra).

{% if enrich_metrics.tracked %}
{% for key,label in [("GTIN_VALID_RATE","Com GTIN válido"),("KB_HIT_RATE","Resolvidos pela base"),("PRODUCT_FOUND_RATE","Produto identificado"),("NCM_AUTO_FILL_SAFE_RATE","NCM preenchível com segurança"),("AMBIGUOUS_RATE","Ambíguos"),("CONFLICT_RATE","Em conflito")] %}
{{ enrich_metrics[key] if enrich_metrics[key] is not none else "—" }}{% if enrich_metrics[key] is not none %}%{% endif %}{{ label }}
{% endfor %}
{% else %}

Nenhum produto novo acompanhado ainda.

{% endif %}
diff --git a/central/tests/test_health.py b/central/tests/test_health.py index b77cafc..f5abe73 100644 --- a/central/tests/test_health.py +++ b/central/tests/test_health.py @@ -17,7 +17,7 @@ def test_health_is_unavailable_when_database_is_down(): def test_version_endpoint_reports_release_and_fiscal_versions(): body = TestClient(create_app(database_probe=lambda: True)).get("/version").json() - assert body["version"] == "1.3.5" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"] + assert body["version"] == "1.3.6" and body["api_compat"] == "1.0" and body["engine_version"] and body["decision_version"] def test_production_refuses_the_dev_token(monkeypatch): diff --git a/central/tests/test_kb_search.py b/central/tests/test_kb_search.py new file mode 100644 index 0000000..6178a4f --- /dev/null +++ b/central/tests/test_kb_search.py @@ -0,0 +1,98 @@ +"""KB search bug fix: the search must be GLOBAL server-side (filter -> count -> order -> paginate), lenient for humans (distinct from the strict +fiscal `kb.alias_match`), and find a product regardless of which page it would fall on. Never touches the fiscal auto-fill rule.""" +from sqlalchemy import select + +from central.app.enrichment import kb +from central.app.enrichment.observability import kb_search, kb_search_ids +from central.tests.test_control_panel_pages import _panel_client +from central.tests.test_product_enrichment_resolver import env # noqa: F401 +from central.tests.test_rtc_security import _become, _post + + +def seed_product(session, gtin, aliases, ncm=None, client="A"): + p = kb.upsert_product(session, gtin, aliases[0]) + for a in aliases[1:]: + kb.add_alias(session, p, a) + if ncm: + kb.add_evidence(session, p, "ncm", ncm, "x", family=kb.CLIENT_FAMILY, client_label=client, lineage={"docs": 20, "years": {"2024": 20}}) + session.commit() + return p + + +def gtin(body12): + total = sum(int(d) * (3 if i % 2 == 0 else 1) for i, d in enumerate(reversed(body12))) + return "0" + body12 + str((10 - total % 10) % 10) + + +def test_search_is_global_not_limited_to_a_rendered_page(env): + target = seed_product(env, gtin("789100050001"), ["COCA-COLA ORIGINAL LATA 350 ML"], ncm="22021000") + for i in range(30): # unrelated noise: if search were page-limited, the target could fall off the visible slice + seed_product(env, gtin(f"78920005{i:04d}"), [f"PRODUTO IRRELEVANTE {i} CAIXA 1 UN"]) + ids = kb_search_ids(env, "COCA COLA LATA 350") # missing the unit "ML": would fail the strict fiscal alias_match + assert target.id in ids + + +def test_search_finds_product_on_first_middle_and_last_page(env): + target = seed_product(env, gtin("789100050002"), ["COCA-COLA ORIGINAL LATA 350 ML"]) + for i in range(25): + seed_product(env, gtin(f"78920006{i:04d}"), [f"COCA-COLA ORIGINAL LATA 350 ML VARIANTE {i}"]) + for page in (1, 2, 3): + res = kb_search(env, "COCA COLA LATA 350", page=page, page_size=10) + assert res["total"] == 26 and res["pages"] == 3 + found_on_last_page = kb_search(env, "COCA COLA LATA 350", page=3, page_size=10) + assert len(found_on_last_page["results"]) >= 1 + + +def test_search_by_gtin_exact(env): + key = gtin("789100050003") + seed_product(env, key, ["COCA-COLA ORIGINAL LATA 350 ML"]) + body = key[1:] + assert kb_search_ids(env, body) and kb_search_ids(env, body)[0] == kb_search_ids(env, key)[0] + assert kb_search_ids(env, "0000000000000") == [] + + +def test_search_by_name_with_and_without_hyphen_and_accents(env): + p = seed_product(env, gtin("789100050004"), ["ACAI ORIGINAL POLPA 100 G"]) + for query in ("ACAI POLPA 100", "AÇAÍ POLPA 100", "acai polpa 100"): + assert p.id in kb_search_ids(env, query), query + + +def test_search_zero_results_for_unknown_product(env): + seed_product(env, gtin("789100050005"), ["FEIJAO CARIOCA 1 KG"]) + assert kb_search_ids(env, "PRODUTO QUE NAO EXISTE NA BASE") == [] + + +def test_search_before_pagination_semantics(env): + """Filter runs over the WHOLE table, THEN the result is paginated - not the other way round.""" + matches = [seed_product(env, gtin(f"78910006{i:04d}"), [f"SABONETE LIQUIDO ERVA DOCE {i} 200 ML"]) for i in range(15)] + for i in range(40): + seed_product(env, gtin(f"78910007{i:04d}"), [f"OUTRO PRODUTO {i} QUALQUER"]) + res = kb_search(env, "SABONETE LIQUIDO ERVA DOCE", page=2, page_size=10) + assert res["total"] == 15 and res["page"] == 2 and len(res["results"]) == 5 + found_ids = {r["gtin_key"] for r in kb_search(env, "SABONETE LIQUIDO ERVA DOCE", page=1, page_size=10)["results"]} | {r["gtin_key"] for r in res["results"]} + assert found_ids == {p.gtin for p in matches} + + +def test_conflict_candidates_all_visible_and_none_auto_confirmed(env): + seed_product(env, gtin("789100050006"), ["COCA COLA LATA 350 ML"], ncm="22030000", client="RINCAO") + p2 = seed_product(env, gtin("789100050006"), ["REFRIGERANTE COCA COLA LATA 350 ML"], ncm="22021000", client="UNIAO") + # both aliases resolve to the SAME kb product (same GTIN key), so seeding into it twice just adds evidence + ids = kb_search_ids(env, "COCA COLA LATA 350") + assert p2.id in ids + + +def test_admin_attest_button_authorization_end_to_end(): + client, agent_id, _ = _panel_client() + with client._session_factory() as s: + from central.tests.test_enrichment_history import staging + from central.tests.test_enrichment_history import run as seed_history + from central.tests.test_product_enrichment_resolver import seed as seed_ncm_dataset + seed_ncm_dataset(s) + seed_history(s, [staging("RINCAO", "7891000500071", "19059090", years={2024: 900, 2025: 900}, desc="COCA LATA 350 ML"), + staging("UNIAO", "7891000500071", "22021000", years={2024: 20, 2025: 13}, desc="REFRIGERANTE COCA COLA LATA 350 ML")]) + _become(client, "PADRAO") + page = client.get("/?kb_q=COCA COLA LATA 350").text + assert "Confirmar este NCM" not in page # button hidden for non-admin + _become(client, "ADMIN") + page = client.get("/?kb_q=COCA COLA LATA 350").text + assert "Confirmar este NCM" in page and "22021000" in page and "19059090" in page diff --git a/deploy/agent-windows/agent.env.template b/deploy/agent-windows/agent.env.template index 9845763..acc53de 100644 --- a/deploy/agent-windows/agent.env.template +++ b/deploy/agent-windows/agent.env.template @@ -19,4 +19,4 @@ FB_PASSWORD= FISCAL_MODE=DRY_RUN FIREBIRD_WRITES=0 POLL_INTERVAL_SECONDS=5 -AGENT_VERSION=1.3.5 +AGENT_VERSION=1.3.6 diff --git a/deploy/agent-windows/gti-agent.ps1 b/deploy/agent-windows/gti-agent.ps1 index 07b697d..5cecf20 100644 --- a/deploy/agent-windows/gti-agent.ps1 +++ b/deploy/agent-windows/gti-agent.ps1 @@ -164,7 +164,7 @@ switch ($Command) { "GTI_HUB_URL=$hub", "AGENT_TOKEN=$token", "AGENT_ID=$agentId", "COMPANY_CODE=$code", "COMPANY_CNPJ=$cnpj", "COMPANY_CRT_SOURCE=$crtSource", "FIREBIRD_DATABASE_PATH=$db", "FB_USER=$fbUser", "FB_PASSWORD=$fbPass", - 'FISCAL_MODE=DRY_RUN', 'FIREBIRD_WRITES=0', 'POLL_INTERVAL_SECONDS=5', 'AGENT_VERSION=1.3.5' + 'FISCAL_MODE=DRY_RUN', 'FIREBIRD_WRITES=0', 'POLL_INTERVAL_SECONDS=5', 'AGENT_VERSION=1.3.6' ) Set-Content -Path $EnvFile -Value $lines -Encoding UTF8 } diff --git a/deploy/agent-windows/pilot-construcao.env.example b/deploy/agent-windows/pilot-construcao.env.example index f65d269..66656e6 100644 --- a/deploy/agent-windows/pilot-construcao.env.example +++ b/deploy/agent-windows/pilot-construcao.env.example @@ -19,4 +19,4 @@ FB_PASSWORD= FISCAL_MODE=DRY_RUN FIREBIRD_WRITES=0 POLL_INTERVAL_SECONDS=5 -AGENT_VERSION=1.3.5 +AGENT_VERSION=1.3.6 diff --git a/deploy/agent-windows/profiles/mmmodas.env.example b/deploy/agent-windows/profiles/mmmodas.env.example index 532d132..99af419 100644 --- a/deploy/agent-windows/profiles/mmmodas.env.example +++ b/deploy/agent-windows/profiles/mmmodas.env.example @@ -19,4 +19,4 @@ FB_PASSWORD= FISCAL_MODE=DRY_RUN FIREBIRD_WRITES=0 POLL_INTERVAL_SECONDS=5 -AGENT_VERSION=1.3.5 +AGENT_VERSION=1.3.6 diff --git a/deploy/agent-windows/profiles/rincao.env.example b/deploy/agent-windows/profiles/rincao.env.example index d38de38..ded70b4 100644 --- a/deploy/agent-windows/profiles/rincao.env.example +++ b/deploy/agent-windows/profiles/rincao.env.example @@ -19,4 +19,4 @@ FB_PASSWORD= FISCAL_MODE=DRY_RUN FIREBIRD_WRITES=0 POLL_INTERVAL_SECONDS=5 -AGENT_VERSION=1.3.5 +AGENT_VERSION=1.3.6 diff --git a/deploy/agent-windows/profiles/uniao.env.example b/deploy/agent-windows/profiles/uniao.env.example index fd44620..2987742 100644 --- a/deploy/agent-windows/profiles/uniao.env.example +++ b/deploy/agent-windows/profiles/uniao.env.example @@ -19,4 +19,4 @@ FB_PASSWORD= FISCAL_MODE=DRY_RUN FIREBIRD_WRITES=0 POLL_INTERVAL_SECONDS=5 -AGENT_VERSION=1.3.5 +AGENT_VERSION=1.3.6 diff --git a/deploy/hub/.env.example b/deploy/hub/.env.example index fe0474d..b301ce0 100644 --- a/deploy/hub/.env.example +++ b/deploy/hub/.env.example @@ -1,5 +1,5 @@ # GTI Fiscal Hub — production environment (copy to .env, never commit .env) -GTI_FISCAL_VERSION=1.3.5 +GTI_FISCAL_VERSION=1.3.6 # REQUIRED. URL-safe secret (used inside a connection URL): openssl rand -hex 24 POSTGRES_PASSWORD= diff --git a/deploy/hub/docker-compose.prod.yml b/deploy/hub/docker-compose.prod.yml index b56b7c1..e95cf70 100644 --- a/deploy/hub/docker-compose.prod.yml +++ b/deploy/hub/docker-compose.prod.yml @@ -36,7 +36,7 @@ services: build: context: ../.. dockerfile: central/Dockerfile - image: gti-fiscal-hub:${GTI_FISCAL_VERSION:-1.3.5} + image: gti-fiscal-hub:${GTI_FISCAL_VERSION:-1.3.6} restart: unless-stopped depends_on: postgres: diff --git a/docs/runs/sandbox_1_3_5.json b/docs/runs/sandbox_1_3_5.json new file mode 100644 index 0000000..3346e2d --- /dev/null +++ b/docs/runs/sandbox_1_3_5.json @@ -0,0 +1,14 @@ +{ + "UPGRADE_SHA_MATCHES_PUBLISHED": true, + "VERSION_1_3_5": true, + "IDENTITY_PRESERVED": true, + "SERVICE_RUNNING": true, + "NAME_ONLY_WRITE_AFTER_UPGRADE": true, + "hub_row": "ENRIQUECIDO|", + "audit": "CLASSIFICACAO_FISCAL:00000000>22021000:APPLIED", + "REBOOT_SAME_IDENTITY": true, + "NO_MANUAL_ENV_EDIT": true, + "HEARTBEAT_AFTER_REBOOT": 3, + "NO_DUPLICATE_AGENT": true, + "AFTER_REBOOT_NAME_ONLY_WRITE": true +} \ No newline at end of file diff --git a/release/RELEASE_MANIFEST.json b/release/RELEASE_MANIFEST.json index 63f5bb8..1ab883d 100644 --- a/release/RELEASE_MANIFEST.json +++ b/release/RELEASE_MANIFEST.json @@ -1,14 +1,14 @@ { "artifact": "RELEASE_MANIFEST", - "GTI_FISCAL_VERSION": "1.3.5", + "GTI_FISCAL_VERSION": "1.3.6", "generated_at": "2026-09-25T15:43:40+00:00", "hub": { - "version": "1.3.5", + "version": "1.3.6", "api_compat": "1.0", "engine_version": "0.3.0", "decision_version": "mission-04c-ipi-ex-split" }, - "agent_version": "1.3.5", + "agent_version": "1.3.6", "fiscal_engine": { "ENGINE_VERSION": "0.3.0", "DECISION_VERSION": "mission-04c-ipi-ex-split", @@ -89,5 +89,5 @@ "record_count": 11103 } ], - "note": "1.3.5: Hub + Agent (re-evaluation polling, stale-write guard). Placeholder semantics arrived in 1.3.4." + "note": "1.3.6: Hub only (KB global search fix: server-side, lenient, paginated). No Agent change; the published installer remains GTIFiscalAgent-Setup-1.3.5.exe." } \ No newline at end of file diff --git a/release/VERSION b/release/VERSION index 80e78df..95b25ae 100644 --- a/release/VERSION +++ b/release/VERSION @@ -1 +1 @@ -1.3.5 +1.3.6 diff --git a/tests/test_release_candidate.py b/tests/test_release_candidate.py index 9db798a..a4352a3 100644 --- a/tests/test_release_candidate.py +++ b/tests/test_release_candidate.py @@ -14,11 +14,11 @@ sys.path.insert(0, str(ROOT)) def test_versions_are_coherent_across_hub_agent_and_packages(): from central.app.version import GTI_FISCAL_VERSION v = (ROOT / "release" / "VERSION").read_text(encoding="utf-8").strip() - assert v == GTI_FISCAL_VERSION == "1.3.5" + assert v == GTI_FISCAL_VERSION == "1.3.6" pep = v.replace("-rc", "rc") for pp in ("central/pyproject.toml", "agent/pyproject.toml"): assert f'version = "{pep}"' in (ROOT / pp).read_text(encoding="utf-8") - assert '"AGENT_VERSION", "1.3.5"' in (ROOT / "agent" / "scripts" / "run_agent_service.py").read_text(encoding="utf-8") + assert '"AGENT_VERSION", "1.3.6"' in (ROOT / "agent" / "scripts" / "run_agent_service.py").read_text(encoding="utf-8") manifest = ROOT / "release" / "RELEASE_MANIFEST.json" if manifest.exists(): m = json.loads(manifest.read_text(encoding="utf-8"))