defmodule Mix.Tasks.CommBus.Simulate do
use Mix.Task
@shortdoc "Simulate a CommBus plan using local YAML fixtures"
@moduledoc """
Run CommBus planning locally using YAML fixtures for conversations and entries.
## Examples
mix comm_bus.simulate --conversation priv/comm_bus/conversation.yml --entries priv/comm_bus/entries.yml --total 8000 --completion 1000
mix comm_bus.simulate --conversation priv/conv.yml --store CommBus.Storage.Ecto
"""
@switches [
conversation: :string,
entries: :string,
store: :string,
total: :integer,
completion: :integer,
section: :keep,
methodology: :keep
]
alias CommBus.{CLI, Context, Methodologies}
@doc """
Runs a full CommBus assembly simulation using YAML fixture files for the
conversation and entries, printing the plan with included/excluded entries,
token usage, and exclusion reasons.
## Parameters
- `args` — Command-line argument list; requires `--conversation`.
"""
@spec run([String.t()]) :: :ok
def run(args) do
Mix.Task.run("app.start")
{opts, _, _} = OptionParser.parse(args, switches: @switches)
conversation_path = opts[:conversation] || Mix.raise("--conversation path is required")
conversation = CLI.conversation_from_file!(conversation_path)
entries =
load_entries(opts[:entries]) ++
load_store_entries(opts) ++
Methodologies.entries_for(Keyword.get_values(opts, :methodology))
if entries == [] do
Mix.raise("no entries provided via --entries or storage adapter")
end
plan_opts = build_plan_opts(opts)
plan = Context.plan(conversation, entries, plan_opts)
print_plan(plan)
end
defp load_entries(nil), do: []
defp load_entries(path), do: CLI.entries_from_file!(path)
defp load_store_entries(opts) do
store = CLI.resolve_entry_store(opts)
case store.list_entries([]) do
{:ok, entries} ->
entries
{:error, reason} ->
Mix.raise("unable to load entries from #{inspect(store)}: #{inspect(reason)}")
end
end
defp build_plan_opts(opts) do
ratios = CLI.parse_section_ratios(Keyword.get_values(opts, :section))
plan_keywords =
CLI.budget_plan_opts(
total: opts[:total],
completion: opts[:completion],
section_ratios: if(ratios == %{}, do: nil, else: ratios)
)
case plan_keywords do
nil -> []
plan -> [budget: %{plan: plan}]
end
end
defp print_plan(plan) do
Mix.shell().info("Conversation: #{inspect(plan.conversation.id)}")
Mix.shell().info("Included entries: #{length(plan.included_entries)}")
Mix.shell().info("Excluded entries: #{length(plan.exclusions)}")
Mix.shell().info("Token usage: #{inspect(plan.token_usage)}")
Mix.shell().info("\nSections:")
plan.sections
|> Enum.each(fn {section, entries} ->
Mix.shell().info(" #{section} (#{length(entries)} entries)")
end)
Mix.shell().info("\nExclusions:")
if plan.exclusions == [] do
Mix.shell().info(" (none)")
else
plan.exclusions
|> Enum.each(fn exclusion ->
Mix.shell().info(
" - #{inspect(exclusion.reason)} #{inspect(exclusion.entry.id || exclusion.entry.content)} #{inspect(exclusion.details)}"
)
end)
end
end
end