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. 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`; 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) so the cell stays a pure leaf with no Imports.

  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.

  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.

---

"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(...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`. 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.

      Algorithm:
      1. Translate `params` into docker flags per the shared param→flag rule
      2. Run docker build <params-flags> -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

      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) -> 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`), not a Config object, so the goga/docker
    cell stays a pure leaf (no Imports, no dependency on goga/config). 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.

    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) 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

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

    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)

"docker_build_if_not_exist(image: str, dockerfile: str | 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`), not a Config object, so the
    goga/docker cell stays a pure leaf with no Imports — same convention as
    `docker_update`. 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).

    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)
    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) 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)

    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 (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 caller runs this UNCONDITIONALLY at launch entry — no --update gate
      (the gate belongs to `docker_update`)

"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").

    `image`: container image (non-None; the caller passes the validated config.image)
  methods:
    "run(args: list[str], ...params: dict[str, str | bool | list[str]]) -> exit_code: int": |
      Assemble and run docker run <params-flags> <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.
      `params`: docker run CLI options, translated to flags by the SAME rule as
                the `DockerBuilder` build method. The caller passes the pre-refactor
                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:
      1. Translate `params` into docker flags per the shared param→flag rule;
         the name param must be present
      2. Assemble argv: ["docker", "run", *flags, image, *args]
      3. Install a SIGTERM/SIGINT handler that exits the process with 128 + signum
      4. Launch docker run, streaming stdout/stderr; wait for the exit code
      5. In finally: docker kill <name> (suppress errors — the container may be
         gone) and restore the previous signal handler
      6. Return the exit code

      Requirements:
      - The assembled docker run form preserves the pre-refactor launchers (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

      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)

---

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. 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); and the stateful
  `DockerRunner` (container launching + lifecycle). Both image-acquisition
  routines take primitives so the cell stays a pure leaf with no Imports.
