docs: record migration head 0020 in the release manifest and docs
RELEASE_MANIFEST.json still said head 0011 (repo was at 0018, now 0020)
and RELEASE_CANDIDATE_V1.md said 0010. Updated both, plus
AI_PROJECT_STATE.md and PILOT_DEPLOYMENT.md, to head
0020_not_null_timestamps as applied in production.
scripts/release_manifest.py reported the 4-digit file prefix as the head
("0020" instead of "0020_not_null_timestamps") and always read the
applied revision from the dev container. It now asks Alembic for the real
head and revision count, and the Hub container is configurable
(HUB_CONTAINER); the source is recorded in the manifest.
tests/test_release_candidate.py now fails when the manifest head differs
from Alembic's head.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -440,7 +440,7 @@ allowlist and the fiscal rule engines still decide what may actually be written
|
|||||||
Hub `classification_cache` fixed: fingerprint (tenant + input + ENGINE_VERSION + dataset_versions()) is computed first and a HIT returns without running the Classifier (test: `central/tests/test_classification_cache.py`). Live benchmark (100 products): MISS 337 ms/product, HIT 15 ms/product, 100/100 hits, identical results. Fingerprint inputs unchanged, so existing cache entries stay valid and version bumps still invalidate naturally.
|
Hub `classification_cache` fixed: fingerprint (tenant + input + ENGINE_VERSION + dataset_versions()) is computed first and a HIT returns without running the Classifier (test: `central/tests/test_classification_cache.py`). Live benchmark (100 products): MISS 337 ms/product, HIT 15 ms/product, 100/100 hits, identical results. Fingerprint inputs unchanged, so existing cache entries stay valid and version bumps still invalidate naturally.
|
||||||
|
|
||||||
## Release Candidate 1.0.0-rc1 (2026-09-19) — COMPLETE_LOCAL_RC
|
## Release Candidate 1.0.0-rc1 (2026-09-19) — COMPLETE_LOCAL_RC
|
||||||
Frozen: `release/VERSION`, `central/app/version.py` (`GET /version`), `release/RELEASE_MANIFEST.json` (versions + ACTIVE dataset hashes + migrations 0001..0010). See `docs/RELEASE_CANDIDATE_V1.md`.
|
Frozen: `release/VERSION`, `central/app/version.py` (`GET /version`), `release/RELEASE_MANIFEST.json` (versions + ACTIVE dataset hashes + migrations; head `0020_not_null_timestamps` since 2026-09-23). See `docs/RELEASE_CANDIDATE_V1.md`.
|
||||||
Architecture: Hub central (Docker Hub+PostgreSQL, `deploy/hub`) <-HTTPS- Agent Windows (`deploy/agent-windows`, service GTIFiscalAgent) -local read-only-> SGV Firebird. The Hub never accesses Firebird.
|
Architecture: Hub central (Docker Hub+PostgreSQL, `deploy/hub`) <-HTTPS- Agent Windows (`deploy/agent-windows`, service GTIFiscalAgent) -local read-only-> SGV Firebird. The Hub never accesses Firebird.
|
||||||
Agent: `FISCAL_MODE` DRY_RUN|AUTO_APPLY (DRY_RUN = technically no writes), `COMPANY_CODE/COMPANY_CNPJ/COMPANY_CRT_SOURCE(AUTO|EMISSOR|C000004)`, `COMPANY_UF` explicit override only; single preflight `agent/scripts/preflight.py` (also enforced at service start); REPROCESS_ALL job result includes the coverage report + company context (`gti_fiscal_agent/coverage.py`, shared with multi-db tooling); `scripts/prove_zero_write.py` (DML counters + table digests). Packaging: `scripts/build_agent_package.py` (secret-scanned zip). Hub prod: `deploy/hub/docker-compose.prod.yml` (Postgres internal only), backup/restore, dataset seed export/import; `APP_ENV=production` refuses AGENT_DEV_TOKEN.
|
Agent: `FISCAL_MODE` DRY_RUN|AUTO_APPLY (DRY_RUN = technically no writes), `COMPANY_CODE/COMPANY_CNPJ/COMPANY_CRT_SOURCE(AUTO|EMISSOR|C000004)`, `COMPANY_UF` explicit override only; single preflight `agent/scripts/preflight.py` (also enforced at service start); REPROCESS_ALL job result includes the coverage report + company context (`gti_fiscal_agent/coverage.py`, shared with multi-db tooling); `scripts/prove_zero_write.py` (DML counters + table digests). Packaging: `scripts/build_agent_package.py` (secret-scanned zip). Hub prod: `deploy/hub/docker-compose.prod.yml` (Postgres internal only), backup/restore, dataset seed export/import; `APP_ENV=production` refuses AGENT_DEV_TOKEN.
|
||||||
Pilot profile: CONSTRUCAO (`pilot-construcao.env.example`); planned, not installed: MMMODAS/RINCAO/UNIAO with `COMPANY_CRT_SOURCE=EMISSOR`. No real host was accessed: a local simulation ran the Agent (registered agent, DRY_RUN) end-to-end on the CONSTRUCAO copy. Follow-up: memoize `dataset_versions()`.
|
Pilot profile: CONSTRUCAO (`pilot-construcao.env.example`); planned, not installed: MMMODAS/RINCAO/UNIAO with `COMPANY_CRT_SOURCE=EMISSOR`. No real host was accessed: a local simulation ran the Agent (registered agent, DRY_RUN) end-to-end on the CONSTRUCAO copy. Follow-up: memoize `dataset_versions()`.
|
||||||
@@ -476,3 +476,19 @@ context. Retrieval V3 evidence hooks exist but V2 remains active until a full
|
|||||||
untouched holdout replay proves no regression. Human/group questionnaire CLIs,
|
untouched holdout replay proves no regression. Human/group questionnaire CLIs,
|
||||||
guarded rollback preview, local validation, and health report are available.
|
guarded rollback preview, local validation, and health report are available.
|
||||||
See `docs/PRE_AI_LOCAL_COMPLETION_V3_REPORT.md` for measured values.
|
See `docs/PRE_AI_LOCAL_COMPLETION_V3_REPORT.md` for measured values.
|
||||||
|
|
||||||
|
## Production Hub + migration baseline (2026-09-23)
|
||||||
|
|
||||||
|
- Hub + PostgreSQL run on VM 10.0.10.117 (`deploy/hub`, compose project
|
||||||
|
`gti-fiscal-hub`), next to the GTI Gerenciador but isolated (own DB, process,
|
||||||
|
login). TLS via Caddy on 443 only; HTTPS still waits on public DNS.
|
||||||
|
- Migration head: `0020_not_null_timestamps`. An empty database is initialized
|
||||||
|
by `alembic upgrade head` alone — never `stamp`/`create_all`.
|
||||||
|
- `0001` is a frozen explicit baseline (it used to call `create_all()`, which
|
||||||
|
broke every fresh install). `0019` fixes the frozen `created_at` defaults
|
||||||
|
from `0006`; `0020` enforces NOT NULL on 20 defaulted timestamps.
|
||||||
|
- Verified: fresh DB == upgraded real-history DB == production (pg_dump);
|
||||||
|
vs models, 0 differences on nullability/types/PK/FK/uniqueness/checks.
|
||||||
|
Remaining drift is naming, column order, extra DB defaults, redundant indexes.
|
||||||
|
- `deploy/hub/*.sh` are forced to LF (`.gitattributes`); `backup.sh` + restore
|
||||||
|
into a scratch database were exercised on production.
|
||||||
|
|||||||
@@ -130,6 +130,10 @@ Agents with `central/scripts/register_agent.py` (see below).
|
|||||||
- `0019` repairs `agents.created_at/updated_at` and `jobs.created_at`, whose
|
- `0019` repairs `agents.created_at/updated_at` and `jobs.created_at`, whose
|
||||||
defaults `0006` had frozen at the instant it ran (rows created before `0019`
|
defaults `0006` had frozen at the instant it ran (rows created before `0019`
|
||||||
keep that frozen value — their real creation time is not recoverable).
|
keep that frozen value — their real creation time is not recoverable).
|
||||||
|
- `0020` enforces NOT NULL on 20 timestamp columns that always had
|
||||||
|
`DEFAULT now()`. It aborts, changing nothing, if any NULL exists — clean
|
||||||
|
those rows explicitly first. Current head: `0020_not_null_timestamps`
|
||||||
|
(also recorded in `release/RELEASE_MANIFEST.json`).
|
||||||
|
|
||||||
### HTTPS / reverse proxy
|
### HTTPS / reverse proxy
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ The Hub never accesses Firebird and never stores Firebird credentials or paths.
|
|||||||
| GTI_FISCAL_VERSION (Hub, Agent, packages) | 1.0.0-rc1 (`release/VERSION`, `central/app/version.py`, `GET /version`) |
|
| GTI_FISCAL_VERSION (Hub, Agent, packages) | 1.0.0-rc1 (`release/VERSION`, `central/app/version.py`, `GET /version`) |
|
||||||
| fiscal engine (`ENGINE_VERSION`) / decision (`DECISION_VERSION`) | 0.3.0 / mission-04c-ipi-ex-split (unchanged: no fiscal semantics touched) |
|
| fiscal engine (`ENGINE_VERSION`) / decision (`DECISION_VERSION`) | 0.3.0 / mission-04c-ipi-ex-split (unchanged: no fiscal semantics touched) |
|
||||||
| decision engines | fiscal-decision-v1, product-defaults-v1, icms-context-v1 |
|
| decision engines | fiscal-decision-v1, product-defaults-v1, icms-context-v1 |
|
||||||
| migrations | 0001..0010 (head 0010, applied) |
|
| migrations | 0001..0020 (head `0020_not_null_timestamps`, applied in production 2026-09-23; a fresh database is initialized by `alembic upgrade head` alone) |
|
||||||
| datasets | 13 ACTIVE kinds with sha256/record_count in the manifest |
|
| datasets | 13 ACTIVE kinds with sha256/record_count in the manifest |
|
||||||
|
|
||||||
## What RC1 adds (no fiscal rule changes)
|
## What RC1 adds (no fiscal rule changes)
|
||||||
|
|||||||
@@ -17,9 +17,10 @@
|
|||||||
"ICMS_CONTEXT_ENGINE_VERSION": "icms-context-v1"
|
"ICMS_CONTEXT_ENGINE_VERSION": "icms-context-v1"
|
||||||
},
|
},
|
||||||
"migrations": {
|
"migrations": {
|
||||||
"alembic_head_in_repo": "0011",
|
"alembic_head_in_repo": "0020_not_null_timestamps",
|
||||||
"applied_revision": "0011",
|
"applied_revision": "0020_not_null_timestamps",
|
||||||
"count": 11
|
"applied_revision_source": "production Hub 10.0.10.117 (gti-fiscal-hub-fiscal-hub-1), verified 2026-09-23",
|
||||||
|
"count": 20
|
||||||
},
|
},
|
||||||
"datasets_active": [
|
"datasets_active": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Registers together: release version, agent/hub versions, fiscal engine + decisio
|
|||||||
(kind, provider, sha256, record_count) read from the running Hub, alembic head (from the migration files) and the applied revision.
|
(kind, provider, sha256, record_count) read from the running Hub, alembic head (from the migration files) and the applied revision.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -31,10 +32,18 @@ def main():
|
|||||||
from central.app.icms_context import ICMS_CONTEXT_ENGINE_VERSION
|
from central.app.icms_context import ICMS_CONTEXT_ENGINE_VERSION
|
||||||
from central.app.product_defaults import PRODUCT_DEFAULTS_ENGINE_VERSION
|
from central.app.product_defaults import PRODUCT_DEFAULTS_ENGINE_VERSION
|
||||||
version = (ROOT / "release" / "VERSION").read_text(encoding="utf-8").strip()
|
version = (ROOT / "release" / "VERSION").read_text(encoding="utf-8").strip()
|
||||||
heads = sorted(p.name.split("_")[0] for p in (ROOT / "central" / "alembic" / "versions").glob("00*.py"))
|
# Real revision ids (e.g. "0020_not_null_timestamps"), not file-name prefixes:
|
||||||
|
# from 0015 on the id is longer than the 4-digit prefix.
|
||||||
|
from alembic.config import Config
|
||||||
|
from alembic.script import ScriptDirectory
|
||||||
|
alembic_cfg = Config(str(ROOT / "central" / "alembic.ini"))
|
||||||
|
alembic_cfg.set_main_option("script_location", str(ROOT / "central" / "alembic"))
|
||||||
|
script = ScriptDirectory.from_config(alembic_cfg)
|
||||||
|
head, revision_count = script.get_current_head(), len(list(script.walk_revisions()))
|
||||||
|
container = os.getenv("HUB_CONTAINER", "projeto-fiscal-hub-1")
|
||||||
try:
|
try:
|
||||||
applied = subprocess.run(["docker", "exec", "projeto-fiscal-hub-1", "sh", "-c", "cd /app && alembic -c central/alembic.ini current"], capture_output=True, text=True, timeout=60).stdout
|
current = subprocess.run(["docker", "exec", container, "sh", "-c", "cd /app && alembic -c central/alembic.ini current"], capture_output=True, text=True, timeout=60).stdout
|
||||||
applied = re.findall(r"^(\d{4})", applied, re.M)[-1:] or ["unknown"]
|
applied = re.findall(r"^(\d{4}\S*)", current, re.M)[-1:] or ["unknown"]
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
applied = ["unknown"]
|
applied = ["unknown"]
|
||||||
datasets = sorted(({"kind": d["kind"], "sha256": d["sha256"][:16], "record_count": d["record_count"]} for d in get(f"{hub}/v1/datasets/status", token) if d["status"] == "ACTIVE"), key=lambda x: x["kind"])
|
datasets = sorted(({"kind": d["kind"], "sha256": d["sha256"][:16], "record_count": d["record_count"]} for d in get(f"{hub}/v1/datasets/status", token) if d["status"] == "ACTIVE"), key=lambda x: x["kind"])
|
||||||
@@ -42,7 +51,7 @@ def main():
|
|||||||
"hub": get(f"{hub}/version", token), "agent_version": version,
|
"hub": get(f"{hub}/version", token), "agent_version": version,
|
||||||
"fiscal_engine": {"ENGINE_VERSION": ENGINE_VERSION, "DECISION_VERSION": DECISION_VERSION, "DECISION_ENGINE_VERSION": DECISION_ENGINE_VERSION,
|
"fiscal_engine": {"ENGINE_VERSION": ENGINE_VERSION, "DECISION_VERSION": DECISION_VERSION, "DECISION_ENGINE_VERSION": DECISION_ENGINE_VERSION,
|
||||||
"PRODUCT_DEFAULTS_ENGINE_VERSION": PRODUCT_DEFAULTS_ENGINE_VERSION, "ICMS_CONTEXT_ENGINE_VERSION": ICMS_CONTEXT_ENGINE_VERSION},
|
"PRODUCT_DEFAULTS_ENGINE_VERSION": PRODUCT_DEFAULTS_ENGINE_VERSION, "ICMS_CONTEXT_ENGINE_VERSION": ICMS_CONTEXT_ENGINE_VERSION},
|
||||||
"migrations": {"alembic_head_in_repo": heads[-1], "applied_revision": applied[0], "count": len(heads)},
|
"migrations": {"alembic_head_in_repo": head, "applied_revision": applied[0], "applied_revision_source": f"container {container}", "count": revision_count},
|
||||||
"datasets_active": datasets, "note": "A release bump does not change fiscal semantics; engine/decision versions are versioned independently."}
|
"datasets_active": datasets, "note": "A release bump does not change fiscal semantics; engine/decision versions are versioned independently."}
|
||||||
(ROOT / "release" / "RELEASE_MANIFEST.json").write_text(json.dumps(manifest, indent=1), encoding="utf-8")
|
(ROOT / "release" / "RELEASE_MANIFEST.json").write_text(json.dumps(manifest, indent=1), encoding="utf-8")
|
||||||
print(json.dumps({"version": version, "engine": ENGINE_VERSION, "migrations": manifest["migrations"], "datasets": len(datasets)}))
|
print(json.dumps({"version": version, "engine": ENGINE_VERSION, "migrations": manifest["migrations"], "datasets": len(datasets)}))
|
||||||
|
|||||||
@@ -25,6 +25,19 @@ def test_versions_are_coherent_across_hub_agent_and_packages():
|
|||||||
assert m["GTI_FISCAL_VERSION"] == v and m["migrations"]["alembic_head_in_repo"] == m["migrations"]["applied_revision"]
|
assert m["GTI_FISCAL_VERSION"] == v and m["migrations"]["alembic_head_in_repo"] == m["migrations"]["applied_revision"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_manifest_records_the_real_migration_head():
|
||||||
|
# The manifest said "0011" while the repository was already at 0018: the
|
||||||
|
# generator used the 4-digit file prefix and nothing compared it to Alembic.
|
||||||
|
from alembic.config import Config
|
||||||
|
from alembic.script import ScriptDirectory
|
||||||
|
config = Config(str(ROOT / "central" / "alembic.ini"))
|
||||||
|
config.set_main_option("script_location", str(ROOT / "central" / "alembic"))
|
||||||
|
script = ScriptDirectory.from_config(config)
|
||||||
|
migrations = json.loads((ROOT / "release" / "RELEASE_MANIFEST.json").read_text(encoding="utf-8"))["migrations"]
|
||||||
|
assert migrations["alembic_head_in_repo"] == script.get_current_head()
|
||||||
|
assert migrations["count"] == len(list(script.walk_revisions()))
|
||||||
|
|
||||||
|
|
||||||
def test_production_compose_never_exposes_postgres_or_dev_token():
|
def test_production_compose_never_exposes_postgres_or_dev_token():
|
||||||
import yaml
|
import yaml
|
||||||
raw = (ROOT / "deploy" / "hub" / "docker-compose.prod.yml").read_text(encoding="utf-8")
|
raw = (ROOT / "deploy" / "hub" / "docker-compose.prod.yml").read_text(encoding="utf-8")
|
||||||
|
|||||||
Reference in New Issue
Block a user