Imports:
  - Types:
      - version_check_enabled
      - ensure_version_match
    Usages:
      - version-check
    From: goga/version

Usages:
  convention: .goga/usages/conventions.md

Annotations: |
  The `convention` practice is used for:
  - Working with the codebase
  - Organizing the REPL development cycle
  - Debugging and testing
  - Organizing the test infrastructure
  - Understanding the general principles and rules of development and testing in the project

  This cell is a seed-cell with a declared responsibility zone — everything related
  to Docker in the project: in-container environment assertions, container launching,
  image building, and reading the goga package version inside an image. The body
  holds the guard routine `ensure_in_docker`; the stateful `DockerBuilder` (image
  building) and its supporting routines `docker_pull`, `docker_update`, and
  `docker_build_if_not_exist`; the image-version probe `docker_image_goga_version`;
  and the stateful `DockerRunner` (container launching).

  `docker_update` is the single --update decision point (force refresh — build
  when a Dockerfile is declared, else pull). `docker_build_if_not_exist` is its
  complement: a first-run safety net that builds the local image only when it is
  absent AND a Dockerfile is declared — it never pulls, and it is a no-op when
  the image already exists or when no Dockerfile is set. Both take PRIMITIVES
  (image, dockerfile, extra_args) so the acquisition surface stays decoupled
  from configuration loading (the cell's only Imports are the version-check
  types).

  The `DockerBuilder` build and `DockerRunner` run methods translate a uniform
  params dict into docker CLI flags by a shared rule: a 1-character key becomes a
  short flag (p → -p, v → -v); a multi-char snake_case key becomes a long flag
  (add_host → --add-host); a str value becomes "flag value"; a True value becomes a
  boolean flag; a False value omits the flag; a list value repeats the flag. In the
  `DockerRunner` run method, the name param is the one exception — it is emitted
  as the --name flag AND captured as the target for the guaranteed docker kill in
  the runner finally.

  Raw extra docker tokens (extra_args: list[str]) are a separate channel from the
  params dict: params are translated to flags by the shared param→flag rule, while
  extra_args are appended verbatim. Appended after the translated flags, before the
  image (docker run) / before -f (docker build). Structural-only validation
  (list[str]); docker surfaces flag conflicts. docker_update and
  docker_build_if_not_exist forward extra_args to DockerBuilder.build in their
  build branch only; their pull / no-op branches ignore it. The extra_args
  primitive keeps the acquisition routines free of configuration Imports.

  Route all output (guard error output, goga diagnostic messages) through sys.stderr
  (click is not used). Docker CLI invocations stream the CLI's own stdout/stderr
  directly.

  The zone also covers reading the goga package version inside an image: the
  probe routine `docker_image_goga_version` runs a short-lived capture
  container in the minimal form — no mounts, no env-file, no extra tokens.

  Every container launch through `DockerRunner` is gated by the host-side
  version consistency check: before the work container starts, the runner
  asks `version_check_enabled` whether the check runs, reads the image
  version via `docker_image_goga_version`, and hands the version string to
  `ensure_version_match`, which owns the outcome matrix. A refusal stops the
  launch before the work container starts. Use the `version-check` practice
  for the check contract, the outcome matrix, and the escape environment
  variable.

---

"ensure_in_docker()":
  location: env.py
  annotations: |
    In-container environment assertion: guard routine for in-container entrypoints.
    Reads the "GOGA_DOCKER" marker and aborts the process when the marker is absent
    or not equal to "1", so host-side invocations of in-container entrypoints fail
    loudly instead of silently producing broken behavior (missing in-container
    binaries, wrong paths, missing runtime directories).

    Algorithm:
    1. Read the "GOGA_DOCKER" environment variable
    2. If the value is not "1": print a clear message to sys.stderr explaining that
       this entrypoint must run inside the goga Docker image; terminate the process
       with exit code 1
    3. Otherwise: return normally

    Requirements:
    - The "GOGA_DOCKER" marker is set only in the Dockerfile; its absence or any
      value other than "1" means host environment
    - Emit output only to sys.stderr (click is not used)
    - Terminate the process with exit code 1 before any filesystem or process work
    - Both branches MUST be covered by tests: the success path
      (marker equals "1" — returns normally) and the refusal path
      (marker unset or any value other than "1" — writes to stderr and exits
      with code 1)

    Constraints:
    - Do NOT perform filesystem or process work — the guard must terminate
      the process before any such work runs
    - Do NOT validate other environment variables — the responsibility is solely
      the "GOGA_DOCKER" marker
    - Do NOT return an error value — the routine returns only in the in-container
      case; the host case terminates the process

