defmodule NPM.IntegrityCheck do @moduledoc """ Verifies installed packages match lockfile entries. Checks version, integrity, and presence of packages in node_modules. """ @doc """ Verifies a single package against its lockfile entry. """ @spec verify_package(String.t(), map(), String.t()) :: :ok | {:error, atom()} def verify_package(name, entry, node_modules \\ "node_modules") do pkg_json_path = Path.join([node_modules, name, "package.json"]) case File.read(pkg_json_path) do {:ok, content} -> installed = NPM.JSON.decode!(content) check_version(installed, entry) {:error, _} -> {:error, :not_installed} end rescue _ -> {:error, :invalid_package_json} end @doc """ Verifies all lockfile packages are correctly installed. """ @spec verify_all(map(), String.t()) :: [map()] def verify_all(lockfile, node_modules \\ "node_modules") do lockfile |> Enum.flat_map(fn {name, entry} -> case verify_package(name, entry, node_modules) do :ok -> [] {:error, reason} -> [%{name: name, reason: reason}] end end) |> Enum.sort_by(& &1.name) end @doc """ Checks if all packages pass verification. """ @spec all_valid?(map(), String.t()) :: boolean() def all_valid?(lockfile, node_modules \\ "node_modules") do verify_all(lockfile, node_modules) == [] end @doc """ Groups verification failures by reason. """ @spec group_failures([map()]) :: map() def group_failures(failures) do Enum.group_by(failures, & &1.reason, & &1.name) end @doc """ Formats verification results. """ @spec format_results([map()]) :: String.t() def format_results([]), do: "All packages verified." def format_results(failures) do grouped = group_failures(failures) grouped |> Enum.sort_by(&elem(&1, 0)) |> Enum.map_join("\n", fn {reason, names} -> "#{reason}: #{Enum.join(names, ", ")}" end) end defp check_version(%{"version" => installed_version}, %{version: locked_version}) do if installed_version == locked_version, do: :ok, else: {:error, :version_mismatch} end defp check_version(%{"version" => installed_version}, %{"version" => locked_version}) do if installed_version == locked_version, do: :ok, else: {:error, :version_mismatch} end defp check_version(_, _), do: {:error, :version_mismatch} end