From dea033c3ff9d1e2ff647306b35e9b52249024009 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 15 Sep 2026 08:58:01 +0100 Subject: [PATCH 1/2] chore: retire the ReScript migration subsystem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine files, 1698 lines, zero tests, and no reachable entry point. MigrationObserve was never registered: `doors.ex` lists exactly three tools in @tools and that module is not among them, so no MCP client could call the 392-line tool at all. The rest of the subsystem was reachable only from an `--migration-observer` argv scan and a FEEDBACK_A_TRON_MIGRATION_MODE env read, both performed inside `Application.start/2` — the same read-at-boot shape removed from the submitter in 6e91317. ReScript is banned estate-wide, so the migration this observed cannot recur. Deleted: migration_observer, batch_reviewer, report_generator, verisim_writer, the four-module pipeline/ tree, and mcp/tools/migration_observe. Dropped {:gen_stage, "~> 1.2"} and unlocked it. Unwired both optional-child clauses and the argv/env gate from application.ex. Corrected the pre-ledger comment in submitter.ex, which listed BatchReviewer as a caller that lands in the drafted clause. The consent path is untouched: nothing here changes what the CLI prints, that it refuses to send without a typed y, or that a non-tty answers no. Verified: 318 tests, 0 failures (unchanged — the subsystem had none); `mix escript.build` clean; ./feedback-o-tron --version prints v1.0.0. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HGbKA9B3XfzRSdsjLg3j33 --- elixir-mcp/lib/feedback_a_tron/application.ex | 36 +- .../lib/feedback_a_tron/batch_reviewer.ex | 222 ---------- .../mcp/tools/migration_observe.ex | 392 ------------------ .../lib/feedback_a_tron/migration_observer.ex | 300 -------------- .../lib/feedback_a_tron/pipeline/producer.ex | 60 --- .../pipeline/review_consumer.ex | 81 ---- .../feedback_a_tron/pipeline/supervisor.ex | 32 -- .../pipeline/verisim_consumer.ex | 43 -- .../lib/feedback_a_tron/report_generator.ex | 349 ---------------- elixir-mcp/lib/feedback_a_tron/submitter.ex | 2 +- .../lib/feedback_a_tron/verisim_writer.ex | 219 ---------- elixir-mcp/mix.exs | 3 - elixir-mcp/mix.lock | 1 - 13 files changed, 2 insertions(+), 1738 deletions(-) delete mode 100644 elixir-mcp/lib/feedback_a_tron/batch_reviewer.ex delete mode 100644 elixir-mcp/lib/feedback_a_tron/mcp/tools/migration_observe.ex delete mode 100644 elixir-mcp/lib/feedback_a_tron/migration_observer.ex delete mode 100644 elixir-mcp/lib/feedback_a_tron/pipeline/producer.ex delete mode 100644 elixir-mcp/lib/feedback_a_tron/pipeline/review_consumer.ex delete mode 100644 elixir-mcp/lib/feedback_a_tron/pipeline/supervisor.ex delete mode 100644 elixir-mcp/lib/feedback_a_tron/pipeline/verisim_consumer.ex delete mode 100644 elixir-mcp/lib/feedback_a_tron/report_generator.ex delete mode 100644 elixir-mcp/lib/feedback_a_tron/verisim_writer.ex diff --git a/elixir-mcp/lib/feedback_a_tron/application.ex b/elixir-mcp/lib/feedback_a_tron/application.ex index 0f1120a..57b1d7d 100644 --- a/elixir-mcp/lib/feedback_a_tron/application.ex +++ b/elixir-mcp/lib/feedback_a_tron/application.ex @@ -12,9 +12,6 @@ defmodule FeedbackATron.Application do - NetworkVerifier: Pre-flight network checks - Doors: MCP stdio server and HTTP intake, opened by `FeedbackATron.Doors` from the `:doors` application env or the `FEEDBACK_O_TRON_*` environment - - MigrationObserver: ReScript migration session tracking (optional) - - BatchReviewer: Issue review queue (optional, with migration observer) - - Pipeline.Supervisor: GenStage pipeline (optional, with migration observer) """ use Application @@ -27,10 +24,7 @@ defmodule FeedbackATron.Application do System.get_env() ) - children = - core_children() ++ - FeedbackATron.Doors.children(doors) ++ - migration_observer_children() + children = core_children() ++ FeedbackATron.Doors.children(doors) opts = [strategy: :one_for_one, name: FeedbackATron.Supervisor] Supervisor.start_link(children, opts) @@ -47,32 +41,4 @@ defmodule FeedbackATron.Application do {FeedbackATron.NetworkVerifier, enabled: true} ] end - - defp migration_observer_children do - if migration_observer_enabled?() do - [ - FeedbackATron.MigrationObserver, - FeedbackATron.BatchReviewer, - FeedbackATron.Pipeline.Supervisor - ] - else - [] - end - end - - defp migration_observer_enabled? do - env_val = System.get_env("FEEDBACK_A_TRON_MIGRATION_MODE") - - env_on? = - case env_val do - nil -> - false - - value -> - normalized = value |> String.trim() |> String.downcase() - Enum.member?(["1", "true", "yes", "on"], normalized) - end - - env_on? || Enum.any?(System.argv(), &(&1 == "--migration-observer")) - end end diff --git a/elixir-mcp/lib/feedback_a_tron/batch_reviewer.ex b/elixir-mcp/lib/feedback_a_tron/batch_reviewer.ex deleted file mode 100644 index 4383d66..0000000 --- a/elixir-mcp/lib/feedback_a_tron/batch_reviewer.ex +++ /dev/null @@ -1,222 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) Jonathan D.A. Jewell -defmodule FeedbackATron.BatchReviewer do - @moduledoc """ - ETS-backed queue for migration issues awaiting human review. - - Issues discovered during migration observation are queued here before - submission to rescript-lang/rescript or other targets. Follows the same - ETS pattern as `FeedbackATron.Deduplicator`. - - MCP tools: - - `review_migration_queue` — list pending items, approve/reject/edit - - `submit_approved_migrations` — send approved items via Submitter - """ - - use GenServer - - require Logger - - @ets_table :migration_review_queue - - defstruct [ - :queue, - :approved, - :rejected, - :submitted - ] - - # --- Client API --- - - def start_link(opts \\ []) do - GenServer.start_link(__MODULE__, opts, name: __MODULE__) - end - - @doc "Enqueue an issue for review" - def enqueue(issue) do - GenServer.cast(__MODULE__, {:enqueue, issue}) - end - - @doc "List all pending items in the review queue" - def list_pending do - GenServer.call(__MODULE__, :list_pending) - end - - @doc "Approve an item by ID" - def approve(item_id, edits \\ %{}) do - GenServer.call(__MODULE__, {:approve, item_id, edits}) - end - - @doc "Reject an item by ID with an optional reason" - def reject(item_id, reason \\ nil) do - GenServer.call(__MODULE__, {:reject, item_id, reason}) - end - - @doc "Submit all approved items via the Submitter" - def submit_approved do - GenServer.call(__MODULE__, :submit_approved, :timer.minutes(5)) - end - - @doc "Get queue statistics" - def stats do - GenServer.call(__MODULE__, :stats) - end - - # --- Server Callbacks --- - - @impl true - def init(_opts) do - :ets.new(@ets_table, [:named_table, :set, :public, read_concurrency: true]) - - state = %__MODULE__{ - queue: %{}, - approved: %{}, - rejected: %{}, - submitted: %{} - } - - Logger.info("[BatchReviewer] Started") - {:ok, state} - end - - @impl true - def handle_cast({:enqueue, issue}, state) do - item_id = generate_item_id() - - item = %{ - id: item_id, - issue: issue, - enqueued_at: DateTime.utc_now(), - status: :pending, - edits: %{}, - rejection_reason: nil - } - - :ets.insert(@ets_table, {item_id, item}) - - Logger.debug("[BatchReviewer] Enqueued: #{item_id} - #{issue.title}") - {:noreply, %{state | queue: Map.put(state.queue, item_id, item)}} - end - - @impl true - def handle_call(:list_pending, _from, state) do - pending = - state.queue - |> Enum.filter(fn {_id, item} -> item.status == :pending end) - |> Enum.map(fn {_id, item} -> item end) - |> Enum.sort_by(& &1.enqueued_at, DateTime) - - {:reply, pending, state} - end - - def handle_call({:approve, item_id, edits}, _from, state) do - case Map.get(state.queue, item_id) do - nil -> - {:reply, {:error, :not_found}, state} - - item -> - updated_item = %{item | status: :approved, edits: edits} - :ets.insert(@ets_table, {item_id, updated_item}) - - new_state = %{ - state - | queue: Map.put(state.queue, item_id, updated_item), - approved: Map.put(state.approved, item_id, updated_item) - } - - Logger.info("[BatchReviewer] Approved: #{item_id}") - {:reply, :ok, new_state} - end - end - - def handle_call({:reject, item_id, reason}, _from, state) do - case Map.get(state.queue, item_id) do - nil -> - {:reply, {:error, :not_found}, state} - - item -> - updated_item = %{item | status: :rejected, rejection_reason: reason} - :ets.insert(@ets_table, {item_id, updated_item}) - - new_state = %{ - state - | queue: Map.put(state.queue, item_id, updated_item), - rejected: Map.put(state.rejected, item_id, updated_item) - } - - Logger.info("[BatchReviewer] Rejected: #{item_id} - #{reason || "no reason"}") - {:reply, :ok, new_state} - end - end - - def handle_call(:submit_approved, _from, state) do - approved_items = - state.approved - |> Enum.filter(fn {_id, item} -> item.status == :approved end) - |> Enum.map(fn {_id, item} -> item end) - - results = - Enum.map(approved_items, fn item -> - issue = apply_edits(item.issue, item.edits) - - case FeedbackATron.Submitter.submit(issue, platforms: [:github]) do - {:ok, submission_id, results} -> - Logger.info("[BatchReviewer] Submitted: #{item.id} -> #{submission_id}") - {:ok, %{item_id: item.id, submission_id: submission_id, results: results}} - - {:error, reason} -> - Logger.error("[BatchReviewer] Submit failed: #{item.id} - #{inspect(reason)}") - {:error, %{item_id: item.id, reason: reason}} - end - end) - - # Move submitted items - submitted_ids = for {:ok, %{item_id: id}} <- results, do: id - - new_submitted = - Enum.reduce(submitted_ids, state.submitted, fn id, acc -> - case Map.get(state.approved, id) do - nil -> acc - item -> Map.put(acc, id, %{item | status: :submitted}) - end - end) - - new_approved = Map.drop(state.approved, submitted_ids) - - new_state = %{state | approved: new_approved, submitted: new_submitted} - {:reply, {:ok, results}, new_state} - end - - def handle_call(:stats, _from, state) do - stats = %{ - pending: state.queue |> Enum.count(fn {_id, i} -> i.status == :pending end), - approved: map_size(state.approved), - rejected: map_size(state.rejected), - submitted: map_size(state.submitted), - total: map_size(state.queue) - } - - {:reply, stats, state} - end - - # --- Private Helpers --- - - defp apply_edits(issue, edits) when map_size(edits) == 0, do: issue - - defp apply_edits(issue, edits) do - issue - |> then(fn i -> - if Map.has_key?(edits, :title), do: Map.put(i, :title, edits.title), else: i - end) - |> then(fn i -> - if Map.has_key?(edits, :body), do: Map.put(i, :body, edits.body), else: i - end) - |> then(fn i -> - if Map.has_key?(edits, :repo), do: Map.put(i, :repo, edits.repo), else: i - end) - end - - defp generate_item_id do - :crypto.strong_rand_bytes(6) |> Base.url_encode64(padding: false) - end -end diff --git a/elixir-mcp/lib/feedback_a_tron/mcp/tools/migration_observe.ex b/elixir-mcp/lib/feedback_a_tron/mcp/tools/migration_observe.ex deleted file mode 100644 index 5a2f14e..0000000 --- a/elixir-mcp/lib/feedback_a_tron/mcp/tools/migration_observe.ex +++ /dev/null @@ -1,392 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) Jonathan D.A. Jewell -defmodule FeedbackATron.MCP.Tools.MigrationObserve do - @moduledoc """ - MCP tool definitions for migration observation. - - Three tools following the pattern of `SubmitFeedback`: - 1. `begin_migration_observation` — start a session - 2. `log_migration_event` — log events during migration - 3. `end_migration_observation` — end session, generate diff + report - 4. `review_migration_queue` — list/approve/reject queued issues - 5. `submit_approved_migrations` — submit approved issues - """ - - alias FeedbackATron.{MigrationObserver, BatchReviewer, ReportGenerator} - - # --- Tool Definitions --- - - def tools do - [ - begin_migration_observation_tool(), - log_migration_event_tool(), - end_migration_observation_tool(), - review_migration_queue_tool(), - submit_approved_migrations_tool() - ] - end - - # --- begin_migration_observation --- - - defp begin_migration_observation_tool do - %{ - name: "begin_migration_observation", - description: """ - Begin a ReScript migration observation session. - - Takes a before-snapshot of the target repo using panic-attack, - capturing deprecated API counts, health score, version bracket, - and other migration metrics. The session stays active until - end_migration_observation is called. - """, - input_schema: %{ - type: "object", - properties: %{ - repo_path: %{ - type: "string", - description: "Absolute path to the ReScript project directory" - }, - label: %{ - type: "string", - description: "Label for this session (e.g. 'v11-to-v12', 'belt-removal')" - }, - build_time: %{ - type: "boolean", - description: "Measure build time (runs rescript build)" - }, - bundle_size: %{ - type: "boolean", - description: "Measure bundle size (scans output directory)" - } - }, - required: ["repo_path", "label"] - }, - handler: &handle_begin/1 - } - end - - def handle_begin(params) do - repo_path = params["repo_path"] - label = params["label"] - - opts = [ - build_time: params["build_time"] || false, - bundle_size: params["bundle_size"] || false - ] - - case MigrationObserver.begin_observation(repo_path, label, opts) do - {:ok, session_id} -> - {:ok, - [ - %{ - type: "text", - text: - "Migration observation started.\n" <> - "Session ID: #{session_id}\n" <> - "Repo: #{repo_path}\n" <> - "Label: #{label}\n\n" <> - "Before-snapshot captured. Proceed with migration work, " <> - "then call end_migration_observation when done.\n" <> - "Use log_migration_event to record issues, decisions, and notes." - } - ]} - - {:error, :session_active} -> - {:error, "A migration session is already active. End it first."} - - {:error, reason} -> - {:error, "Failed to start observation: #{inspect(reason)}"} - end - end - - # --- log_migration_event --- - - defp log_migration_event_tool do - %{ - name: "log_migration_event", - description: """ - Log a timestamped event during an active migration session. - - Events are captured for the migration report and may be queued - for review (issues) or stored in VeriSimDB. - """, - input_schema: %{ - type: "object", - properties: %{ - event_type: %{ - type: "string", - enum: ["issue", "complication", "note", "decision", "benchmark"], - description: "Type of event to log" - }, - description: %{ - type: "string", - description: "Description of the event" - }, - severity: %{ - type: "string", - enum: ["info", "warning", "critical"], - description: "Severity level (default: info)" - } - }, - required: ["event_type", "description"] - }, - handler: &handle_log_event/1 - } - end - - def handle_log_event(params) do - event_type = String.to_existing_atom(params["event_type"]) - description = params["description"] - - severity = - case params["severity"] do - "critical" -> :critical - "warning" -> :warning - _ -> :info - end - - case MigrationObserver.log_event(event_type, description, severity: severity) do - :ok -> - {:ok, - [ - %{ - type: "text", - text: "Event logged: [#{params["event_type"]}] #{description}" - } - ]} - - {:error, :no_active_session} -> - {:error, "No active migration session. Call begin_migration_observation first."} - - {:error, reason} -> - {:error, "Failed to log event: #{inspect(reason)}"} - end - end - - # --- end_migration_observation --- - - defp end_migration_observation_tool do - %{ - name: "end_migration_observation", - description: """ - End the current migration observation session. - - Takes an after-snapshot, computes the diff between before/after, - generates a migration report, and queues discovered issues for review. - Returns the full session summary with health score delta. - """, - input_schema: %{ - type: "object", - properties: %{ - notes: %{ - type: "string", - description: "Optional session notes for the report" - } - }, - required: [] - }, - handler: &handle_end/1 - } - end - - def handle_end(params) do - notes = params["notes"] - - case MigrationObserver.end_observation(notes) do - {:ok, session} -> - report = ReportGenerator.per_repo_report(session) - - health_delta = session.diff[:health_delta] || 0.0 - deprecated_delta = session.diff[:deprecated_delta] || 0 - - summary = - "Migration observation completed.\n\n" <> - "Session: #{session.session_id}\n" <> - "Health delta: #{format_delta(health_delta)}\n" <> - "Deprecated APIs delta: #{deprecated_delta}\n" <> - "Events logged: #{length(session.events)}\n" <> - "Issues queued for review: #{session.events |> Enum.count(&(&1.type == :issue))}\n\n" <> - "---\n\n" <> - report - - {:ok, [%{type: "text", text: summary}]} - - {:error, :no_active_session} -> - {:error, "No active migration session."} - - {:error, reason} -> - {:error, "Failed to end observation: #{inspect(reason)}"} - end - end - - # --- review_migration_queue --- - - defp review_migration_queue_tool do - %{ - name: "review_migration_queue", - description: """ - Review the migration issue queue. Lists pending items that were - discovered during migration sessions. Items can be approved, - rejected, or edited before submission. - """, - input_schema: %{ - type: "object", - properties: %{ - action: %{ - type: "string", - enum: ["list", "approve", "reject", "stats"], - description: "Action to perform (default: list)" - }, - item_id: %{ - type: "string", - description: "Item ID (required for approve/reject)" - }, - reason: %{ - type: "string", - description: "Rejection reason (for reject action)" - }, - edits: %{ - type: "object", - description: "Edits to apply before approval (title, body, repo)" - } - }, - required: [] - }, - handler: &handle_review/1 - } - end - - def handle_review(params) do - action = params["action"] || "list" - - case action do - "list" -> - pending = BatchReviewer.list_pending() - - if length(pending) == 0 do - {:ok, [%{type: "text", text: "No pending items in the review queue."}]} - else - items = - pending - |> Enum.map(fn item -> - "- **#{item.id}**: #{item.issue.title}\n Severity: #{item.issue[:severity] || "info"}\n Enqueued: #{DateTime.to_iso8601(item.enqueued_at)}" - end) - |> Enum.join("\n\n") - - {:ok, - [ - %{ - type: "text", - text: "## Pending Migration Issues (#{length(pending)})\n\n#{items}" - } - ]} - end - - "approve" -> - item_id = params["item_id"] - - if item_id do - edits = - (params["edits"] || %{}) - |> Enum.map(fn {k, v} -> {String.to_existing_atom(k), v} end) - |> Map.new() - - case BatchReviewer.approve(item_id, edits) do - :ok -> {:ok, [%{type: "text", text: "Approved: #{item_id}"}]} - {:error, :not_found} -> {:error, "Item not found: #{item_id}"} - end - else - {:error, "item_id required for approve action"} - end - - "reject" -> - item_id = params["item_id"] - reason = params["reason"] - - if item_id do - case BatchReviewer.reject(item_id, reason) do - :ok -> {:ok, [%{type: "text", text: "Rejected: #{item_id}"}]} - {:error, :not_found} -> {:error, "Item not found: #{item_id}"} - end - else - {:error, "item_id required for reject action"} - end - - "stats" -> - stats = BatchReviewer.stats() - - {:ok, - [ - %{ - type: "text", - text: - "## Review Queue Stats\n\n" <> - "Pending: #{stats.pending}\n" <> - "Approved: #{stats.approved}\n" <> - "Rejected: #{stats.rejected}\n" <> - "Submitted: #{stats.submitted}\n" <> - "Total: #{stats.total}" - } - ]} - - _ -> - {:error, "Unknown action: #{action}"} - end - end - - # --- submit_approved_migrations --- - - defp submit_approved_migrations_tool do - %{ - name: "submit_approved_migrations", - description: """ - Submit all approved migration issues via the feedback-o-tron - Submitter. Issues are sent to their configured platforms - (typically GitHub issues on rescript-lang/rescript). - """, - input_schema: %{ - type: "object", - properties: %{}, - required: [] - }, - handler: &handle_submit_approved/1 - } - end - - def handle_submit_approved(_params) do - case BatchReviewer.submit_approved() do - {:ok, results} -> - success_count = Enum.count(results, fn r -> match?({:ok, _}, r) end) - error_count = Enum.count(results, fn r -> match?({:error, _}, r) end) - - summary = - "Submitted #{success_count} issues (#{error_count} errors).\n\n" <> - (results - |> Enum.map(fn - {:ok, %{item_id: id, submission_id: sid}} -> - "- #{id}: submitted (#{sid})" - - {:error, %{item_id: id, reason: reason}} -> - "- #{id}: FAILED (#{inspect(reason)})" - end) - |> Enum.join("\n")) - - {:ok, [%{type: "text", text: summary}]} - - {:error, reason} -> - {:error, "Failed to submit: #{inspect(reason)}"} - end - end - - # --- Helpers --- - - defp format_delta(delta) when is_float(delta) do - cond do - delta > 0 -> "+#{Float.round(delta, 3)}" - delta < 0 -> "#{Float.round(delta, 3)}" - true -> "0" - end - end - - defp format_delta(_), do: "0" -end diff --git a/elixir-mcp/lib/feedback_a_tron/migration_observer.ex b/elixir-mcp/lib/feedback_a_tron/migration_observer.ex deleted file mode 100644 index e28a485..0000000 --- a/elixir-mcp/lib/feedback_a_tron/migration_observer.ex +++ /dev/null @@ -1,300 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) Jonathan D.A. Jewell -defmodule FeedbackATron.MigrationObserver do - @moduledoc """ - GenServer that orchestrates ReScript migration observation sessions. - - A migration session has three phases: - 1. `begin_observation/2` — takes a before-snapshot via panic-attack - 2. `log_event/2` — captures timestamped events during migration - 3. `end_observation/1` — takes an after-snapshot, computes diff, creates report - - Each session produces a `MigrationSession` struct that can be stored - as a VeriSimDB hexad and queued for review. - """ - - use GenServer - - require Logger - - alias FeedbackATron.{VeriSimWriter, BatchReviewer} - - defstruct [ - :current_session, - :sessions - ] - - # --- Client API --- - - def start_link(opts \\ []) do - GenServer.start_link(__MODULE__, opts, name: __MODULE__) - end - - @doc """ - Begin a migration observation session. - - Calls `panic-attack migration-snapshot` on the target repo to capture - the before state. - """ - def begin_observation(repo_path, label, opts \\ []) do - GenServer.call(__MODULE__, {:begin_observation, repo_path, label, opts}, :timer.minutes(5)) - end - - @doc """ - Log a timestamped event during an active migration session. - - Event types: :issue, :complication, :note, :decision, :benchmark - """ - def log_event(event_type, description, opts \\ []) do - GenServer.call(__MODULE__, {:log_event, event_type, description, opts}) - end - - @doc """ - End the current migration observation session. - - Takes an after-snapshot, computes the diff, generates a report, - and queues discovered issues for review. - """ - def end_observation(notes \\ nil) do - GenServer.call(__MODULE__, {:end_observation, notes}, :timer.minutes(5)) - end - - @doc "Get the current session state" - def current_session do - GenServer.call(__MODULE__, :current_session) - end - - @doc "List all completed sessions" - def list_sessions do - GenServer.call(__MODULE__, :list_sessions) - end - - # --- Server Callbacks --- - - @impl true - def init(_opts) do - state = %__MODULE__{ - current_session: nil, - sessions: [] - } - - Logger.info("[MigrationObserver] Started") - {:ok, state} - end - - @impl true - def handle_call({:begin_observation, repo_path, label, opts}, _from, state) do - if state.current_session do - {:reply, {:error, :session_active}, state} - else - build_time = Keyword.get(opts, :build_time, false) - bundle_size = Keyword.get(opts, :bundle_size, false) - - case take_snapshot(repo_path, "before-#{label}", build_time, bundle_size) do - {:ok, before_snapshot} -> - session = %{ - session_id: generate_session_id(), - repo_path: repo_path, - label: label, - started_at: DateTime.utc_now(), - ended_at: nil, - before_snapshot: before_snapshot, - after_snapshot: nil, - events: [], - diff: nil, - notes: nil, - build_time: build_time, - bundle_size: bundle_size - } - - Logger.info( - "[MigrationObserver] Session #{session.session_id} started for #{repo_path}" - ) - - {:reply, {:ok, session.session_id}, %{state | current_session: session}} - - {:error, reason} -> - {:reply, {:error, reason}, state} - end - end - end - - def handle_call({:log_event, event_type, description, opts}, _from, state) do - case state.current_session do - nil -> - {:reply, {:error, :no_active_session}, state} - - session -> - event = %{ - timestamp: DateTime.utc_now(), - type: event_type, - description: description, - severity: Keyword.get(opts, :severity, :info), - metadata: Keyword.get(opts, :metadata, %{}) - } - - updated_session = %{session | events: session.events ++ [event]} - Logger.debug("[MigrationObserver] Event logged: #{event_type} - #{description}") - {:reply, :ok, %{state | current_session: updated_session}} - end - end - - def handle_call({:end_observation, notes}, _from, state) do - case state.current_session do - nil -> - {:reply, {:error, :no_active_session}, state} - - session -> - label = "after-#{session.label}" - - case take_snapshot(session.repo_path, label, session.build_time, session.bundle_size) do - {:ok, after_snapshot} -> - diff = compute_diff(session.before_snapshot, after_snapshot) - - completed_session = %{ - session - | ended_at: DateTime.utc_now(), - after_snapshot: after_snapshot, - diff: diff, - notes: notes - } - - # Store in VeriSimDB (async, non-blocking) - spawn(fn -> VeriSimWriter.write_migration_session(completed_session) end) - - # Queue issues for review - queue_issues_for_review(completed_session) - - Logger.info( - "[MigrationObserver] Session #{session.session_id} completed " <> - "(health delta: #{format_delta(diff)})" - ) - - new_state = %{ - state - | current_session: nil, - sessions: [completed_session | state.sessions] - } - - {:reply, {:ok, completed_session}, new_state} - - {:error, reason} -> - {:reply, {:error, reason}, state} - end - end - end - - def handle_call(:current_session, _from, state) do - {:reply, state.current_session, state} - end - - def handle_call(:list_sessions, _from, state) do - {:reply, state.sessions, state} - end - - # --- Private Helpers --- - - defp take_snapshot(repo_path, label, build_time, bundle_size) do - args = - ["migration-snapshot", repo_path, "--label", label] ++ - if(build_time, do: ["--build-time"], else: []) ++ - if(bundle_size, do: ["--bundle-size"], else: []) - - case System.cmd("panic-attack", args, stderr_to_stdout: true) do - {output, 0} -> - case Jason.decode(output) do - {:ok, snapshot} -> {:ok, snapshot} - {:error, _} -> {:error, {:parse_error, "failed to parse panic-attack output"}} - end - - {output, code} -> - {:error, {:panic_attack_failed, code, output}} - end - end - - defp compute_diff(before_snapshot, after_snapshot) do - before_deprecated = - get_in(before_snapshot, ["migration_metrics", "deprecated_api_count"]) || 0 - - after_deprecated = get_in(after_snapshot, ["migration_metrics", "deprecated_api_count"]) || 0 - before_modern = get_in(before_snapshot, ["migration_metrics", "modern_api_count"]) || 0 - after_modern = get_in(after_snapshot, ["migration_metrics", "modern_api_count"]) || 0 - before_health = get_in(before_snapshot, ["migration_metrics", "health_score"]) || 0.0 - after_health = get_in(after_snapshot, ["migration_metrics", "health_score"]) || 0.0 - - %{ - health_delta: after_health - before_health, - deprecated_delta: after_deprecated - before_deprecated, - modern_delta: after_modern - before_modern, - version_before: get_in(before_snapshot, ["migration_metrics", "version_bracket"]), - version_after: get_in(after_snapshot, ["migration_metrics", "version_bracket"]), - config_before: get_in(before_snapshot, ["migration_metrics", "config_format"]), - config_after: get_in(after_snapshot, ["migration_metrics", "config_format"]) - } - end - - defp queue_issues_for_review(session) do - issues = - session.events - |> Enum.filter(fn event -> event.type == :issue end) - |> Enum.map(fn event -> - %{ - title: "[Migration] #{session.label}: #{event.description}", - body: build_issue_body(session, event), - repo: extract_repo_name(session.repo_path), - source: :migration_observer, - session_id: session.session_id, - severity: event.severity - } - end) - - Enum.each(issues, fn issue -> - BatchReviewer.enqueue(issue) - end) - end - - defp build_issue_body(session, event) do - """ - ## Migration Issue - - **Session:** #{session.session_id} - **Repo:** #{session.repo_path} - **Label:** #{session.label} - **Detected:** #{DateTime.to_iso8601(event.timestamp)} - **Severity:** #{event.severity} - - ### Description - - #{event.description} - - ### Context - - - Version bracket: #{get_in(session.before_snapshot, ["migration_metrics", "version_bracket"])} - - Health score (before): #{get_in(session.before_snapshot, ["migration_metrics", "health_score"])} - - Deprecated APIs (before): #{get_in(session.before_snapshot, ["migration_metrics", "deprecated_api_count"])} - - --- - _Generated by feedback-o-tron migration observer_ - """ - end - - defp extract_repo_name(path) do - path - |> String.split("/") - |> List.last() - end - - defp generate_session_id do - :crypto.strong_rand_bytes(8) |> Base.url_encode64(padding: false) - end - - defp format_delta(%{health_delta: delta}) do - cond do - delta > 0 -> "+#{Float.round(delta, 2)}" - delta < 0 -> "#{Float.round(delta, 2)}" - true -> "0" - end - end - - defp format_delta(_), do: "unknown" -end diff --git a/elixir-mcp/lib/feedback_a_tron/pipeline/producer.ex b/elixir-mcp/lib/feedback_a_tron/pipeline/producer.ex deleted file mode 100644 index 24c517f..0000000 --- a/elixir-mcp/lib/feedback_a_tron/pipeline/producer.ex +++ /dev/null @@ -1,60 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) Jonathan D.A. Jewell -defmodule FeedbackATron.Pipeline.Producer do - @moduledoc """ - GenStage producer that emits completed migration sessions. - - The MigrationObserver pushes completed sessions into this producer, - which then dispatches them to downstream consumers (VeriSimConsumer, - ReviewConsumer). - """ - - use GenStage - - require Logger - - def start_link(opts \\ []) do - GenStage.start_link(__MODULE__, opts, name: __MODULE__) - end - - @doc "Push a completed migration session into the pipeline" - def push_session(session) do - GenStage.cast(__MODULE__, {:push, session}) - end - - # --- GenStage Callbacks --- - - @impl true - def init(_opts) do - Logger.info("[Pipeline.Producer] Started") - {:producer, %{queue: :queue.new()}} - end - - @impl true - def handle_cast({:push, session}, %{queue: queue} = state) do - new_queue = :queue.in(session, queue) - {:noreply, [], %{state | queue: new_queue}} - end - - @impl true - def handle_demand(demand, %{queue: queue} = state) when demand > 0 do - {events, remaining} = take_from_queue(queue, demand) - {:noreply, events, %{state | queue: remaining}} - end - - defp take_from_queue(queue, count) do - take_from_queue(queue, count, []) - end - - defp take_from_queue(queue, 0, acc), do: {Enum.reverse(acc), queue} - - defp take_from_queue(queue, count, acc) do - case :queue.out(queue) do - {{:value, item}, remaining} -> - take_from_queue(remaining, count - 1, [item | acc]) - - {:empty, _} -> - {Enum.reverse(acc), queue} - end - end -end diff --git a/elixir-mcp/lib/feedback_a_tron/pipeline/review_consumer.ex b/elixir-mcp/lib/feedback_a_tron/pipeline/review_consumer.ex deleted file mode 100644 index 50602bc..0000000 --- a/elixir-mcp/lib/feedback_a_tron/pipeline/review_consumer.ex +++ /dev/null @@ -1,81 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) Jonathan D.A. Jewell -defmodule FeedbackATron.Pipeline.ReviewConsumer do - @moduledoc """ - GenStage consumer that extracts issues from completed migration sessions - and queues them for human review via BatchReviewer. - - Subscribes to the Pipeline.Producer alongside VeriSimConsumer. - """ - - use GenStage - - require Logger - - alias FeedbackATron.BatchReviewer - - def start_link(opts \\ []) do - GenStage.start_link(__MODULE__, opts, name: __MODULE__) - end - - @impl true - def init(_opts) do - Logger.info("[Pipeline.ReviewConsumer] Started") - - {:consumer, %{}, - subscribe_to: [{FeedbackATron.Pipeline.Producer, max_demand: 5, min_demand: 1}]} - end - - @impl true - def handle_events(sessions, _from, state) do - for session <- sessions do - issues = - session.events - |> Enum.filter(fn e -> e.type == :issue end) - - for event <- issues do - issue = %{ - title: "[Migration] #{session.label}: #{event.description}", - body: build_issue_body(session, event), - repo: extract_repo_name(session.repo_path), - source: :migration_pipeline, - session_id: session.session_id, - severity: event.severity - } - - BatchReviewer.enqueue(issue) - end - - if length(issues) > 0 do - Logger.info( - "[ReviewConsumer] Queued #{length(issues)} issues from session #{session.session_id}" - ) - end - end - - {:noreply, [], state} - end - - defp build_issue_body(session, event) do - """ - ## Migration Issue (Pipeline) - - **Session:** #{session.session_id} - **Repo:** #{session.repo_path} - **Label:** #{session.label} - **Detected:** #{DateTime.to_iso8601(event.timestamp)} - **Severity:** #{event.severity} - - ### Description - - #{event.description} - - --- - _Queued by feedback-o-tron pipeline_ - """ - end - - defp extract_repo_name(path) do - path |> String.split("/") |> List.last() - end -end diff --git a/elixir-mcp/lib/feedback_a_tron/pipeline/supervisor.ex b/elixir-mcp/lib/feedback_a_tron/pipeline/supervisor.ex deleted file mode 100644 index 6524321..0000000 --- a/elixir-mcp/lib/feedback_a_tron/pipeline/supervisor.ex +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) Jonathan D.A. Jewell -defmodule FeedbackATron.Pipeline.Supervisor do - @moduledoc """ - Supervisor for the migration observation GenStage pipeline. - - ``` - MigrationObserver (Producer) -> VeriSimConsumer (writes hexads) - -> ReviewConsumer (queues for review) - ``` - - Started conditionally when `--migration-observer` flag is set or - `FEEDBACK_A_TRON_MIGRATION_MODE` environment variable is truthy. - """ - - use Supervisor - - def start_link(opts \\ []) do - Supervisor.start_link(__MODULE__, opts, name: __MODULE__) - end - - @impl true - def init(_opts) do - children = [ - {FeedbackATron.Pipeline.Producer, []}, - {FeedbackATron.Pipeline.VeriSimConsumer, []}, - {FeedbackATron.Pipeline.ReviewConsumer, []} - ] - - Supervisor.init(children, strategy: :rest_for_one) - end -end diff --git a/elixir-mcp/lib/feedback_a_tron/pipeline/verisim_consumer.ex b/elixir-mcp/lib/feedback_a_tron/pipeline/verisim_consumer.ex deleted file mode 100644 index 1c925c7..0000000 --- a/elixir-mcp/lib/feedback_a_tron/pipeline/verisim_consumer.ex +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) Jonathan D.A. Jewell -defmodule FeedbackATron.Pipeline.VeriSimConsumer do - @moduledoc """ - GenStage consumer that writes completed migration sessions to VeriSimDB. - - Subscribes to the Pipeline.Producer and persists each session as a - hexad via VeriSimWriter. - """ - - use GenStage - - require Logger - - alias FeedbackATron.VeriSimWriter - - def start_link(opts \\ []) do - GenStage.start_link(__MODULE__, opts, name: __MODULE__) - end - - @impl true - def init(_opts) do - Logger.info("[Pipeline.VeriSimConsumer] Started") - - {:consumer, %{}, - subscribe_to: [{FeedbackATron.Pipeline.Producer, max_demand: 5, min_demand: 1}]} - end - - @impl true - def handle_events(sessions, _from, state) do - for session <- sessions do - case VeriSimWriter.write_migration_session(session) do - {:ok, hexad_id} -> - Logger.info("[VeriSimConsumer] Wrote hexad: #{hexad_id}") - - {:error, reason} -> - Logger.error("[VeriSimConsumer] Failed to write hexad: #{inspect(reason)}") - end - end - - {:noreply, [], state} - end -end diff --git a/elixir-mcp/lib/feedback_a_tron/report_generator.ex b/elixir-mcp/lib/feedback_a_tron/report_generator.ex deleted file mode 100644 index 56518d9..0000000 --- a/elixir-mcp/lib/feedback_a_tron/report_generator.ex +++ /dev/null @@ -1,349 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) Jonathan D.A. Jewell -defmodule FeedbackATron.ReportGenerator do - @moduledoc """ - Report generator for migration observation data. - - Three report types: - 1. **Per-repo mini report** — before/after table, issues, time taken - 2. **Cross-repo aggregation** — common pain points, average health improvement - 3. **v13 pre-release trial report** — structured for ReScript team - """ - - require Logger - - @doc """ - Generate a per-repo migration report from a completed session. - """ - def per_repo_report(session) do - before = session.before_snapshot["migration_metrics"] || %{} - after_m = session.after_snapshot["migration_metrics"] || %{} - - duration = - if session.ended_at, - do: DateTime.diff(session.ended_at, session.started_at, :second), - else: 0 - - issues = - session.events - |> Enum.filter(fn e -> e.type == :issue end) - |> Enum.map(fn e -> "- #{e.description} (#{e.severity})" end) - |> Enum.join("\n") - - complications = - session.events - |> Enum.filter(fn e -> e.type == :complication end) - |> Enum.map(fn e -> "- #{e.description}" end) - |> Enum.join("\n") - - """ - # Migration Report: #{session.label} - - **Repo:** #{extract_repo_name(session.repo_path)} - **Duration:** #{format_duration(duration)} - **Session ID:** #{session.session_id} - - ## Before / After Comparison - - | Metric | Before | After | Delta | - |--------|--------|-------|-------| - | Health Score | #{before["health_score"] || "-"} | #{after_m["health_score"] || "-"} | #{delta(before["health_score"], after_m["health_score"])} | - | Deprecated APIs | #{before["deprecated_api_count"] || "-"} | #{after_m["deprecated_api_count"] || "-"} | #{delta(before["deprecated_api_count"], after_m["deprecated_api_count"])} | - | Modern APIs | #{before["modern_api_count"] || "-"} | #{after_m["modern_api_count"] || "-"} | #{delta(before["modern_api_count"], after_m["modern_api_count"])} | - | Version | #{before["version_bracket"] || "-"} | #{after_m["version_bracket"] || "-"} | - | - | Config | #{before["config_format"] || "-"} | #{after_m["config_format"] || "-"} | - | - | Files | #{before["file_count"] || "-"} | #{after_m["file_count"] || "-"} | #{delta(before["file_count"], after_m["file_count"])} | - | Lines | #{before["rescript_lines"] || "-"} | #{after_m["rescript_lines"] || "-"} | #{delta(before["rescript_lines"], after_m["rescript_lines"])} | - - #{if issues != "", do: "## Issues Discovered\n\n#{issues}\n", else: ""} - #{if complications != "", do: "## Complications\n\n#{complications}\n", else: ""} - #{if session.notes, do: "## Notes\n\n#{session.notes}\n", else: ""} - - ## Recommendations - - #{generate_recommendations(before, after_m)} - - --- - _Generated by feedback-o-tron report generator_ - """ - end - - @doc """ - Generate a cross-repo aggregation report from multiple sessions. - """ - def cross_repo_report(sessions) when is_list(sessions) do - count = length(sessions) - - health_deltas = - sessions - |> Enum.map(fn s -> - before_h = get_in(s.before_snapshot, ["migration_metrics", "health_score"]) || 0.0 - after_h = get_in(s.after_snapshot, ["migration_metrics", "health_score"]) || 0.0 - after_h - before_h - end) - - avg_health_delta = - if count > 0, do: Enum.sum(health_deltas) / count, else: 0.0 - - total_deprecated_removed = - sessions - |> Enum.map(fn s -> - before_d = - get_in(s.before_snapshot, ["migration_metrics", "deprecated_api_count"]) || 0 - - after_d = - get_in(s.after_snapshot, ["migration_metrics", "deprecated_api_count"]) || 0 - - before_d - after_d - end) - |> Enum.sum() - - all_issues = - sessions - |> Enum.flat_map(fn s -> - s.events - |> Enum.filter(fn e -> e.type == :issue end) - |> Enum.map(fn e -> e.description end) - end) - - # Group issues by frequency - issue_frequency = - all_issues - |> Enum.frequencies() - |> Enum.sort_by(fn {_desc, count} -> count end, :desc) - |> Enum.take(20) - - issue_table = - issue_frequency - |> Enum.map(fn {desc, count} -> "| #{desc} | #{count} |" end) - |> Enum.join("\n") - - durations = - sessions - |> Enum.map(fn s -> - if s.ended_at, do: DateTime.diff(s.ended_at, s.started_at, :second), else: 0 - end) - - avg_duration = if count > 0, do: div(Enum.sum(durations), count), else: 0 - - """ - # Cross-Repo Migration Report - - **Sessions:** #{count} - **Average health improvement:** #{Float.round(avg_health_delta, 3)} - **Total deprecated APIs removed:** #{total_deprecated_removed} - **Average session duration:** #{format_duration(avg_duration)} - - ## Migration Velocity - - | Repo | Health Delta | Deprecated Removed | Duration | - |------|-------------|-------------------|----------| - #{sessions |> Enum.map(&format_session_row/1) |> Enum.join("\n")} - - ## Common Pain Points - - | Issue | Frequency | - |-------|-----------| - #{issue_table} - - --- - _Generated by feedback-o-tron report generator_ - """ - end - - @doc """ - Generate a v13 pre-release trial report structured for the ReScript team. - """ - def v13_trial_report(sessions) when is_list(sessions) do - v13_sessions = - sessions - |> Enum.filter(fn s -> - version = get_in(s.after_snapshot, ["migration_metrics", "version_bracket"]) - version && String.contains?(to_string(version), "V13") - end) - - non_v13 = sessions -- v13_sessions - - regressions = - sessions - |> Enum.flat_map(fn s -> - s.events - |> Enum.filter(fn e -> e.type == :issue && e.severity == :critical end) - |> Enum.map(fn e -> - %{repo: extract_repo_name(s.repo_path), description: e.description} - end) - end) - - """ - # ReScript v13 Pre-Release Trial Report - - **Date:** #{DateTime.utc_now() |> DateTime.to_iso8601()} - **Total repos tested:** #{length(sessions)} - **Repos on v13:** #{length(v13_sessions)} - **Repos on earlier versions:** #{length(non_v13)} - - ## Performance Data - - #{format_performance_table(sessions)} - - ## Regressions - - #{if length(regressions) > 0 do - regressions |> Enum.map(fn r -> "- **#{r.repo}**: #{r.description}" end) |> Enum.join("\n") - else - "No regressions detected." - end} - - ## Missing Features / v14 Opportunities - - #{format_missing_features(sessions)} - - ## Individual Program Stats - - #{sessions |> Enum.map(&format_program_stats/1) |> Enum.join("\n\n")} - - --- - _Generated by feedback-o-tron for the ReScript team_ - """ - end - - # --- Private Helpers --- - - defp delta(nil, _), do: "-" - defp delta(_, nil), do: "-" - - defp delta(before, after_val) when is_number(before) and is_number(after_val) do - d = after_val - before - - cond do - d > 0 -> "+#{d}" - d < 0 -> "#{d}" - true -> "0" - end - end - - defp delta(_, _), do: "-" - - defp format_duration(seconds) when seconds < 60, do: "#{seconds}s" - - defp format_duration(seconds) when seconds < 3600, - do: "#{div(seconds, 60)}m #{rem(seconds, 60)}s" - - defp format_duration(seconds), - do: "#{div(seconds, 3600)}h #{div(rem(seconds, 3600), 60)}m" - - defp format_session_row(session) do - repo = extract_repo_name(session.repo_path) - before_h = get_in(session.before_snapshot, ["migration_metrics", "health_score"]) || 0.0 - after_h = get_in(session.after_snapshot, ["migration_metrics", "health_score"]) || 0.0 - before_d = get_in(session.before_snapshot, ["migration_metrics", "deprecated_api_count"]) || 0 - after_d = get_in(session.after_snapshot, ["migration_metrics", "deprecated_api_count"]) || 0 - - duration = - if session.ended_at, - do: DateTime.diff(session.ended_at, session.started_at, :second), - else: 0 - - "| #{repo} | #{Float.round(after_h - before_h, 3)} | #{before_d - after_d} | #{format_duration(duration)} |" - end - - defp format_performance_table(sessions) do - rows = - sessions - |> Enum.map(fn s -> - repo = extract_repo_name(s.repo_path) - before_bt = get_in(s.before_snapshot, ["migration_metrics", "build_time_ms"]) - after_bt = get_in(s.after_snapshot, ["migration_metrics", "build_time_ms"]) - before_bs = get_in(s.before_snapshot, ["migration_metrics", "bundle_size_bytes"]) - after_bs = get_in(s.after_snapshot, ["migration_metrics", "bundle_size_bytes"]) - - "| #{repo} | #{before_bt || "-"} | #{after_bt || "-"} | #{before_bs || "-"} | #{after_bs || "-"} |" - end) - |> Enum.join("\n") - - """ - | Repo | Build Time (before) | Build Time (after) | Bundle Size (before) | Bundle Size (after) | - |------|--------------------|--------------------|---------------------|---------------------| - #{rows} - """ - end - - defp format_missing_features(sessions) do - features = - sessions - |> Enum.flat_map(fn s -> - s.events - |> Enum.filter(fn e -> e.type == :note end) - |> Enum.filter(fn e -> - desc = String.downcase(e.description) - String.contains?(desc, "missing") || String.contains?(desc, "feature") - end) - |> Enum.map(fn e -> e.description end) - end) - |> Enum.uniq() - - if length(features) > 0 do - features - |> Enum.map(fn f -> "- #{f}" end) - |> Enum.join("\n") - else - "No missing features reported." - end - end - - defp format_program_stats(session) do - repo = extract_repo_name(session.repo_path) - after_m = session.after_snapshot["migration_metrics"] || %{} - - """ - ### #{repo} - - - **Version:** #{after_m["version_bracket"] || "unknown"} - - **Files:** #{after_m["file_count"] || "?"}, **Lines:** #{after_m["rescript_lines"] || "?"} - - **Health:** #{after_m["health_score"] || "?"}, **Ratio:** #{after_m["api_migration_ratio"] || "?"} - - **Deprecated:** #{after_m["deprecated_api_count"] || 0}, **Modern:** #{after_m["modern_api_count"] || 0} - """ - end - - defp generate_recommendations(_before, after_m) do - recs = [] - - recs = - if (after_m["deprecated_api_count"] || 0) > 0 do - recs ++ - [ - "- Migrate remaining #{after_m["deprecated_api_count"]} deprecated API calls to @rescript/core" - ] - else - recs - end - - recs = - case after_m["config_format"] do - f when f in ["BsConfig", "Both"] -> - recs ++ ["- Migrate bsconfig.json to rescript.json"] - - _ -> - recs - end - - recs = - case after_m["jsx_version"] do - 3 -> recs ++ ["- Upgrade JSX from v3 to v4"] - _ -> recs - end - - recs = - if after_m["uncurried"] != true do - recs ++ ["- Enable uncurried mode"] - else - recs - end - - if length(recs) > 0, do: Enum.join(recs, "\n"), else: "All migration targets met." - end - - defp extract_repo_name(path) do - path |> String.split("/") |> List.last() - end -end diff --git a/elixir-mcp/lib/feedback_a_tron/submitter.ex b/elixir-mcp/lib/feedback_a_tron/submitter.ex index d80559a..4c45fcf 100644 --- a/elixir-mcp/lib/feedback_a_tron/submitter.ex +++ b/elixir-mcp/lib/feedback_a_tron/submitter.ex @@ -100,7 +100,7 @@ defmodule FeedbackATron.Submitter do # SP1b pre-ledger rule. Nothing leaves this machine unless a person saw # the whole payload and typed y. Only CLI.submit/3 attaches :consent, # and only after tty_confirm/0 returned true. Every other caller — the - # MCP door, the HTTP door, BatchReviewer — lands in the drafted clause + # MCP door and the HTTP door — lands in the drafted clause # by construction, because there is no way to assert consent from # outside the CLI's interactive confirm. consented = Keyword.get(opts, :consent) == :human_confirmed diff --git a/elixir-mcp/lib/feedback_a_tron/verisim_writer.ex b/elixir-mcp/lib/feedback_a_tron/verisim_writer.ex deleted file mode 100644 index b79f3fa..0000000 --- a/elixir-mcp/lib/feedback_a_tron/verisim_writer.ex +++ /dev/null @@ -1,219 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) Jonathan D.A. Jewell -defmodule FeedbackATron.VeriSimWriter do - @moduledoc """ - VeriSimDB hexad writer for migration sessions. - - Builds octad hexads from migration session data following the pattern - from verisimdb/elixir-orchestration/lib/verisim/hypatia/scan_ingester.ex. - - Each migration session becomes a hexad with: - - Document: full markdown report - - Temporal: session timestamps - - Provenance: source chain (feedback-o-tron -> panic-attack -> repo) - - Semantic: tags [rescript-migration, repo_name, version_bracket] - - Graph: triples linking repo -> session -> snapshots - """ - - require Logger - - @hexad_dir "verisimdb-data/migration-hexads" - - @doc """ - Write a completed migration session as a VeriSimDB hexad. - - Falls back to file-based storage when the VeriSimDB API is unavailable. - """ - def write_migration_session(session) do - hexad = build_hexad(session) - - case write_hexad_file(hexad) do - {:ok, path} -> - Logger.info("[VeriSimWriter] Migration hexad written: #{path}") - {:ok, hexad["hexad_id"]} - - {:error, reason} -> - Logger.error("[VeriSimWriter] Failed to write hexad: #{inspect(reason)}") - {:error, reason} - end - end - - @doc """ - Build a VeriSimDB hexad from a migration session. - """ - def build_hexad(session) do - hexad_id = "migration-#{session.session_id}" - now = DateTime.utc_now() |> DateTime.to_iso8601() - - repo_name = extract_repo_name(session.repo_path) - before_health = get_in(session.before_snapshot, ["migration_metrics", "health_score"]) || 0.0 - after_health = get_in(session.after_snapshot, ["migration_metrics", "health_score"]) || 0.0 - - version_bracket = - get_in(session.after_snapshot, ["migration_metrics", "version_bracket"]) || "unknown" - - %{ - "schema" => "verisimdb.hexad.v1", - "hexad_id" => hexad_id, - "created_at" => now, - "document" => %{ - "title" => "Migration session: #{session.label} (#{repo_name})", - "body" => build_document_body(session), - "content_type" => "text/markdown" - }, - "temporal" => %{ - "started_at" => DateTime.to_iso8601(session.started_at), - "ended_at" => if(session.ended_at, do: DateTime.to_iso8601(session.ended_at), else: nil), - "duration_seconds" => compute_duration(session), - "event_type" => "rescript_migration_session" - }, - "provenance" => %{ - "source" => "feedback-o-tron", - "actor" => "migration-observer", - "operation" => "migration-session", - "input_path" => session.repo_path, - "chain" => [ - %{"tool" => "feedback-o-tron", "version" => "1.0.0"}, - %{"tool" => "panic-attack", "version" => "2.0.0"} - ] - }, - "semantic" => %{ - "types" => ["rescript_migration", "migration_session", "observatory"], - "tags" => [ - "rescript-migration", - "repo:#{repo_name}", - "version:#{version_bracket}", - "label:#{session.label}" - ], - "health_before" => before_health, - "health_after" => after_health, - "health_delta" => after_health - before_health, - "event_count" => length(session.events), - "issue_count" => - session.events - |> Enum.count(fn e -> e.type == :issue end) - }, - "graph" => %{ - "triples" => build_graph_triples(hexad_id, session) - }, - "vector" => %{ - "text_for_embedding" => build_embedding_text(session), - "dimensions" => nil - } - } - end - - # --- Private Helpers --- - - defp build_document_body(session) do - repo_name = extract_repo_name(session.repo_path) - before_metrics = session.before_snapshot["migration_metrics"] || %{} - after_metrics = session.after_snapshot["migration_metrics"] || %{} - - events_md = - session.events - |> Enum.map(fn event -> - "- **#{event.type}** (#{event.severity}): #{event.description}" - end) - |> Enum.join("\n") - - """ - # Migration Session: #{session.label} - - **Repo:** #{repo_name} - **Session ID:** #{session.session_id} - **Started:** #{DateTime.to_iso8601(session.started_at)} - **Ended:** #{if session.ended_at, do: DateTime.to_iso8601(session.ended_at), else: "in progress"} - - ## Before - - | Metric | Value | - |--------|-------| - | Health Score | #{before_metrics["health_score"] || "N/A"} | - | Deprecated APIs | #{before_metrics["deprecated_api_count"] || "N/A"} | - | Modern APIs | #{before_metrics["modern_api_count"] || "N/A"} | - | Version | #{before_metrics["version_bracket"] || "N/A"} | - | Config | #{before_metrics["config_format"] || "N/A"} | - | Files | #{before_metrics["file_count"] || "N/A"} | - - ## After - - | Metric | Value | - |--------|-------| - | Health Score | #{after_metrics["health_score"] || "N/A"} | - | Deprecated APIs | #{after_metrics["deprecated_api_count"] || "N/A"} | - | Modern APIs | #{after_metrics["modern_api_count"] || "N/A"} | - | Version | #{after_metrics["version_bracket"] || "N/A"} | - | Config | #{after_metrics["config_format"] || "N/A"} | - | Files | #{after_metrics["file_count"] || "N/A"} | - - ## Events - - #{events_md} - - #{if session.notes, do: "## Notes\n\n#{session.notes}", else: ""} - """ - end - - defp build_graph_triples(hexad_id, session) do - repo_name = extract_repo_name(session.repo_path) - session_uri = "session:#{session.session_id}" - repo_uri = "repo:#{repo_name}" - - base = [ - [repo_uri, "has_migration_session", session_uri], - [session_uri, "has_label", "label:#{session.label}"], - [session_uri, "has_hexad", "hexad:#{hexad_id}"] - ] - - event_triples = - session.events - |> Enum.with_index() - |> Enum.map(fn {_event, idx} -> - event_uri = "#{session_uri}:event:#{idx}" - [session_uri, "has_event", event_uri] - end) - - base ++ event_triples - end - - defp build_embedding_text(session) do - repo_name = extract_repo_name(session.repo_path) - version = get_in(session.after_snapshot, ["migration_metrics", "version_bracket"]) || "" - - event_text = - session.events - |> Enum.map(fn e -> "#{e.type}: #{e.description}" end) - |> Enum.join(". ") - - "ReScript migration session for #{repo_name}. " <> - "Label: #{session.label}. Version: #{version}. " <> - "Events: #{event_text}" - end - - defp compute_duration(session) do - case session.ended_at do - nil -> 0 - ended -> DateTime.diff(ended, session.started_at, :second) - end - end - - defp write_hexad_file(hexad) do - dir = @hexad_dir - File.mkdir_p!(dir) - - path = Path.join(dir, "#{hexad["hexad_id"]}.json") - content = Jason.encode!(hexad, pretty: true) - - case File.write(path, content) do - :ok -> {:ok, path} - {:error, reason} -> {:error, reason} - end - end - - defp extract_repo_name(path) do - path - |> String.split("/") - |> List.last() - end -end diff --git a/elixir-mcp/mix.exs b/elixir-mcp/mix.exs index 8b3b101..8697836 100644 --- a/elixir-mcp/mix.exs +++ b/elixir-mcp/mix.exs @@ -62,9 +62,6 @@ defmodule FeedbackATron.MixProject do # Fuzzy string matching for deduplication {:the_fuzz, "~> 0.6"}, - # GenStage producer/consumer pipeline (migration observer) - {:gen_stage, "~> 1.2"}, - # For testing {:mox, "~> 1.0", only: :test}, {:bypass, "~> 2.1", only: :test}, diff --git a/elixir-mcp/mix.lock b/elixir-mcp/mix.lock index ba0cefb..44b3b8c 100644 --- a/elixir-mcp/mix.lock +++ b/elixir-mcp/mix.lock @@ -8,7 +8,6 @@ "elixir_mcp_server": {:hex, :elixir_mcp_server, "0.1.0", "cf47ed06f1136a9e09aa80be3b7f62f8335e940b038910ca6c94aa0826f34c89", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f317a1f9e90d491369157da55a928de065bc83facd4b75e5d1ad7f18c36bc85e"}, "ex_doc": {:hex, :ex_doc, "0.40.4", "66f2e42bf588594d5a8aab31cad87f2ddad09d0da1b1a2f379340ec2c2e497cb", [:mix], [{:earmark_parser, "~> 1.4.46", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "6222b9e423d76584ee34df2c82a5ed72c2d53dc153f7f483ad28b378694186cc"}, "finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"}, - "gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"}, "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"}, From c5e8b0546ac451220a44a3286965dff276598b3d Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:04:32 +0100 Subject: [PATCH 2/2] docs: record the ReScript-migration retire where it is still advertised MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The code retire in 7ca576c left two tracked documents naming the nine deleted modules. A repo-wide sweep (not just lib/test/mix.exs) found no hits in config/, README.adoc, ARCHITECTURE.adoc, the CLI help text, CI, humans.txt or STATE.a2ml — no user-facing flag was ever advertised — but two records still pointed at the subsystem: - TEST-NEEDS.adoc names the nine deleted modules in its file list. The document is also superseded for an unrelated reason: it predates #93 and still claims "Unit tests: NONE (0 test files found)" against an engine with 318 passing tests. Banner rather than curation — surgically editing nine filenames out of a document whose headline claim is false would polish a lie. Re-running the audit is separate work. - The 2026-09-07 design spec listed the pipeline as "Real and tested" and "the natural seed for burst clustering at L5". Both are now wrong: it carried zero tests, and it is gone. Left unmarked, a later session plans L5 on a deleted subsystem. VeriSimDB is explicitly NOT retired. It remains a live estate integration (.machine_readable/integrations/verisimdb.a2ml, gated by dogfood-gate.yml); only the two FoT-internal writers went with the pipeline. Recorded in both documents so a future sweep does not mistake the name for dead code. The plan document's "Noticed, not in this plan" mention is historical and is deliberately left as written. asciidoctor -o /dev/null is silent on both files. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HGbKA9B3XfzRSdsjLg3j33 --- TEST-NEEDS.adoc | 13 +++++++++++++ .../specs/2026-09-07-fot-everywhere-design.adoc | 6 ++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/TEST-NEEDS.adoc b/TEST-NEEDS.adoc index e096cc2..2b15654 100644 --- a/TEST-NEEDS.adoc +++ b/TEST-NEEDS.adoc @@ -1,5 +1,18 @@ == Test & Benchmark Requirements +[NOTE] +==== +SUPERSEDED 2026-09-15. This audit predates PR #93 — the engine now has 318 +passing tests, so "Unit tests: NONE (0 test files found)" below is false — and +predates the FOT-R-22 retire, which deleted the nine migration-observer, +batch-reviewer, report-generator and GenStage-pipeline modules still named in +the file list below. Re-run the audit before acting on any figure here. + +VeriSimDB itself is NOT retired: it remains a live estate integration +(`.machine_readable/integrations/verisimdb.a2ml`, gated by `dogfood-gate.yml`). +Only the two FoT-internal writers went with the pipeline. +==== + === CRG Grade: C — ACHIEVED 2026-04-04 === Current State diff --git a/docs/superpowers/specs/2026-09-07-fot-everywhere-design.adoc b/docs/superpowers/specs/2026-09-07-fot-everywhere-design.adoc index 9d74065..916fa99 100644 --- a/docs/superpowers/specs/2026-09-07-fot-everywhere-design.adoc +++ b/docs/superpowers/specs/2026-09-07-fot-everywhere-design.adoc @@ -60,8 +60,10 @@ Elixir 1.19 / OTP 27._ question, not a build. This is the core and it stays the core. * A GenStage migration-observation pipeline (producer → review consumer → VeriSim writer) with a batch reviewer and report generator, built for the - ReScript migration. Real and tested, narrow in purpose, and the natural - seed for burst clustering at L5. + ReScript migration. RETIRED 2026-09-15 by owner ruling FOT-R-22: it was never + wired into the supervision tree, carried zero tests despite the claim above, + and the ReScript migration it was built for is moot under the bun-only + doctrine. Do not seed L5 burst clustering on it — it no longer exists. * The MCP server (`mcp/server.ex`, stdio by default, optional loopback TCP) exposing three tools: research, synthesize, submit. * The HTTP intake (`http_intake/router.ex`, 127.0.0.1:7722, off by default)