diff --git a/.semgrep.yaml b/.semgrep.yaml index a7276eb5..a83a1c8c 100644 --- a/.semgrep.yaml +++ b/.semgrep.yaml @@ -188,6 +188,21 @@ rules: - "**/tests/**" - "**/docs/**" + - id: no-legacy-defk-handler-signatures + languages: [generic] + severity: ERROR + message: > + HYP-197: production Hy effect handlers must use defhandler. Do not write + legacy defk handlers with effect, eff, or k parameters because doeff-hy + rejects those signatures at import time. + pattern-regex: '(?m)^\s*\(defk\s+[^\s\[]+\s+\[[^\]\n]*(?:\beffect\b|\beff\b|\bk\b)[^\]\n]*\]' + paths: + include: + - "/packages/**/src/**/*.hy" + exclude: + - "**/tests/**" + - "**/examples/**" + - id: cache-handler-no-result-duck-typing languages: [python] severity: ERROR diff --git a/Hypha/Evidence/HYP-197/pr-explanation.imagegen-prompt.md b/Hypha/Evidence/HYP-197/pr-explanation.imagegen-prompt.md new file mode 100644 index 00000000..a2970c49 --- /dev/null +++ b/Hypha/Evidence/HYP-197/pr-explanation.imagegen-prompt.md @@ -0,0 +1,13 @@ +Tool: imagegen + +Prompt: +Use case: infographic-diagram +Asset type: GitHub PR説明図, 16:9 PNG +Primary request: HYP-197 の実装内容を説明する、霞ヶ関スタイルの情報密度が高い日本語インフォグラフィックを作成する。 +Visual style: 白地に濃紺と深緑の罫線、行政資料のような整然とした区画、細い矢印、チェック印、コードファイルの小さなピクトグラム、盾の検査アイコン。装飾は控えめ、文字は読みやすい太めのゴシック体。 +Layout: 上部に大見出し「HYP-197: 旧 handler 署名を defhandler へ移行」。左から右へ 3 カラム。 +Left column heading: 「問題」。内容: 「production Hy source に旧 defk [effect k] が残存」「doeff-hy macro guard が import / pytest collection を停止」。 +Middle column heading: 「変更」。内容: 「doeff-docker: Dockerfile / Docker build / push を defhandler 化」「doeff-ml-nexus: file / docker / rsync / resolve を defhandler 化」「Dockerfile 生成 Program に契約を追加」。中央に大きな矢印「defk 署名禁止 → defhandler 節」。 +Right column heading: 「確認」。内容: 「handler module import 通過」「Dockerfile 生成 4 tests 通過」「package tests 27 tests 通過」「Semgrep guard: no-legacy-defk-handler-signatures」。 +Bottom band heading: 「レビュアーが見る点」。内容: 「未対応 effect は defhandler の既定転送」「Tell / Ask / slog の effect delegation を保持」「旧署名の再混入は architecture test と Semgrep が検出」。 +Constraints: すべての見出し、ラベル、説明文は日本語。技術識別子は必要なものだけそのまま使う。Program.resolve や lazy_ask や resolved["tdnet"] のような、このPRに存在しない疑似APIや処理は一切描かない。Markdown、HTML、SVG、Mermaid風ではなく、生成されたビットマップの完成図にする。 diff --git a/Hypha/Evidence/HYP-197/pr-explanation.png b/Hypha/Evidence/HYP-197/pr-explanation.png new file mode 100644 index 00000000..faf84710 Binary files /dev/null and b/Hypha/Evidence/HYP-197/pr-explanation.png differ diff --git a/packages/doeff-docker/src/doeff_docker/handlers/docker.hy b/packages/doeff-docker/src/doeff_docker/handlers/docker.hy index a9b98a86..edede70d 100644 --- a/packages/doeff-docker/src/doeff_docker/handlers/docker.hy +++ b/packages/doeff-docker/src/doeff_docker/handlers/docker.hy @@ -2,9 +2,8 @@ ;;; Handles DockerBuild, ImagePush effects via shell commands. ;;; DockerRun is application-specific (depends on runner module) — not included here. -(require doeff_hy.macros [defk <-]) +(require doeff_hy.macros [<- defhandler]) (import doeff [do :as _doeff-do]) -(import doeff [Resume Pass]) (import doeff_core_effects [slog]) (import subprocess) @@ -14,6 +13,7 @@ (import doeff_docker.effects [DockerBuild ImagePush]) +;; Plain callable: shared synchronous subprocess boundary used by handler clauses. (defn run-cmd [args * [host "localhost"] [stdin-data None]] "Run a command as list of args, optionally on a remote host via SSH. Returns CompletedProcess." @@ -27,24 +27,20 @@ proc) -(defk docker-build-handler [effect k] +(defhandler docker-build-handler "Handle DockerBuild: build Docker image from Dockerfile string." - (if (isinstance effect DockerBuild) - (do - (<- (slog :msg f"docker build: {effect.tag} on {effect.host}")) - (setv args ["docker" "build" "-t" effect.tag "-f" "-" (str effect.context-path)]) - (run-cmd args :host effect.host - :stdin-data (.encode effect.dockerfile "utf-8")) - (yield (Resume k effect.tag))) - (yield (Pass effect k)))) + (DockerBuild [dockerfile tag context-path host] + (<- (slog :msg f"docker build: {tag} on {host}")) + (setv args ["docker" "build" "-t" tag "-f" "-" (str context-path)]) + (run-cmd args :host host + :stdin-data (.encode dockerfile "utf-8")) + (resume tag))) -(defk image-push-handler [effect k] +(defhandler image-push-handler "Handle ImagePush: tag and push image to registry." - (if (isinstance effect ImagePush) - (do - (<- (slog :msg f"docker push: {effect.local-tag} -> {effect.remote-tag}")) - (run-cmd ["docker" "tag" effect.local-tag effect.remote-tag]) - (run-cmd ["docker" "push" effect.remote-tag]) - (yield (Resume k effect.remote-tag))) - (yield (Pass effect k)))) + (ImagePush [local-tag remote-tag] + (<- (slog :msg f"docker push: {local-tag} -> {remote-tag}")) + (run-cmd ["docker" "tag" local-tag remote-tag]) + (run-cmd ["docker" "push" remote-tag]) + (resume remote-tag))) diff --git a/packages/doeff-docker/src/doeff_docker/handlers/dockerfile.hy b/packages/doeff-docker/src/doeff_docker/handlers/dockerfile.hy index 5f26a837..1cc0c7af 100644 --- a/packages/doeff-docker/src/doeff_docker/handlers/dockerfile.hy +++ b/packages/doeff-docker/src/doeff_docker/handlers/dockerfile.hy @@ -2,49 +2,47 @@ ;;; Collects Dockerfile instruction effects (From, Run, Copy, etc.) ;;; via WithObserve + Tell, then retrieves via state. -(require doeff_hy.macros [defk <-]) +(require doeff_hy.macros [defk <- defhandler]) (import doeff [do :as _doeff-do]) -(import doeff [Resume Pass WithObserve WithHandler]) +(import doeff [Program WithObserve]) (import doeff_core_effects [Tell WriterTellEffect]) (import doeff_docker.effects [From Run Copy Workdir SetEnv Expose]) -(defk dockerfile-collector-handler [effect k] +(defhandler dockerfile-collector-handler "Handler that converts typed Dockerfile effects (From, Run, etc.) into Tell messages, then Resumes with None. Pair with WithObserve or Listen to collect the messages." - (cond - (isinstance effect From) - (do (<- (Tell f"FROM {effect.image}")) - (yield (Resume k None))) + (From [image] + (<- (Tell f"FROM {image}")) + (resume None)) - (isinstance effect Run) - (do (<- (Tell f"RUN {effect.command}")) - (yield (Resume k None))) + (Run [command] + (<- (Tell f"RUN {command}")) + (resume None)) - (isinstance effect Copy) - (do (<- (Tell f"COPY {effect.src} {effect.dst}")) - (yield (Resume k None))) + (Copy [src dst] + (<- (Tell f"COPY {src} {dst}")) + (resume None)) - (isinstance effect Workdir) - (do (<- (Tell f"WORKDIR {effect.path}")) - (yield (Resume k None))) + (Workdir [path] + (<- (Tell f"WORKDIR {path}")) + (resume None)) - (isinstance effect SetEnv) - (do (<- (Tell f"ENV {effect.key}={effect.value}")) - (yield (Resume k None))) + (SetEnv [key value] + (<- (Tell f"ENV {key}={value}")) + (resume None)) - (isinstance effect Expose) - (do (<- (Tell f"EXPOSE {effect.port}")) - (yield (Resume k None))) + (Expose [port] + (<- (Tell f"EXPOSE {port}")) + (resume None))) - True - (yield (Pass effect k)))) - -(defn render-dockerfile [messages] +(defk render-dockerfile [messages] "Convert collected WriterTellEffect list to Dockerfile string. Pure function." + {:pre [(: messages list)] + :post [(: % str)]} (.join "\n" (lfor m messages m.msg))) @@ -52,6 +50,8 @@ "Run an image-definition Program, collect Dockerfile instructions. Uses WithObserve to capture Tell effects emitted by dockerfile-collector-handler. Returns Dockerfile string." + {:pre [(: image-program Program)] + :post [(: % str)]} (setv collected []) (defn _observer [effect] @@ -59,5 +59,6 @@ (.append collected effect))) (<- (WithObserve _observer - (WithHandler dockerfile-collector-handler image-program))) - (render-dockerfile collected)) + (dockerfile-collector-handler image-program))) + (<- dockerfile (render-dockerfile collected)) + dockerfile) diff --git a/packages/doeff-ml-nexus/src/doeff_ml_nexus/docker.hy b/packages/doeff-ml-nexus/src/doeff_ml_nexus/docker.hy index 1d9f2eea..97e7d084 100644 --- a/packages/doeff-ml-nexus/src/doeff_ml_nexus/docker.hy +++ b/packages/doeff-ml-nexus/src/doeff_ml_nexus/docker.hy @@ -17,17 +17,29 @@ (defk copy-from [src dst build-context-path] "Copy a file from outside build context: rsync to context, then COPY." - (<- (RsyncTo :src src :host "localhost" - :dst-path (str (/ build-context-path (Path src) .name)))) - (<- (Copy :src (. (Path src) name) :dst dst))) + {:pre [(: src "str or Path") + (or (isinstance src str) (isinstance src Path)) + (: dst str) + (: build-context-path Path)] + :post [(: % "None") (is % None)]} + (setv src-path (Path src)) + (<- (RsyncTo :src src-path :host "localhost" + :dst-path (str (/ build-context-path src-path.name)))) + (<- (Copy :src src-path.name :dst dst))) (defk copy-resolved [source-id dst build-context-path] "Resolve a source id to a path, rsync into context, then COPY." + {:pre [(: source-id str) + (: dst str) + (: build-context-path Path)] + :post [(: % "None") (is % None)]} (<- src-path (Resolve :target source-id :kind Path)) - (copy-from src-path dst build-context-path)) + (<- (copy-from src-path dst build-context-path))) (defk apt-install [#* packages] "RUN apt-get install." + {:pre [(: packages tuple)] + :post [(: % "None") (is % None)]} (<- (Run :command (+ "apt-get update && apt-get install -y --no-install-recommends " (.join " " packages) @@ -35,11 +47,15 @@ (defk install-uv [] "Install uv into the image." + {:pre [] + :post [(: % "None") (is % None)]} (<- (Run :command "curl -LsSf https://astral.sh/uv/install.sh | sh")) (<- (SetEnv :key "PATH" :value "/root/.local/bin:$PATH"))) (defk install-rust [] "Install Rust toolchain into the image." + {:pre [] + :post [(: % "None") (is % None)]} (<- (Run :command "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y")) (<- (SetEnv :key "PATH" :value "/root/.cargo/bin:$PATH"))) @@ -51,12 +67,16 @@ (defk copy-local-deps [deps] "COPY top-level local deps into container at /deps/. Sub-deps (e.g. doeff-vm inside doeff) are included automatically." + {:pre [(: deps list)] + :post [(: % "None") (is % None)]} (setv top-deps (_find-top-level-deps deps)) (for [dep top-deps] (<- (Copy :src f"deps/{dep.name}" :dst f"/deps/{dep.name}")))) -(defn has-rust-extension [deps] +(defk has-rust-extension [deps] "Check if any local dep requires Rust build (has Cargo.toml)." + {:pre [(: deps list)] + :post [(: % bool)]} (any (lfor dep deps (.exists (/ dep.local-path "Cargo.toml"))))) @@ -78,6 +98,11 @@ 2. Local deps COPY (if any) 3. Dependency layer (pyproject.toml + uv.lock only) 4. Full source + project install" + {:pre [(: base-image str) + (: project-root "Path or None") + (or (is project-root None) (isinstance project-root Path)) + (: gpu bool)] + :post [(: % "None") (is % None)]} (<- (From :image base-image)) (when gpu (<- (SetEnv :key "NVIDIA_VISIBLE_DEVICES" :value "all")) @@ -87,7 +112,8 @@ ;; Check for local deps and rust extensions (setv deps (if project-root (find-local-deps project-root) [])) - (when (and deps (has-rust-extension deps)) + (<- has-rust (has-rust-extension deps)) + (when (and deps has-rust) (<- (install-rust))) ;; Copy local deps if any @@ -107,8 +133,16 @@ (defk uv-image [base-image * [project-root None]] "Standard uv project Dockerfile." + {:pre [(: base-image str) + (: project-root "Path or None") + (or (is project-root None) (isinstance project-root Path))] + :post [(: % "None") (is % None)]} (<- (uv-image-core base-image project-root))) (defk uv-gpu-image [base-image * [project-root None]] "uv project with NVIDIA GPU support." + {:pre [(: base-image str) + (: project-root "Path or None") + (or (is project-root None) (isinstance project-root Path))] + :post [(: % "None") (is % None)]} (<- (uv-image-core base-image project-root :gpu True))) diff --git a/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/docker.hy b/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/docker.hy index 11306c62..0566f365 100644 --- a/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/docker.hy +++ b/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/docker.hy @@ -2,9 +2,8 @@ ;;; DockerBuild and ImagePush handlers are in doeff-docker. ;;; This module provides DockerRun handler with file-based cloudpickle exchange. -(require doeff_hy.macros [defk <-]) +(require doeff_hy.macros [<- defhandler]) (import doeff [do :as _doeff-do]) -(import doeff [Resume Pass]) (import doeff_core_effects [Ask slog]) (import uuid) @@ -13,55 +12,53 @@ (import doeff_docker.handlers.docker [run-cmd]) -(defk docker-run-handler [effect k] +(defhandler docker-run-handler "Handle DockerRun: execute Program[T] in container via file-based cloudpickle. 1. Write pickled program to temp file on host 2. Docker run with volume mount, doeff run invokes p_run 3. Read pickled result from output file 4. Clean up" - (if (isinstance effect DockerRun) - (do - (<- (slog :msg f"docker run: {effect.image} on {effect.host} gpu={effect.gpu}")) - (<- serializer (Ask "serializer")) + (DockerRun [image program host gpu mounts env-vars] + (<- (slog :msg f"docker run: {image} on {host} gpu={gpu}")) + (<- serializer (Ask "serializer")) - ;; Temp paths on host - (setv run-id (cut (. (uuid.uuid4) hex) 0 8)) - (setv tmp-dir f"/tmp/doeff-run/{run-id}") - (setv container-exchange "/tmp/doeff-exchange") + ;; Temp paths on host + (setv run-id (cut (. (uuid.uuid4) hex) 0 8)) + (setv tmp-dir f"/tmp/doeff-run/{run-id}") + (setv container-exchange "/tmp/doeff-exchange") - ;; Create tmp dir and write pickled program - (run-cmd ["mkdir" "-p" tmp-dir] :host effect.host) - (setv pickled (.dumps serializer effect.program)) - (run-cmd ["tee" f"{tmp-dir}/program.pkl"] :host effect.host :stdin-data pickled) + ;; Create tmp dir and write pickled program + (run-cmd ["mkdir" "-p" tmp-dir] :host host) + (setv pickled (.dumps serializer program)) + (run-cmd ["tee" f"{tmp-dir}/program.pkl"] :host host :stdin-data pickled) - ;; Docker run args - (setv parts ["docker" "run" "--rm"]) - (when effect.gpu - (.extend parts ["--gpus" "all"])) - (.extend parts ["-v" f"{tmp-dir}:{container-exchange}"]) - (for [m effect.mounts] - (.extend parts ["-v" m])) - (for [e effect.env-vars] - (.extend parts ["-e" e])) - (.extend parts ["-e" "DOEFF_DISABLE_PROFILE=1" - "-e" "DOEFF_DISABLE_RUNBOX=1" - "-e" f"DOEFF_INPUT={container-exchange}/program.pkl" - "-e" f"DOEFF_OUTPUT={container-exchange}/result.pkl" - effect.image - "uv" "run" "doeff" "run" - "--program" "doeff_ml_nexus.runner.p_run" - "--interpreter" "doeff_ml_nexus.runner.runner_interpreter"]) + ;; Docker run args + (setv parts ["docker" "run" "--rm"]) + (when gpu + (.extend parts ["--gpus" "all"])) + (.extend parts ["-v" f"{tmp-dir}:{container-exchange}"]) + (for [m mounts] + (.extend parts ["-v" m])) + (for [e env-vars] + (.extend parts ["-e" e])) + (.extend parts ["-e" "DOEFF_DISABLE_PROFILE=1" + "-e" "DOEFF_DISABLE_RUNBOX=1" + "-e" f"DOEFF_INPUT={container-exchange}/program.pkl" + "-e" f"DOEFF_OUTPUT={container-exchange}/result.pkl" + image + "uv" "run" "doeff" "run" + "--program" "doeff_ml_nexus.runner.p_run" + "--interpreter" "doeff_ml_nexus.runner.runner_interpreter"]) - ;; Execute - (run-cmd parts :host effect.host) + ;; Execute + (run-cmd parts :host host) - ;; Read result - (setv proc (run-cmd ["cat" f"{tmp-dir}/result.pkl"] :host effect.host)) - (setv result (.loads serializer proc.stdout)) + ;; Read result + (setv proc (run-cmd ["cat" f"{tmp-dir}/result.pkl"] :host host)) + (setv result (.loads serializer proc.stdout)) - ;; Cleanup - (run-cmd ["rm" "-rf" tmp-dir] :host effect.host) + ;; Cleanup + (run-cmd ["rm" "-rf" tmp-dir] :host host) - (yield (Resume k result))) - (yield (Pass effect k)))) + (resume result))) diff --git a/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/file.hy b/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/file.hy index b6c53f68..92766a0a 100644 --- a/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/file.hy +++ b/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/file.hy @@ -1,9 +1,8 @@ ;;; File operation handlers ;;; Handles WriteFile effect. -(require doeff_hy.macros [defk <-]) +(require doeff_hy.macros [defhandler]) (import doeff [do :as _doeff-do]) -(import doeff [Resume Pass]) (import subprocess) (import pathlib [Path]) @@ -11,14 +10,12 @@ (import doeff_ml_nexus.effects [WriteFile]) -(defk write-file-handler [effect k] +(defhandler write-file-handler "Handle WriteFile: write content to a file on a host." - (if (isinstance effect WriteFile) - (do - (if (= effect.host "localhost") - (.write-text (Path effect.path) effect.content) - (subprocess.run ["ssh" effect.host f"cat > {effect.path}"] - :input (.encode effect.content) :check True - :capture-output True)) - (yield (Resume k effect.path))) - (yield (Pass effect k)))) + (WriteFile [host path content] + (if (= host "localhost") + (.write-text (Path path) content) + (subprocess.run ["ssh" host f"cat > {path}"] + :input (.encode content) :check True + :capture-output True)) + (resume path))) diff --git a/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/resolve.hy b/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/resolve.hy index b9da62eb..593aaad3 100644 --- a/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/resolve.hy +++ b/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/resolve.hy @@ -1,9 +1,8 @@ ;;; Resolve handler ;;; Resolves targets to requested kinds using Ask-injected configuration. -(require doeff_hy.macros [defk <-]) +(require doeff_hy.macros [<- defhandler]) (import doeff [do :as _doeff-do]) -(import doeff [Resume Pass]) (import doeff_core_effects [Ask]) (import pathlib [Path]) @@ -11,20 +10,16 @@ (import doeff_ml_nexus.effects [Resolve]) -(defk resolve-handler [effect k] +(defhandler resolve-handler "Resolve handler using Ask-injected source_root. Supports: Resolve(target=str, kind=Path) -> Path(source_root / target) Extend by adding more match branches." - (if (isinstance effect Resolve) - (cond - ;; str id -> Path: look up in source_root - (and (isinstance effect.target str) (is effect.kind Path)) + (Resolve [target kind] + ;; str id -> Path: look up in source_root + (if (and (isinstance target str) (is kind Path)) (do (<- source-root (Ask "source_root")) - (yield (Resume k (/ (Path source-root) effect.target)))) - - True + (resume (/ (Path source-root) target))) (raise (NotImplementedError - f"Resolve: unsupported target={effect.target!r} kind={effect.kind!r}"))) - (yield (Pass effect k)))) + f"Resolve: unsupported target={target!r} kind={kind!r}"))))) diff --git a/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/rsync.hy b/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/rsync.hy index 8c8eea04..761a1600 100644 --- a/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/rsync.hy +++ b/packages/doeff-ml-nexus/src/doeff_ml_nexus/handlers/rsync.hy @@ -1,18 +1,24 @@ ;;; Rsync handler ;;; Handles RsyncTo effect via rsync shell command. -(require doeff_hy.macros [defk <-]) +(require doeff_hy.macros [defk <- defhandler]) (import doeff [do :as _doeff-do]) -(import doeff [Resume Pass]) (import doeff_core_effects [slog]) (import subprocess) +(import pathlib [Path]) (import doeff_ml_nexus.effects [RsyncTo]) -(defn _rsync-args [src host dst-path * [excludes #()] [includes #()]] +(defk _rsync-args [src host dst-path * [excludes #()] [includes #()]] "Build rsync command as list of args." + {:pre [(: src Path) + (: host str) + (: dst-path str) + (: excludes tuple) + (: includes tuple)] + :post [(: % list)]} (setv parts ["rsync" "-avz" "--delete"]) ;; includes must come before excludes in rsync (for [i includes] @@ -28,18 +34,16 @@ parts) -(defk rsync-handler [effect k] +(defhandler rsync-handler "Handle RsyncTo: rsync files to destination." - (if (isinstance effect RsyncTo) - (do - (<- (slog :msg f"rsync: {effect.src} -> {effect.host}:{effect.dst-path}")) - ;; Ensure destination directory exists - (when (!= effect.host "localhost") - (subprocess.run ["ssh" effect.host f"mkdir -p {effect.dst-path}"] - :check True :capture-output True)) - (setv args (_rsync-args effect.src effect.host effect.dst-path - :excludes effect.excludes - :includes effect.includes)) - (subprocess.run args :check True :capture-output True) - (yield (Resume k effect.dst-path))) - (yield (Pass effect k)))) + (RsyncTo [src host dst-path excludes includes] + (<- (slog :msg f"rsync: {src} -> {host}:{dst-path}")) + ;; Ensure destination directory exists + (when (!= host "localhost") + (subprocess.run ["ssh" host f"mkdir -p {dst-path}"] + :check True :capture-output True)) + (<- args (_rsync-args src host dst-path + :excludes excludes + :includes includes)) + (subprocess.run args :check True :capture-output True) + (resume dst-path))) diff --git a/packages/doeff-ml-nexus/tests/test_handler_imports.py b/packages/doeff-ml-nexus/tests/test_handler_imports.py new file mode 100644 index 00000000..e53231b8 --- /dev/null +++ b/packages/doeff-ml-nexus/tests/test_handler_imports.py @@ -0,0 +1,23 @@ +"""Import coverage for Hy handler modules used by doeff-ml-nexus.""" + +from __future__ import annotations + +import importlib + +import hy # noqa: F401 +import pytest + + +@pytest.mark.parametrize( + "module_name", + [ + "doeff_docker.handlers.dockerfile", + "doeff_docker.handlers.docker", + "doeff_ml_nexus.handlers.file", + "doeff_ml_nexus.handlers.docker", + "doeff_ml_nexus.handlers.rsync", + "doeff_ml_nexus.handlers.resolve", + ], +) +def test_handler_modules_import_under_current_macro_guard(module_name: str) -> None: + importlib.import_module(module_name) diff --git a/tests/architecture/test_no_legacy_defk_handler_signatures.py b/tests/architecture/test_no_legacy_defk_handler_signatures.py new file mode 100644 index 00000000..35a7a6d2 --- /dev/null +++ b/tests/architecture/test_no_legacy_defk_handler_signatures.py @@ -0,0 +1,30 @@ +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +PRODUCTION_HY_SOURCES = REPO_ROOT / "packages" +DEFK_SIGNATURE_RE = re.compile(r"^\s*\(defk\s+([^\s\[]+)\s+\[([^\]]*)\]", re.MULTILINE) +HANDLER_LIKE_PARAMS = {"effect", "eff", "k"} + + +def _legacy_defk_handler_signatures() -> list[str]: + violations: list[str] = [] + for hy_file in sorted(PRODUCTION_HY_SOURCES.glob("*/src/**/*.hy")): + text = hy_file.read_text(encoding="utf-8") + for match in DEFK_SIGNATURE_RE.finditer(text): + params = set(match.group(2).split()) + handler_params = sorted(params & HANDLER_LIKE_PARAMS) + if not handler_params: + continue + line_number = text.count("\n", 0, match.start()) + 1 + relative_path = hy_file.relative_to(REPO_ROOT) + violations.append( + f"{relative_path}:{line_number} {match.group(1)} " + f"uses handler-like params {handler_params}" + ) + return violations + + +def test_package_production_hy_handlers_do_not_use_defk_signatures() -> None: + """Production Hy handlers must use defhandler instead of legacy defk(effect, k).""" + assert _legacy_defk_handler_signatures() == []