"DockerBuilder(image: str, dockerfile: str = 'Dockerfile', context: str = '.')":
  location: builder.py
  annotations: |
    Stateful Docker image builder. The image to build (the tag), the Dockerfile
    path, and the build context are concrete per build, so they are supplied to
    the constructor and held as state. Part of the goga/docker zone ("image building").

    `image`: image:tag to build and tag — MUST be non-None (the caller validates
             config.image) so the locally built image overrides the registry tag
             consumed by docker run
    `dockerfile`: path to the Dockerfile, relative to `context` (default "Dockerfile")
    `context`: build context directory (default "." — the project root)

    Docker invocations stream the CLI's own stdout/stderr directly; click is not
    used (build failure surfaces as a raised exception the caller maps to exit 1).
  methods:
    "build(extra_args: list[str] | None = None, ...params: dict[str, str | bool | list[str]]) -> none: None": |
      Run docker build for this builder's image/dockerfile/context, with extra
      CLI options passed as `...params` and raw extra tokens passed as `extra_args`.
      Build failure is FATAL — it propagates as a raised exception so the caller
      exits non-zero (exit 1).

      `...params`: additional docker build CLI options, translated to flags by the
                shared rule in the cell Annotations (e.g. add_host → --add-host,
                pull=True → --pull). The `...params` form is the DSL
                arbitrary-arguments notation.
      `extra_args`: raw extra docker tokens appended verbatim AFTER the translated
                    params flags and BEFORE -f (structural-only; docker surfaces
                    conflicts). Defaults to [].

      Algorithm:
      1. Translate `...params` into docker flags per the shared param→flag rule
      2. Run docker build <params-flags> <extra_args> -f <dockerfile> -t <image>
         <context>, streaming docker output to the host stdout/stderr
      3. On non-zero docker exit: raise (fatal) — do NOT swallow

      Requirements:
      - The -t flag is always set to the constructor image — local build overrides
        the registry image
      - <context> is the constructor value (project root); <dockerfile> resolves
        relative to it
      - `extra_args` tokens are appended verbatim (no translation) between the
        params flags and -f

      Constraints:
      - Do not add cache / multi-stage / platform flags unless supplied via `...params`
      - Do not push the built image to a registry

"docker_pull(image: str) -> ok: bool":
  location: builder.py
  annotations: |
    Standalone routine — pull a Docker image from the registry. NOT a method of
    `DockerBuilder` (build and pull are separate operations). NON-fatal: returns
    False and logs a WARNING on failure, so the caller continues on the local image.

    `image`: image:tag to pull (non-None; the caller passes the validated config.image)
    `ok`: True on success, False on pull failure (network / auth / not-found)

    Algorithm:
    1. Run docker pull <image>, streaming docker output
    2. Return True on success; on non-zero docker exit log a WARNING and return False

    Requirements:
    - Pull failure is recoverable — return False, never raise

    Constraints:
    - Do not raise on pull failure — return False so the caller warns and continues

