From 9ec8bd6b1d20fae7732bd08bb3d8e4be7aea3477 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 16:20:58 +0000 Subject: [PATCH 1/2] fix(rules): SD004 is layout drift, not a blocking critical SD004 flags A2ML descriptiles in a retired location (.machine_readable/6a2/, .machine_readable/, the root) at severity :critical. The static-analysis gate that minted repos run counts every critical as a 'critical security issue' and blocks the merge, so every repository still on the old layout was blocked (6 findings on hyperpolymath/system-et, one per descriptile). The files are not unsafe, and the advice was older than the deed rulings: descriptiles are now clauses of the repo deed, converted by kyX (standards D312, D313). Severity becomes :medium (routed to rhodibot, not blocking) and the reason says what it is. Detection, action and target are unchanged, so fixers that read them behave as before. mix test: 2471 tests, 0 failures. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0136P7ZnmBahbhfkFEP6ark4 --- lib/rules/structural_drift.ex | 14 ++++++++++---- test/structural_drift_test.exs | 4 ++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/rules/structural_drift.ex b/lib/rules/structural_drift.ex index fce5028a..9a3f202e 100644 --- a/lib/rules/structural_drift.ex +++ b/lib/rules/structural_drift.ex @@ -143,8 +143,11 @@ defmodule Hypatia.Rules.StructuralDrift do SD004: Detect descriptile files outside their canonical location. Canonical location is .machine_readable/descriptiles/.a2ml. The old .machine_readable/6a2/.a2ml directory is retired. - Severity: critical. - Action: move to .machine_readable/descriptiles/. + Severity: medium. This is layout drift, not a security finding, so it + must not block a merge: a repository still on the old layout is not + unsafe. The current canon keeps the descriptiles as clauses of the repo + deed, and the A2ML-to-deed conversion is kyX's (standards D312, D313). + Action: move to .machine_readable/descriptiles/ (unchanged for fixers). """ def sd004_misplaced_a2ml(repo_path) do @a2ml_state_files @@ -161,8 +164,11 @@ defmodule Hypatia.Rules.StructuralDrift do %{ rule: "SD004", file: Path.relative_to(path, repo_path), - severity: :critical, - reason: "Descriptile in retired location -- must be in .machine_readable/descriptiles/", + severity: :medium, + reason: + "Descriptile in retired location (layout drift, not a security issue); " <> + "the current canon keeps descriptiles as clauses of the repo deed, " <> + "converted by kyX (standards D312, D313)", action: :move, target: ".machine_readable/descriptiles/#{a2ml_file}" } diff --git a/test/structural_drift_test.exs b/test/structural_drift_test.exs index 1b43057c..33916158 100644 --- a/test/structural_drift_test.exs +++ b/test/structural_drift_test.exs @@ -121,6 +121,10 @@ defmodule Hypatia.Rules.StructuralDriftTest do assert finding.file == ".machine_readable/6a2/STATE.a2ml" assert finding.target == ".machine_readable/descriptiles/STATE.a2ml" assert finding.reason =~ "retired location" + # Layout drift must not read as a blocking security finding. + assert finding.severity == :medium + assert finding.reason =~ "not a security issue" + assert finding.reason =~ "deed" end test "accepts a descriptile in canonical descriptiles/", %{repo: repo} do From a43ab4e36fd37567883193a95e41d00c98cbc7d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 16:53:20 +0000 Subject: [PATCH 2/2] fix(rules): make SD004 advisory so the CLI gate never blocks on it At the CLI's default threshold (medium), SD004 at :medium still fell in report_sets/2's blocking set. Mark the finding advisory: true, as WF026 and WF027 are, and keep :advisory through the structural_drift normaliser, which dropped it. A CLI test runs the real normaliser on a 6a2 layout and asserts the finding is shown but not blocking; it fails without the normaliser change. mix test: 2473 tests, 0 failures. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0136P7ZnmBahbhfkFEP6ark4 --- lib/hypatia/cli.ex | 2 +- lib/rules/structural_drift.ex | 4 +++- test/cli_test.exs | 17 +++++++++++++++++ test/structural_drift_test.exs | 1 + 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/hypatia/cli.ex b/lib/hypatia/cli.ex index 004be994..464e67ba 100644 --- a/lib/hypatia/cli.ex +++ b/lib/hypatia/cli.ex @@ -1027,7 +1027,7 @@ defmodule Hypatia.CLI do reason: f.reason, action: to_string(f.action) } - |> Map.merge(Map.take(f, [:line, :category, :recipe_id, :fix_script, :target])) + |> Map.merge(Map.take(f, [:line, :category, :recipe_id, :fix_script, :target, :advisory])) end) results ++ normalized diff --git a/lib/rules/structural_drift.ex b/lib/rules/structural_drift.ex index 9a3f202e..b9305b02 100644 --- a/lib/rules/structural_drift.ex +++ b/lib/rules/structural_drift.ex @@ -145,7 +145,8 @@ defmodule Hypatia.Rules.StructuralDrift do The old .machine_readable/6a2/.a2ml directory is retired. Severity: medium. This is layout drift, not a security finding, so it must not block a merge: a repository still on the old layout is not - unsafe. The current canon keeps the descriptiles as clauses of the repo + unsafe. The finding is `advisory: true`, so the CLI never counts it + towards exit 1 at any threshold (as WF026, WF027). The current canon keeps the descriptiles as clauses of the repo deed, and the A2ML-to-deed conversion is kyX's (standards D312, D313). Action: move to .machine_readable/descriptiles/ (unchanged for fixers). """ @@ -165,6 +166,7 @@ defmodule Hypatia.Rules.StructuralDrift do rule: "SD004", file: Path.relative_to(path, repo_path), severity: :medium, + advisory: true, reason: "Descriptile in retired location (layout drift, not a security issue); " <> "the current canon keeps descriptiles as clauses of the repo deed, " <> diff --git a/test/cli_test.exs b/test/cli_test.exs index 38c60911..fea5490f 100644 --- a/test/cli_test.exs +++ b/test/cli_test.exs @@ -188,6 +188,23 @@ defmodule Hypatia.CLITest do assert {[], []} = Hypatia.CLI.report_sets(wf027, %{@base | advisory: false}) assert {^wf027, []} = Hypatia.CLI.report_sets(wf027, %{@base | advisory: true}) end + + test "SD004 survives the structural_drift normalizer and never blocks" do + dir = Path.join(System.tmp_dir!(), "hyp-sd004-#{:erlang.unique_integer([:positive])}") + File.mkdir_p!(Path.join(dir, ".machine_readable/6a2")) + on_exit(fn -> File.rm_rf!(dir) end) + File.write!(Path.join(dir, ".machine_readable/6a2/STATE.a2ml"), "[metadata]\n") + + sd004 = + dir + |> Hypatia.CLI.collect_findings([:structural_drift]) + |> Enum.filter(&(&1.type == "SD004")) + + assert [%{advisory: true, severity: "medium"}] = sd004 + + # Shown at the default threshold, but not in the blocking set. + assert {^sd004, []} = Hypatia.CLI.report_sets(sd004, @base) + end end # Regression coverage for the placeholder-message defect. diff --git a/test/structural_drift_test.exs b/test/structural_drift_test.exs index 33916158..02b44fea 100644 --- a/test/structural_drift_test.exs +++ b/test/structural_drift_test.exs @@ -123,6 +123,7 @@ defmodule Hypatia.Rules.StructuralDriftTest do assert finding.reason =~ "retired location" # Layout drift must not read as a blocking security finding. assert finding.severity == :medium + assert finding.advisory == true assert finding.reason =~ "not a security issue" assert finding.reason =~ "deed" end