From 3656400cebd6fc84fa5811b7c702ccb67df30813 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:34:27 +0100 Subject: [PATCH 1/2] ci: remove retired Semgrep action --- .github/workflows/actions.lock | 6 --- .github/workflows/security-policy.yml | 63 --------------------------- 2 files changed, 69 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 793349be..7bee7df4 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -97,7 +97,6 @@ workflows: - 'dtolnay/rust-toolchain@master' - 'github/codeql-action@v4.37.8' - 'gitleaks/gitleaks-action@v3.0.0' - - 'returntocorp/semgrep-action@v1' - 'taiki-e/install-action@v2.86.7' - 'trufflesecurity/trufflehog@v3.97.1' '.github/workflows/tests.yml': @@ -310,11 +309,6 @@ dependencies: commit: 'sha1-c74f04c77a36247a813493945e803178b93d170f' owner_id: 6759885 repo_id: 1275650185 - 'returntocorp/semgrep-action@v1': - ref: 'v1' - commit: 'sha1-713efdd345f3035192eaa63f56867b88e63e4e5d' - owner_id: 147861678 - repo_id: 715166645 'ruby/setup-ruby@v1.321.0': ref: 'v1.321.0' commit: 'sha1-95ef2b042f9d7a56d8268cba8559e2842e2ad01b' diff --git a/.github/workflows/security-policy.yml b/.github/workflows/security-policy.yml index d331acbd..239c5ad8 100644 --- a/.github/workflows/security-policy.yml +++ b/.github/workflows/security-policy.yml @@ -103,37 +103,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # ============================================================================ - # SAST Scanning - # ============================================================================ - - semgrep: - name: Semgrep SAST - runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: read - security-events: write - steps: - - name: Checkout repository - uses: actions/checkout@v7.0.1 - - - name: Run Semgrep - uses: returntocorp/semgrep-action@v1 - with: - config: >- - p/rust - p/elixir - p/security-audit - p/secrets - args: --sarif --output=semgrep.sarif - - - name: Upload SARIF results - uses: github/codeql-action/upload-sarif@v4.37.8 - if: always() - with: - sarif_file: semgrep.sarif - codeql: name: CodeQL Analysis runs-on: ubuntu-latest @@ -237,7 +206,6 @@ jobs: - rust-audit - rust-deny - secret-scan - - semgrep - codeql - sbom-rust if: always() @@ -251,7 +219,6 @@ jobs: ["rust-audit"]="${{ needs.rust-audit.result }}" ["rust-deny"]="${{ needs.rust-deny.result }}" ["secret-scan"]="${{ needs.secret-scan.result }}" - ["semgrep"]="${{ needs.semgrep.result }}" ["codeql"]="${{ needs.codeql.result }}" ["sbom-rust"]="${{ needs.sbom-rust.result }}" ) @@ -564,34 +531,6 @@ jobs: with: category: "/language:${{matrix.language}}" - audit_semgrep: - name: Semgrep SAST - runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: read - security-events: write - steps: - - name: Checkout repository - uses: actions/checkout@v7.0.1 - - - name: Run Semgrep - uses: returntocorp/semgrep-action@v1 - with: - config: >- - p/rust - p/security-audit - p/secrets - p/dockerfile - p/supply-chain - args: --sarif --output=semgrep.sarif - - - name: Upload SARIF results - uses: github/codeql-action/upload-sarif@v4.37.8 - if: always() - with: - sarif_file: semgrep.sarif - # ============================================================================ # License Compliance # ============================================================================ @@ -716,7 +655,6 @@ jobs: - gitleaks - trufflehog - audit_codeql - - audit_semgrep - license-check - sbom-generation if: always() @@ -745,7 +683,6 @@ jobs: echo "| Gitleaks | $(status_icon '${{ needs.gitleaks.result }}') ${{ needs.gitleaks.result }} |" >> $GITHUB_STEP_SUMMARY echo "| TruffleHog | $(status_icon '${{ needs.trufflehog.result }}') ${{ needs.trufflehog.result }} |" >> $GITHUB_STEP_SUMMARY echo "| CodeQL | $(status_icon '${{ needs.audit_codeql.result }}') ${{ needs.audit_codeql.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Semgrep | $(status_icon '${{ needs.audit_semgrep.result }}') ${{ needs.audit_semgrep.result }} |" >> $GITHUB_STEP_SUMMARY echo "| License Check | $(status_icon '${{ needs.license-check.result }}') ${{ needs.license-check.result }} |" >> $GITHUB_STEP_SUMMARY echo "| SBOM Generation | $(status_icon '${{ needs.sbom-generation.result }}') ${{ needs.sbom-generation.result }} |" >> $GITHUB_STEP_SUMMARY From c7a19baf6ba4be1886e476872ed65b52b9ef6550 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 17:32:20 +0100 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=94=A7=20CodeRabbit=20CI=20Fix:=20Fix?= =?UTF-8?q?=2016=20Failing=20CI=20Checks=20(#756)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI failure fixes was requested by @hyperpolymath. * https://github.com/hyperpolymath/hypatia/pull/740#issuecomment-5479895667 The following files were modified: * `lib/fleet_dispatcher.ex` * `lib/hypatia/diagnostics/monitor.ex` * `lib/hypatia/web/api_router.ex` * `lib/hypatia/web/router.ex` * `lib/merge_orchestration/strategist.ex` * `lib/neural/prover_recommender.ex` * `lib/rules/admin_merge_eligibility.ex` * `lib/rules/rules.ex` * `lib/vcl/client.ex` Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/fleet_dispatcher.ex | 150 +++++++++++++------------- lib/hypatia/diagnostics/monitor.ex | 4 +- lib/hypatia/web/api_router.ex | 78 ++++++-------- lib/hypatia/web/router.ex | 60 +++++------ lib/merge_orchestration/strategist.ex | 49 ++++++--- lib/neural/prover_recommender.ex | 4 +- lib/rules/admin_merge_eligibility.ex | 6 +- lib/rules/rules.ex | 1 - lib/vcl/client.ex | 16 +-- 9 files changed, 182 insertions(+), 186 deletions(-) diff --git a/lib/fleet_dispatcher.ex b/lib/fleet_dispatcher.ex index c7cc1f07..072c0e49 100644 --- a/lib/fleet_dispatcher.ex +++ b/lib/fleet_dispatcher.ex @@ -45,54 +45,6 @@ defmodule Hypatia.FleetDispatcher do end end - # Standard fleet dispatch path for eliminate tier. - defp dispatch_eliminate_via_fleet(recipe, pattern) do - confidence = Map.get(recipe, "confidence", 0.0) - strategy = TriangleRouter.dispatch_strategy(confidence) - - bot_id = - case strategy do - :auto_execute -> "robot-repo-automaton" - :review -> "rhodibot" - :report_only -> "sustainabot" - end - - action_type = - case strategy do - :auto_execute -> :commit_push - :review -> :pr_create - :report_only -> :advisory - end - - # Gate review -- every action must pass through the Kin Gate - gate_action = %{ - bot_id: bot_id, - repo: get_pattern_repo(pattern), - action_type: action_type, - confidence: confidence, - pattern_id: Map.get(pattern, "id", Map.get(pattern, "description", "")), - scan_timestamp: Map.get(pattern, "scan_timestamp"), - dispatch_tier: strategy - } - - case gate_review(gate_action) do - {:approved, _} -> - do_eliminate_dispatch(strategy, recipe, pattern, confidence) - - {:held, reason} -> - Logger.warning("Gate held eliminate dispatch: #{reason}") - {:ok, :held} - - {:rejected, reason} -> - Logger.warning("Gate rejected eliminate dispatch: #{reason}") - {:error, :gate_rejected, reason} - - {:deferred, wait_ms} -> - Logger.info("Gate deferred eliminate dispatch -- retry in #{div(wait_ms, 1000)}s") - {:ok, :deferred} - end - end - def dispatch_routed_action({:substitute, recipe, pattern}) do proven_module = Map.get(recipe, "proven_module", "unknown") @@ -123,22 +75,20 @@ defmodule Hypatia.FleetDispatcher do }) end - @doc """ - Dispatch a ProofObligation recipe through the Safety Triangle. - - Called by `ProofObligation.obligations_from_patterns/2` and any code - that constructs `{:proof_obligation, recipe, pattern}` tuples. - - Triangle routing for proof obligations: - - `:eliminate` (auto-provable, confidence >= 0.90) → - robot-repo-automaton applies tactic inline - - `:eliminate` (confidence < 0.90) → - echidnabot with eliminate-tier hint - - `:substitute` → - echidnabot with VeriSimDB-recommended prover hint - - `:control` → - sustainabot advisory (sorry/Admitted present, human required) - """ + # Dispatch a ProofObligation recipe through the Safety Triangle. + # + # Called by `ProofObligation.obligations_from_patterns/2` and any code + # that constructs `{:proof_obligation, recipe, pattern}` tuples. + # + # Triangle routing for proof obligations: + # - `:eliminate` (auto-provable, confidence >= 0.90) -> + # robot-repo-automaton applies tactic inline + # - `:eliminate` (confidence < 0.90) -> + # echidnabot with eliminate-tier hint + # - `:substitute` -> + # echidnabot with VeriSimDB-recommended prover hint + # - `:control` -> + # sustainabot advisory (sorry/Admitted present, human required) def dispatch_routed_action({:proof_obligation, recipe, pattern}) do tier = Map.get(recipe, "triangle_tier", "substitute") claim = Map.get(recipe, "claim", Map.get(pattern, "description", "")) @@ -202,19 +152,17 @@ defmodule Hypatia.FleetDispatcher do end end - @doc """ - Dispatch a DependabotAlerts recipe through the Safety Triangle. - - Called by `DependabotAlerts.fixes_from_alerts/3` and any code that - constructs `{:dependabot_fix, recipe, pattern}` tuples. - - Triangle routing for Dependabot alerts: - - `:eliminate` + confidence >= 0.95 -> robot-repo-automaton auto-bumps - (subject to Kin Gate, rate limiter, exclusion registry) - - `:eliminate` + confidence in [0.85, 0.95) -> rhodibot opens a PR - - `:substitute` -> rhodibot opens a PR (major bump / breaking change) - - `:control` -> sustainabot advisory (no auto-fix path) - """ + # Dispatch a DependabotAlerts recipe through the Safety Triangle. + # + # Called by `DependabotAlerts.fixes_from_alerts/3` and any code that + # constructs `{:dependabot_fix, recipe, pattern}` tuples. + # + # Triangle routing for Dependabot alerts: + # - `:eliminate` + confidence >= 0.95 -> robot-repo-automaton auto-bumps + # (subject to Kin Gate, rate limiter, exclusion registry) + # - `:eliminate` + confidence in [0.85, 0.95) -> rhodibot opens a PR + # - `:substitute` -> rhodibot opens a PR (major bump / breaking change) + # - `:control` -> sustainabot advisory (no auto-fix path) def dispatch_routed_action({:dependabot_fix, recipe, pattern}) do tier = Map.get(recipe, "triangle_tier", "control") confidence = Map.get(recipe, "confidence", 0.5) @@ -271,6 +219,54 @@ defmodule Hypatia.FleetDispatcher do # --- Eliminate dispatch helpers (called after Gate approval) --- + # Standard fleet dispatch path for eliminate tier. + defp dispatch_eliminate_via_fleet(recipe, pattern) do + confidence = Map.get(recipe, "confidence", 0.0) + strategy = TriangleRouter.dispatch_strategy(confidence) + + bot_id = + case strategy do + :auto_execute -> "robot-repo-automaton" + :review -> "rhodibot" + :report_only -> "sustainabot" + end + + action_type = + case strategy do + :auto_execute -> :commit_push + :review -> :pr_create + :report_only -> :advisory + end + + # Gate review -- every action must pass through the Kin Gate + gate_action = %{ + bot_id: bot_id, + repo: get_pattern_repo(pattern), + action_type: action_type, + confidence: confidence, + pattern_id: Map.get(pattern, "id", Map.get(pattern, "description", "")), + scan_timestamp: Map.get(pattern, "scan_timestamp"), + dispatch_tier: strategy + } + + case gate_review(gate_action) do + {:approved, _} -> + do_eliminate_dispatch(strategy, recipe, pattern, confidence) + + {:held, reason} -> + Logger.warning("Gate held eliminate dispatch: #{reason}") + {:ok, :held} + + {:rejected, reason} -> + Logger.warning("Gate rejected eliminate dispatch: #{reason}") + {:error, :gate_rejected, reason} + + {:deferred, wait_ms} -> + Logger.info("Gate deferred eliminate dispatch -- retry in #{div(wait_ms, 1000)}s") + {:ok, :deferred} + end + end + defp do_eliminate_dispatch(:auto_execute, recipe, pattern, confidence) do recipe_id = Map.get(recipe, "id") diff --git a/lib/hypatia/diagnostics/monitor.ex b/lib/hypatia/diagnostics/monitor.ex index b30667f9..f602ab53 100644 --- a/lib/hypatia/diagnostics/monitor.ex +++ b/lib/hypatia/diagnostics/monitor.ex @@ -158,12 +158,12 @@ defmodule Hypatia.Diagnostics.Monitor do nil -> {:error, :neural_unresponsive} status -> {:ok, {:neural, status}} end + rescue + _ -> {:error, :neural_crashed} catch # Training cycles can take minutes -- a timeout means busy, not crashed :exit, {:timeout, _} -> {:ok, {:neural, :training_in_progress}} :exit, _ -> {:error, :neural_crashed} - rescue - _ -> {:error, :neural_crashed} end end diff --git a/lib/hypatia/web/api_router.ex b/lib/hypatia/web/api_router.ex index f771c91f..1f67528e 100644 --- a/lib/hypatia/web/api_router.ex +++ b/lib/hypatia/web/api_router.ex @@ -64,11 +64,9 @@ defmodule Hypatia.Web.ApiRouter do end end - @doc """ - GET /api/recipes/:id -- single-recipe drill-down. Returns the same - shape as one row from `/api/recipes`, plus the recipe definition - itself when found in the registry. - """ + # GET /api/recipes/:id -- single-recipe drill-down. Returns the same + # shape as one row from `/api/recipes`, plus the recipe definition + # itself when found in the registry. get "/recipes/:id" do health = Hypatia.OutcomeTracker.recipe_health() row = Enum.find(health, &(&1.recipe_id == id)) @@ -81,11 +79,9 @@ defmodule Hypatia.Web.ApiRouter do end end - @doc """ - GET /api/quarantine -- everything currently auto-quarantined: - recipes (verification-rate gate) and bots (consecutive-failure / - FP-rate gate from Hypatia.Safety.Quarantine). - """ + # GET /api/quarantine -- everything currently auto-quarantined: + # recipes (verification-rate gate) and bots (consecutive-failure / + # FP-rate gate from Hypatia.Safety.Quarantine). get "/quarantine" do recipes = Hypatia.OutcomeTracker.recipe_health() @@ -103,11 +99,9 @@ defmodule Hypatia.Web.ApiRouter do }) end - @doc """ - GET /api/alerts -- Recent threshold-rule alerts emitted by - Hypatia.Watcher.Alerts (ring buffer, newest first). Powers the - dashboard alert ribbon and supports manual triage. - """ + # GET /api/alerts -- Recent threshold-rule alerts emitted by + # Hypatia.Watcher.Alerts (ring buffer, newest first). Powers the + # dashboard alert ribbon and supports manual triage. get "/alerts" do rows = case Process.whereis(Hypatia.Watcher.Alerts) do @@ -118,20 +112,18 @@ defmodule Hypatia.Web.ApiRouter do json(conn, 200, %{count: length(rows), rows: rows}) end - @doc """ - POST /api/alerts/ingest -- Federation ingress. Peer hypatia - instances POST their alerts here via the Peer sink. - - Auth: the auth_gate plug enforces a valid bearer token, so this - endpoint is only reachable when HYPATIA_API_BEARER_TOKEN is set - and the request carries it. Federation without shared auth is - refused at the gate, not here. - - Loop prevention: the ingested alert is tagged with - `metadata.federated_from = ` so the - Peer sink can skip it on broadcast and the dashboard can - attribute it. - """ + # POST /api/alerts/ingest -- Federation ingress. Peer hypatia + # instances POST their alerts here via the Peer sink. + # + # Auth: the auth_gate plug enforces a valid bearer token, so this + # endpoint is only reachable when HYPATIA_API_BEARER_TOKEN is set + # and the request carries it. Federation without shared auth is + # refused at the gate, not here. + # + # Loop prevention: the ingested alert is tagged with + # `metadata.federated_from = ` so the + # Peer sink can skip it on broadcast and the dashboard can + # attribute it. post "/alerts/ingest" do {:ok, body, conn} = Plug.Conn.read_body(conn) @@ -174,20 +166,18 @@ defmodule Hypatia.Web.ApiRouter do defp parse_atom(_), do: :unknown - @doc """ - GET /api/events -- Server-Sent Events stream of telemetry as it - fires. Each event arrives as - - event: hypatia.scan.complete - data: {"measurements": {...}, "metadata": {...}, "at": ms} - - Optional `?events=hypatia.scan.complete,hypatia.outcome.recorded` - filter narrows the stream to specific event kinds. - - Heartbeats every 15s as comment lines (`: keepalive`) defeat proxy - idle-timeouts. The handler exits cleanly when the client disconnects - (Bandit closes the chunked response). - """ + # GET /api/events -- Server-Sent Events stream of telemetry as it + # fires. Each event arrives as + # + # event: hypatia.scan.complete + # data: {"measurements": {...}, "metadata": {...}, "at": ms} + # + # Optional `?events=hypatia.scan.complete,hypatia.outcome.recorded` + # filter narrows the stream to specific event kinds. + # + # Heartbeats every 15s as comment lines (`: keepalive`) defeat proxy + # idle-timeouts. The handler exits cleanly when the client disconnects + # (Bandit closes the chunked response). get "/events" do conn = Plug.Conn.fetch_query_params(conn) filter = parse_event_filter(conn.query_params["events"]) @@ -368,7 +358,7 @@ defmodule Hypatia.Web.ApiRouter do a |> :binary.bin_to_list() |> Enum.zip(:binary.bin_to_list(b)) - |> Enum.reduce(0, fn {x, y}, acc -> acc ||| Bitwise.bxor(x, y) end) == 0 + |> Enum.reduce(0, fn {x, y}, acc -> acc ||| bxor(x, y) end) == 0 end end diff --git a/lib/hypatia/web/router.ex b/lib/hypatia/web/router.ex index c9b30eac..e2098bc9 100644 --- a/lib/hypatia/web/router.ex +++ b/lib/hypatia/web/router.ex @@ -28,20 +28,16 @@ defmodule Hypatia.Web.Router do plug(:match) plug(:dispatch) - @doc """ - GET / -- Single-page live operational dashboard. HTML + vanilla JS, - polls /api/status and EventSource-streams /api/events. The dashboard - itself is publicly reachable; the data endpoints it calls are - loopback-only (gated in ApiRouter), so a non-local browser would - render the chrome but get 403 from the XHR/SSE calls. - """ + # GET / -- Single-page live operational dashboard. HTML + vanilla JS, + # polls /api/status and EventSource-streams /api/events. The dashboard + # itself is publicly reachable; the data endpoints it calls are + # loopback-only (gated in ApiRouter), so a non-local browser would + # render the chrome but get 403 from the XHR/SSE calls. get "/" do Hypatia.Web.Dashboard.call(conn, []) end - @doc """ - GET /health -- Basic health check for the HTTP endpoint. - """ + # GET /health -- Basic health check for the HTTP endpoint. get "/health" do health = %{ status: "ok", @@ -54,27 +50,23 @@ defmodule Hypatia.Web.Router do |> send_resp(200, Jason.encode!(health)) end - @doc """ - GET /metrics -- Prometheus text-format exposition. Publicly - reachable (NOT loopback-only) because scrapers routinely run on a - different host; there's no operational data in the metric body - that isn't already implied by the dashboard's existence. - """ + # GET /metrics -- Prometheus text-format exposition. Publicly + # reachable (NOT loopback-only) because scrapers routinely run on a + # different host; there's no operational data in the metric body + # that isn't already implied by the dashboard's existence. get "/metrics" do Hypatia.Web.Metrics.call(conn, []) end - @doc """ - GET /metrics/snapshot -- Compact JSON snapshot of estate-level - counters: repos scanned, weak points, dispatched actions, outcomes, - recipes, average confidence. Consumed by the optional Ada TUI - (`lib/tui/port.ex`) on its 10s tick, and useful as a single-call - status read for external dashboards. - - Reads from the verisim-data flat-file store via VerisimConnector; - any failure returns a degraded snapshot with status="degraded" - rather than 500, so the TUI keeps rendering. - """ + # GET /metrics/snapshot -- Compact JSON snapshot of estate-level + # counters: repos scanned, weak points, dispatched actions, outcomes, + # recipes, average confidence. Consumed by the optional Ada TUI + # (`lib/tui/port.ex`) on its 10s tick, and useful as a single-call + # status read for external dashboards. + # + # Reads from the verisim-data flat-file store via VerisimConnector; + # any failure returns a degraded snapshot with status="degraded" + # rather than 500, so the TUI keeps rendering. get "/metrics/snapshot" do snapshot = Hypatia.Web.MetricsSnapshot.build() @@ -88,14 +80,12 @@ defmodule Hypatia.Web.Router do # reachable for container orchestrators. forward("/api", to: Hypatia.Web.ApiRouter) - @doc """ - POST /graphql -- GraphQL-shaped query endpoint (M14). - - Minimal hand-rolled implementation; no introspection, no schema - federation, no Absinthe dep. See lib/hypatia/web/graphql.ex for - the supported field set and limitations. Loopback-only by sharing - the bearer-auth gate when HYPATIA_API_BEARER_TOKEN is configured. - """ + # POST /graphql -- GraphQL-shaped query endpoint (M14). + # + # Minimal hand-rolled implementation; no introspection, no schema + # federation, no Absinthe dep. See lib/hypatia/web/graphql.ex for + # the supported field set and limitations. Loopback-only by sharing + # the bearer-auth gate when HYPATIA_API_BEARER_TOKEN is configured. post "/graphql" do Hypatia.Web.GraphQL.call(conn, []) end diff --git a/lib/merge_orchestration/strategist.ex b/lib/merge_orchestration/strategist.ex index e0ae384b..00c85d53 100644 --- a/lib/merge_orchestration/strategist.ex +++ b/lib/merge_orchestration/strategist.ex @@ -115,36 +115,55 @@ defmodule Hypatia.MergeOrchestration.Strategist do vetoes = [] # License/SPDX touch veto - if Map.get(ctx, :license_touch, false) do - vetoes = [%{bot: "policy-gate", reason: "license/SPDX -- owner-only"} | vetoes] - end + vetoes = + if Map.get(ctx, :license_touch, false) do + [%{bot: "policy-gate", reason: "license/SPDX -- owner-only"} | vetoes] + else + vetoes + end # DO NOT MERGE / WIP in title (case-insensitive) title = Map.get(ctx, :title, "") title_lower = String.downcase(title) - if String.contains?(title_lower, "do not merge") || String.contains?(title_lower, "wip") do - vetoes = [%{bot: "policy-gate", reason: "title contains DO NOT MERGE or WIP"} | vetoes] - end + + vetoes = + if String.contains?(title_lower, "do not merge") || String.contains?(title_lower, "wip") do + [%{bot: "policy-gate", reason: "title contains DO NOT MERGE or WIP"} | vetoes] + else + vetoes + end # do-not-merge / hold labels (case-insensitive) labels = Map.get(ctx, :labels, []) hold_labels = ["do-not-merge", "hold", "do not merge"] labels_lower = Enum.map(labels, &String.downcase/1) - if Enum.any?(labels_lower, &(&1 in hold_labels)) do - vetoes = [%{bot: "policy-gate", reason: "PR has do-not-merge or hold label"} | vetoes] - end + + vetoes = + if Enum.any?(labels_lower, &(&1 in hold_labels)) do + [%{bot: "policy-gate", reason: "PR has do-not-merge or hold label"} | vetoes] + else + vetoes + end # litmus/ or test/ branch prefix branch = Map.get(ctx, :branch, "") - if String.starts_with?(branch, "litmus/") || String.starts_with?(branch, "test/") do - vetoes = [%{bot: "policy-gate", reason: "branch prefix litmus/ or test/"} | vetoes] - end + + vetoes = + if String.starts_with?(branch, "litmus/") || String.starts_with?(branch, "test/") do + [%{bot: "policy-gate", reason: "branch prefix litmus/ or test/"} | vetoes] + else + vetoes + end # Draft state state = Map.get(ctx, :state, "open") - if state == "draft" do - vetoes = [%{bot: "policy-gate", reason: "PR is in draft state"} | vetoes] - end + + vetoes = + if state == "draft" do + [%{bot: "policy-gate", reason: "PR is in draft state"} | vetoes] + else + vetoes + end vetoes end diff --git a/lib/neural/prover_recommender.ex b/lib/neural/prover_recommender.ex index 8f5a6887..b41b9e70 100644 --- a/lib/neural/prover_recommender.ex +++ b/lib/neural/prover_recommender.ex @@ -120,7 +120,7 @@ defmodule Hypatia.Neural.ProverRecommender do # --- verisim-api bridge --------------------------------------------------- - defp fetch_attempts(limit, base_url \\ nil) do + defp fetch_attempts(limit, base_url) do resolved_url = base_url || @verisim_base_url url = "#{resolved_url}/api/v1/proof_attempts?limit=#{limit}" # verisim-api /proof_attempts GET doesn't exist yet -- fall back to ClickHouse @@ -131,7 +131,7 @@ defmodule Hypatia.Neural.ProverRecommender do end end - defp fetch_attempts_via_clickhouse(limit, base_url \\ nil) do + defp fetch_attempts_via_clickhouse(limit, base_url) do resolved_url = base_url || @verisim_base_url # ClickHouse HTTP: reach it by probing each active class's strategy endpoint # and folding the recommendations back into synthetic attempt rows. diff --git a/lib/rules/admin_merge_eligibility.ex b/lib/rules/admin_merge_eligibility.ex index 5d3a3a7a..409858ff 100644 --- a/lib/rules/admin_merge_eligibility.ex +++ b/lib/rules/admin_merge_eligibility.ex @@ -253,7 +253,9 @@ defmodule Hypatia.Rules.AdminMergeEligibility do Returns `:stalled` if the heuristic fires, `:ok` otherwise. """ @spec dependabot_stalled?(map(), pos_integer()) :: :stalled | :ok - def dependabot_stalled?(%{author: %{login: "dependabot[bot]"}} = pr, days_threshold \\ 7) do + def dependabot_stalled?(pr, days_threshold \\ 7) + + def dependabot_stalled?(%{author: %{login: "dependabot[bot]"}} = pr, days_threshold) do created = Map.get(pr, :createdAt, "") has_review_requests = Map.get(pr, :reviewRequests, []) != [] @@ -360,7 +362,7 @@ defmodule Hypatia.Rules.AdminMergeEligibility do """ @spec obsolete_supersedes?(map(), (String.t() -> String.t() | nil)) :: {:obsolete, String.t()} | :not_obsolete - def obsolete_supersedes?(%{files: files} = pr, main_lookup) + def obsolete_supersedes?(%{files: files} = _pr, main_lookup) when is_function(main_lookup, 1) do # Look at workflow YAML edits that change a `uses: ...@` line. Enum.find_value(files, :not_obsolete, fn file -> diff --git a/lib/rules/rules.ex b/lib/rules/rules.ex index 6f5dfe5f..83d121b8 100644 --- a/lib/rules/rules.ex +++ b/lib/rules/rules.ex @@ -28,7 +28,6 @@ defmodule Hypatia.Rules do alias Hypatia.Rules.WorkflowHardening alias Hypatia.Rules.SupplyChain alias Hypatia.Rules.BranchProtection - alias Hypatia.Rules.AdminMergeEligibility # alias Hypatia.Rules.ResearchExtensions # wired in follow-up after PR #325 merges @doc """ diff --git a/lib/vcl/client.ex b/lib/vcl/client.ex index 9b7487de..7e14ff3a 100644 --- a/lib/vcl/client.ex +++ b/lib/vcl/client.ex @@ -123,14 +123,6 @@ defmodule Hypatia.VCL.Client do end end - # Route a parsed AST to the right executor. Multi-URL remote federation - # goes to RemoteExecutor; every other source stays on FileExecutor. - defp dispatch(%{source: {:federation_remote, urls, _policy}} = ast, opts) do - Hypatia.VCL.RemoteExecutor.execute(urls, ast, opts) - end - - defp dispatch(ast, opts), do: Hypatia.VCL.FileExecutor.execute(ast, opts) - @impl true def handle_call(:stats, _from, state) do {:reply, @@ -141,6 +133,14 @@ defmodule Hypatia.VCL.Client do }, state} end + # Route a parsed AST to the right executor. Multi-URL remote federation + # goes to RemoteExecutor; every other source stays on FileExecutor. + defp dispatch(%{source: {:federation_remote, urls, _policy}} = ast, opts) do + Hypatia.VCL.RemoteExecutor.execute(urls, ast, opts) + end + + defp dispatch(ast, opts), do: Hypatia.VCL.FileExecutor.execute(ast, opts) + # --------------------------------------------------------------------------- # Built-in VCL Parser (derived from VeriSim.Query.VQLBridge) # ---------------------------------------------------------------------------