"docker_update(image: str, dockerfile: str | None, extra_args: list[str] | None = None) -> none: None":
  location: builder.py
  annotations: |
    Orchestrator for the --update/-u flag: BUILD when a Dockerfile is declared,
    otherwise PULL. Centralizes the conditional so the three call sites (build,
    pipeline-discovery, pipeline-run) share one decision point and one
    error-semantics contract (build fatal, pull warning).

    Takes PRIMITIVES (`image`, `dockerfile`, `extra_args`), not a Config object, so
    the acquisition routines stay decoupled from configuration loading (the cell's
    only Imports are the version-check types, consumed by `DockerRunner`).
    The `image: str` signature makes the non-None precondition
    explicit at the call site — callers pass the already-validated config.image.

    `image`: image:tag — non-None (the caller validates config.image before
             calling); used as the build tag and the pull target
    `dockerfile`: path to a project Dockerfile. None → pull branch.
    `extra_args`: raw extra docker tokens forwarded verbatim to `DockerBuilder`
                  .build in the build branch (appended before -f); ignored by the
                  pull branch. Defaults to [].

    Algorithm:
    1. When `dockerfile` is not None: construct `DockerBuilder` with this
       `image` and `dockerfile`, context ".", and call its build method with
       pull=True (translated to the --pull flag) and the `extra_args` tokens so
       base images declared via FROM refresh from the registry instead of being
       served from the local cache — build failure is fatal (propagates)
    2. When `dockerfile` is None: call `docker_pull` with `image` — pull failure
       is a WARNING, non-fatal (`extra_args` is ignored on the pull branch)

    Requirements:
    - `image` MUST be non-None (caller-validated precondition); it is forwarded
      verbatim into `DockerBuilder` / `docker_pull`
    - dockerfile set  → build with --pull + `extra_args` (fatal on failure)
    - dockerfile None → pull (WARNING on failure); `extra_args` ignored

    Constraints:
    - Exactly one of build / pull runs — no third branch
    - The caller gates this with the --update flag; do nothing otherwise
    - The build branch always emits --pull; callers needing other custom build
      CLI options construct `DockerBuilder` directly (and would then import it
      themselves)
    - The pull branch ignores `extra_args` — extra tokens apply to image BUILD
      only, not to a registry pull

