666312dfbdsettled the catalog client on the dedicated LibreTranslate. One engine cannot be compared against itself, so the direct path had no way to improve on a language it translates badly: of the 2854 entries of the `ko` catalog it produced 1 translated entry and 2704 `structure` rejections. The client now asks the gateway, which fronts several engines and keeps a history of which one survives a given signature. Protected spans are masked as `<x id="N"></x>` before the request and declared per item, so the gateway can reject an answer that altered them instead of passing it on. `_reask` then retries each rejected entry with the engine that produced it in `exclude`, which reaches a different backend rather than the same one. The same catalog measured 17 translated entries with 2649 rejections once the gateway checked the markup, and 331 with 796 once the retry could exclude. The full picture a rejection needs is only available in the client: 2431 of those 2649 structure rejections involve markup the masking hides from the gateway, so the engine-switching decision belongs on this side of the call. `server()` reuses a gateway that already answers and otherwise deploys the bundle the gateway needs. It does not reintroduce the throwaway container666312dfbdremoved: there is no `docker run` here, the roles deploy through the normal path and their models still come from the role images built through the package cache. `redeploy=true` is offered because the reuse check only asks whether a gateway answers, which cannot see that the gateway's own code changed since it started. A run refuses outright while a deploy is in progress, because both touch the same container stack, and leaves the deploy alone. `deployed()` additionally requires a non-empty catalogue, so a gateway that answers while serving no pairs counts as not ready rather than as a backend that refuses everything. `probe(url)` is split out of `deployed(root)` so one readiness check serves a single engine and the gateway alike. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
utils/env 📐
This directory hosts the Python implementation behind make dotenv.
For the general rules that govern READMEs in code directories see documentation.md.
For the per-variable defaults that this code consumes see default.env.
Purpose 🎯
The package converts the committed default.env plus a small runtime context into the gitignored .env at the repo root.
Every module here belongs to one of four roles: parse, resolve, orchestrate, write.
Structure 🗂️
| File | Role |
|---|---|
parser.py |
Read default.env into (values, comments) tuples. |
runtime.py |
Resolve host-context lookups (disk, RAM, hostname, GHA/Act flags, /proc/version, helper-script invocation). |
builder.py |
Thin orchestrator that defines EnvBuilder, BuildContext, and build_env(). Walks the handler registry. |
writer.py |
Serialise an EnvBuilder into a docker-compose-compatible .env on disk. |
handlers/ |
One module per dynamically-computed variable. See handlers README. |
The CLI entry point lives at __main__.py under cli/meta/env/.
Shell consumers source load.sh under scripts/meta/env/.
File Naming 🏷️
- Module names MUST stay lowercase snake_case.
- Pure passthrough or simple parser/writer helpers MUST live directly under
utils/env/. - Per-variable computation MUST live under
handlers/(see handlers README for the naming convention used there). - Test files live in
tests/unit/python/utils/env/and mirror the module name with atest_prefix.
Machine Facts vs Run Facts 🧭
The writer a handler picks decides how long a value lives. Both kinds are registered in default.env and both are visible to the lints; they differ only in whether the generated .env remembers what the environment said.
| Writer | Meaning | On the next make dotenv |
|---|---|---|
eb.setdefault |
machine fact | a non-empty value in the process env is adopted and written back |
eb.set |
run fact | recomputed from BuildContext; whatever the process env said is discarded |
setdefault is the operator-override contract: exporting a key changes it and the change persists. That persistence is why it MUST NOT carry a value an environment declares for a single run. Makefile exports BASH_ENV, so every make recipe sources .env — a value adopted once is re-exported into every later recipe and into every container that mounts the repo, and a per-run decision has silently become a property of the machine.
A key that some environment declares per run therefore MUST be written by a dedicated handler through eb.set. The exporting shell still wins for its own run, because load.sh preserves non-empty caller values when it sources .env; the file keeps the derived default. INFINITO_RUNNING_ON_GITHUB and INFINITO_CACHE_STACK are written this way. Note the consequence: for those keys .env and the running shell can legitimately disagree, and .env is not the authority.
Import Rules 🔗
parser.py,runtime.py, andwriter.pyMUST NOT import frombuilder.pyorhandlers/.builder.pyMUST NOT import individual handler modules; it MUST go through theORDERED_HANDLERSlist exposed byhandlers/__init__.py.- Handler modules MUST NOT import each other (see handlers README).