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:
gti
2026-09-23 07:23:13 -03:00
co-authored by Claude Opus 5.5
parent 6bcfac6497
commit d33149f5ee
6 changed files with 52 additions and 9 deletions
+17 -1
View File
@@ -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.
+4
View File
@@ -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
+1 -1
View File
@@ -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)
+4 -3
View File
@@ -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": [
{ {
+13 -4
View File
@@ -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)}))
+13
View File
@@ -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")