Skip to main content

lib/mix/tasks/scoria.milestone.archive_thread.ex

defmodule Mix.Tasks.Scoria.Milestone.ArchiveThread do
  use Mix.Task

  @shortdoc "Archives a planning thread with v2.6 closeout resolution metadata"

  @threads_dir ".planning/threads"

  @archives %{
    "warning-ratchet-followup" => %{
      file: "2026-05-27-warning-ratchet-followup.md",
      status: "archived (superseded — shipped v2.6)",
      resolution: """
      ## Resolution

      Shipped v2.6 Warning Ratchet milestone (phases 66–69).

      - **Phases:** 66 Baseline Expiry And Inventory, 67 High-Signal Ratchet, 68 Full-Suite Warning Closure, 69 CI Trust And Milestone Closeout
      - **Requirements:** WARN-03, WARN-04, WARN-05, WARN-06, WARN-07, CI-03
      - **Evidence:** `mix scoria.test.ci_trust`, `.planning/milestones/v2.6-MILESTONE-AUDIT.md`

      **v2.7 follow-up (deferred):** Hex publish, README docs-truth, optional knowledge WAE in CI — see Phase 69 CONTEXT deferred section.
      """
    }
  }

  @impl Mix.Task
  def run([slug]) when is_binary(slug) do
    case Map.get(@archives, slug) do
      nil ->
        Mix.raise("unknown thread slug #{inspect(slug)}; known: #{Map.keys(@archives) |> Enum.join(", ")}")

      %{file: file, status: status, resolution: resolution} ->
        path = Path.join([@threads_dir, file])

        unless File.exists?(path) do
          Mix.raise("thread file not found: #{path}")
        end

        content = File.read!(path)

        updated =
          content
          |> replace_status(status)
          |> append_resolution_if_missing(resolution)

        File.write!(path, updated)
        Mix.shell().info("Archived thread #{slug}#{path}")
    end
  end

  def run(_args) do
    Mix.raise("usage: mix scoria.milestone.archive_thread <slug>")
  end

  defp replace_status(content, status) do
    Regex.replace(~r/\*\*Status:\*\* .+/, content, "**Status:** #{status}", global: false)
  end

  defp append_resolution_if_missing(content, resolution) do
    if content =~ "## Resolution" do
      content
    else
      String.trim_trailing(content) <> "\n\n" <> resolution
    end
  end
end