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 fce5028a..b9305b02 100644 --- a/lib/rules/structural_drift.ex +++ b/lib/rules/structural_drift.ex @@ -143,8 +143,12 @@ 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 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). """ def sd004_misplaced_a2ml(repo_path) do @a2ml_state_files @@ -161,8 +165,12 @@ 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, + advisory: true, + 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/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 1b43057c..02b44fea 100644 --- a/test/structural_drift_test.exs +++ b/test/structural_drift_test.exs @@ -121,6 +121,11 @@ 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.advisory == true + assert finding.reason =~ "not a security issue" + assert finding.reason =~ "deed" end test "accepts a descriptile in canonical descriptiles/", %{repo: repo} do