"docker_build_if_not_exist(image: str, dockerfile: str | None, extra_args: list[str] | None = None) -> none: None":
  location: builder.py
  annotations: |
    First-run safety net: build the local image when it is ABSENT and a project
    Dockerfile is declared. Complementary to `docker_update` — `docker_update`
    is gated by the --update flag (force refresh); this routine runs
    UNCONDITIONALLY at launch entry so the launcher never hands a non-existent
    locally-built image to docker run. Its purpose is to guarantee the image
    exists, not to refresh it.

    Takes PRIMITIVES (`image`, `dockerfile`, `extra_args`), not a Config object,
    so the acquisition routines stay decoupled from configuration loading — same
    convention as `docker_update` (the cell's only Imports are the version-check
    types, consumed by `DockerRunner`). The `image: str` signature makes
    the non-None precondition explicit at the call site — callers pass the
    already-validated config.image.

    `image`: image:tag — non-None (caller-validated precondition); used as the
             local-image probe target and the build tag
    `dockerfile`: path to a project Dockerfile. None → no-op when the image is
                  absent (this routine never pulls — a registry image is pulled
                  by docker run itself or by an explicit --update).
    `extra_args`: raw extra docker tokens forwarded verbatim to `DockerBuilder`
                  .build in the build branch (appended before -f); ignored by the
                  no-op branches. Defaults to [].

    Algorithm:
    1. Probe the local image store for `image` via docker image inspect (a
       silent capture-stdout/stderr subprocess; returncode 0 means present).
       A missing docker binary is tolerated and treated as "image not present"
       (the caller has already verified docker availability via _check_docker).
    2. When the image is present: return immediately (no-op — do not refresh;
       refreshing is `docker_update`'s responsibility under --update). `extra_args`
       is ignored.
    3. When the image is absent and `dockerfile` is not None: construct
       `DockerBuilder` with this `image` and `dockerfile`, context ".", and
       call its build method with pull=True (translated to the --pull flag) and
       the `extra_args` tokens so base images declared via FROM refresh from
       the registry instead of being served from the local cache — build failure
       is fatal (propagates, same error semantics as the build branch of
       `docker_update`)
    4. When the image is absent and `dockerfile` is None: return (no-op — never
       pulls). `extra_args` is ignored.

    Requirements:
    - `image` MUST be non-None (caller-validated precondition); it is forwarded
      verbatim into the docker image inspect probe and `DockerBuilder`
    - image present → no-op (no refresh — this routine is a safety net, not a
      refresh mechanism)
    - image absent + dockerfile set → build with --pull + `extra_args` (fatal on
      failure, same as `docker_update` build branch)
    - image absent + dockerfile None → no-op (NEVER pulls)

    Constraints:
    - Never pull — pulling is `docker_update`'s responsibility under --update
      or docker run's implicit registry fetch; this routine only BUILDS
    - Do not refresh existing images — that is `docker_update` under --update
    - The build branch always emits --pull; callers needing other custom build
      CLI options construct `DockerBuilder` directly (and would then import it
      themselves)
    - The no-op branches (image present, or absent with no dockerfile) ignore
      `extra_args` — extra tokens apply to image BUILD only
    - The caller runs this UNCONDITIONALLY at launch entry — no --update gate
      (the gate belongs to `docker_update`)

"docker_image_goga_version(image: str) -> version: str | None":
  location: builder.py
  annotations: |
    Read the goga package version inside a Docker image with a short-lived
    probe container, in capture mode.

    `image`: image to probe (non-None; the caller passes the validated
             image)
    `version`: version string printed inside the image, or None when the
               version could not be determined

    Algorithm:
    1. Run one short-lived container: docker run --rm --entrypoint python3
       <image> -c "from importlib.metadata import version; print(version('goga'))",
       capturing stdout and stderr instead of streaming them
    2. Keep the probe minimal: no mounts, no env-file, no extra docker
       tokens, no runner params
    3. When the docker exit code is 0 and the first stdout line, stripped
       of whitespace, begins with a non-empty ASCII-digit major segment
       (the leading release-segment shape — digits, then optionally
       ".digits", before any dev/pre/post/local tail), return that
       stripped line
    4. Return None on every failure: a non-zero docker exit, a missing
       docker binary, empty output, or output that does not look like a
       version

    Requirements:
    - Exactly one short-lived container per call (--rm)
    - Silent on the host — nothing is printed or streamed
    - Never raises — every failure is returned as None
    - Tolerate a missing docker binary (return None; the caller has
      already verified docker availability)

    Constraints:
    - Do not cache the result — one probe per call
    - Do not validate that the version is a real released version — shape
      recognition only
    - Do not pass credential material, mounts, env-file, or extra tokens
      to the probe

    Apply the `convention` practice for docstring style and intra-package
    imports.

"DockerRunner(image: str)":
  location: runner.py
  annotations: |
    Stateful Docker container runner — mirrors `DockerBuilder`: the image to run
    is concrete, so it is supplied to the constructor. Its run method assembles the
    docker run command from the image + CLI-option params + command args,
    manages the lifecycle (SIGTERM/SIGINT handler, guaranteed docker kill +
    handler restore in finally), and returns the container exit code. Part of the
    goga/docker zone ("container launching").

    The runner is the launch governor as well as the executor: before the
    work container starts, run applies the version consistency check
    (decide, probe, verify). The check adds no parameters — its only escape
    is the host-side environment variable handled by `version_check_enabled`.

    `image`: container image (non-None; the caller passes the validated config.image)
  methods:
    "run(args: list[str], extra_args: list[str] | None = None, ...params: dict[str, str | bool | list[str]]) -> exit_code: int": |
      Assemble and run docker run <params-flags> <extra_args> <image> <args>, then
      manage the container lifecycle.

      `args`: the COMMAND + ARGs after the image (e.g. the goga.build /
              goga.pipeline module invocation + flags). Required positional — a
              docker run needs a command, which cannot be a flag.
      `extra_args`: raw extra docker tokens appended verbatim AFTER the translated
                    params flags and BEFORE the image (structural-only; docker
                    surfaces conflicts). Defaults to [].
      `...params`: docker run CLI options, translated to flags by the SAME rule as
                the `DockerBuilder` build method. The caller passes the standard
                launch options: name, rm, v (mounts), p (port publish), add_host,
                env_file, entrypoint. The name param is REQUIRED and SPECIAL — it is
                emitted as the --name flag AND captured as the target for the
                guaranteed docker kill in finally (the one exception to the
                uniform param→flag rule). list values repeat the flag (multiple
                mounts / hosts). The `...params` form is the DSL
                arbitrary-arguments notation.
      `exit_code`: the container exit code

      Algorithm:
      0. Validate that the name param is present: it is the docker kill
         target of the finally block, so a call without it is
         programmatically doomed — fail fast with ValueError BEFORE the
         version gate, so a doomed call launches not even the probe
         container
      1. Version check gate: when `version_check_enabled` is True, read the
         image version via `docker_image_goga_version` (the constructor
         image; the probe runs in the minimal form) and hand the string to
         `ensure_version_match`. On refusal the process exits with code 1
         before the work container starts. At this point the method has not
         yet translated params, installed its signal handlers, or entered
         its try/finally — nothing was started and no handler was replaced,
         so there is no runner teardown to perform; the caller's cleanup
         blocks run during the unwind. When the check is disabled, neither
         the probe nor the comparison runs
      2. Translate `...params` into docker flags per the shared param→flag rule
      3. Assemble argv: ["docker", "run", *flags, *extra_args, image, *args]
      4. Install a SIGTERM/SIGINT handler that exits the process with 128 + signum
      5. Launch docker run, streaming stdout/stderr; wait for the exit code
      6. In finally: docker kill <name> (suppress errors — the container may be
         gone) and restore the previous signal handler
      7. Return the exit code

      Requirements:
      - The assembled docker run form preserves the launcher contracts (same
        options and effect): --rm, --name, mounts via -v, --add-host,
        --env-file, --entrypoint, image, args
      - SIGTERM/SIGINT results in exit code 128 + signum (130 SIGINT, 143 SIGTERM)
      - docker kill <name> in finally is mandatory under every exit path; the
        previous signal handler is always restored
      - The container exit code is always returned
      - Exactly one short-lived probe container per run when the version
        check is enabled; none when disabled
      - The probe carries no runner params, mounts, env-file, or extra
        tokens
      - The version check inherits whatever setup the caller has already
        completed: callers that install a signal handler and write secret
        files do so BEFORE invoking run, so the probe runs inside that
        window on those paths; the probe itself mounts nothing, writes
        nothing, and receives no tokens — it adds no leak surface on any
        path, including paths without a caller-side handler (the read-only
        info forms)

      Constraints:
      - Do NOT resolve proxy / hosts / credential mounts / runtime-dir / env-file
        here — the caller resolves them and passes them as `...params` (the runner is
        a thin executor, like build)
      - Do NOT delete env-file / tmpfile or remove .ralphex here — host-side
        cleanup is the CALLER's job in its own finally (it is not a docker flag,
        so it cannot be a param). Run order is preserved: the runner finally
        (docker kill + handler restore) runs before the caller finally (file /
        .ralphex cleanup)
      - Do not add a parameter that disables the version check — the escape
        is the host-side environment variable only
      - Do not change the signature or the return contract — the exit code
        of the work container remains the only return value; a check
        refusal is an exceptional exit, not a return value

---

Author: Goga
CreatedAt: 09/07/26

Description: |
  Seed-cell with a declared responsibility zone — everything related to Docker in
  the project: in-container environment assertions, container launching, image
  building, and reading the goga package version inside an image. The body holds
  the guard routine `ensure_in_docker`; the stateful `DockerBuilder` plus the
  `docker_pull`, `docker_update`, and `docker_build_if_not_exist` routines
  (image acquisition — `docker_update` refreshes under `--update`: build when a
  project Dockerfile is declared, else pull; `docker_build_if_not_exist` is the
  first-run safety net that builds the local image when it is absent and a
  Dockerfile is declared); the `docker_image_goga_version` probe (a short-lived
  capture container reading the goga version inside an image); and the stateful
  `DockerRunner` (container launching + lifecycle, gated before every launch by
  the host-side version consistency check). The image-acquisition routines and
  the probe take primitives; the runner applies the version check imported from
  goga/version.
