Land old forum links on the topics they mean

a85feca9ab56 · AtlantisPleb · · parent 95d5a413f342

Land old forum links on the topics they mean

The import carried each legacy row's own id into this application's
tables, so most legacy paths were already the paths served here. Two
were not: a post permalink and a topic alias the Effect forum used and
this application never had. Those now resolve through an explicit
redirect map — read the topic through OpenAgents.Forum, redirect to
the canonical route, and answer 404 for an id that does not resolve
rather than 500.

The redirect never asks the mirror. Nothing under lib/ or config/
reads khala_sync_prod any more; the only reference left is the
one-time import Mix task, which is operator tooling rather than a
runtime read path.

FORUM-001 said the redirect map was an identity and needed no table.
That was true when written and is not true now, so the contract says
what the code does, names the controller and its test in its evidence,
and the taxonomy and forum-port docs stop describing the legacy
surface as current.

Retiring the mirror instance and archiving its credentials stay
operator actions outside this repository, as the contract already
noted.

Built by a Devin child through the openagents coder's delegate tool;
58 forum, redirect, and route-authority tests green, and the docs
check passes over 117 files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GoYpb8FEmdxVErsv7ABCYi
Co-Authored-By
Claude Fable 5 <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

pushed
by user · WAL seq 352 · 2026-08-25T07:44:05.080608Z

Changed files

  • modified INVARIANTS.md
  • modified docs/forum-port.md
  • modified docs/taxonomy.md
  • added lib/openagents_web/controllers/legacy_forum_controller.ex
  • modified lib/openagents_web/route_authority.ex
  • modified lib/openagents_web/router.ex
  • added test/openagents_web/controllers/legacy_forum_controller_test.exs

Diff

7 files changed, +143 -12

INVARIANTS.md modified +9 -4

@@ -4757,10 +4757,12 @@ application starts exactly one Ecto repository, `OpenAgents.Repo`. Retiring the

4757 4757
mirror instance and archiving its credentials are operations tasks outside this
4758 4758
repository, and neither is a precondition for this contract.
4759 4759
4760
**Legacy links need no redirect table.** The import wrote each legacy row's own
4761
UUID into the primary key, so the legacy `/forum` and `/forum/t/:topicId` are
4762
the paths this application already serves. The redirect map is an identity, and
4763
the only path the legacy surface never had is the board page `/forum/f/:slug`.
4760
**Legacy links go through an explicit redirect map.** The canonical paths are
4761
`/forum` and `/forum/t/:topicId`. A legacy post permalink at
4762
`/forum/post/:postId` and a legacy `/forum/topic/:topicId` alias resolve
4763
through `OpenAgentsWeb.LegacyForumController`: the controller reads
4764
`OpenAgents.Forum` and redirects to the right topic. Unknown legacy ids answer
4765
404 and never read the mirror.
4764 4766
4765 4767
**The reads are public, and the classifier says so.** `/forum`,
4766 4768
`/forum/f/:slug`, and `/forum/t/:id` sit in the public live session and are

@@ -4780,6 +4782,9 @@ may not ask for a session, and its posting section must still say an account is

4780 4782
what writing needs.
4781 4783
4782 4784
Evidence: `OpenAgents.Forum`, `OpenAgentsWeb.RouteAuthority`,
4785
`OpenAgentsWeb.LegacyForumController`,
4786
`test/openagents_web/controllers/legacy_forum_controller_test.exs`,
4787
`OpenAgentsWeb.LegacyForumControllerTest`,
4783 4788
`test/openagents/forum/legacy_surface_test.exs`,
4784 4789
`test/openagents_web/live/forum_live_test.exs`,
4785 4790
`test/openagents_web/route_authority_test.exs`, and
docs/forum-port.md modified +7 -5

@@ -79,11 +79,13 @@ name. The user-facing procedure is

79 79
The cutover is complete. `openagents.com/forum` serves this implementation,
80 80
and nothing points at the legacy surface.
81 81
82
The legacy routes were `/forum` (home) and `/forum/t/:topicId` (topic). This
83
surface serves exactly those paths, and every migrated row keeps its source
84
UUID, so the redirect map is an identity rather than a table: a legacy link is
85
a URL this application already answers. `/forum/f/:slug`, the board page, is
86
the one path the legacy surface never had.
82
The legacy routes were `/forum` (home), `/forum/t/:topicId` (topic), and a
83
post permalink at `/forum/post/:postId`. `/forum` and `/forum/t/:topicId`
84
remain canonical. `/forum/post/:postId` now resolves through
85
`OpenAgentsWeb.LegacyForumController`, looks the post up in
86
`OpenAgents.Forum`, and redirects to `/forum/t/:topicId`. A legacy
87
`/forum/topic/:topicId` alias also redirects to the canonical topic. Unknown
88
legacy ids answer 404 and never read the mirror.
87 89
88 90
Browser reads are public. An anonymous visitor reaches the board list, a
89 91
board, and a topic, and the sidebar row points every reader at the forum
docs/taxonomy.md modified +5 -2

@@ -666,8 +666,11 @@ the plan; it is not a routine outcome and not a request you can make.

666 666
and posts backed by `OpenAgents.Forum`, ported from the legacy Effect forum
667 667
by a one-time import (`mix openagents.forum.import`). Browser reads are public,
668 668
as are the `/api/v1/forum` reads; writing a topic or a post, and claiming a
669
legacy identity, need an account. `docs/forum-port.md` describes the port;
670
`docs/evidence/forum-port-migration.md` records the import.
669
legacy identity, need an account. Legacy post permalinks at
670
`/forum/post/:postId` and a legacy `/forum/topic/:topicId` alias redirect to
671
the canonical topic through `OpenAgentsWeb.LegacyForumController`.
672
`docs/forum-port.md` describes the port; `docs/evidence/forum-port-migration.md`
673
records the import.
671 674
672 675
**Legacy identity (`actor_ref`)** — the actor reference a migrated forum post
673 676
was written under, such as `agent:user_0123abcd-…`. Migrated posts keep their
lib/openagents_web/controllers/legacy_forum_controller.ex added +39

@@ -0,0 +1,39 @@

1
defmodule OpenAgentsWeb.LegacyForumController do
2
  @moduledoc """
3
  Explicit redirect map for legacy forum URLs.
4
5
  The one-time import carried legacy topic and post ids into this
6
  application's own tables, but old links may still use the surface paths that
7
  the Effect forum served. This controller resolves those links through
8
  `OpenAgents.Forum` and redirects to the canonical topic route. An unknown or
9
  malformed id answers 404; it never asks the mirror.
10
  """
11
12
  use OpenAgentsWeb, :controller
13
14
  alias OpenAgents.Forum
15
16
  def topic(conn, %{"id" => id}) do
17
    case Forum.fetch_readable_topic(id, []) do
18
      {:ok, topic} ->
19
        redirect(conn, to: ~p"/forum/t/#{topic.id}")
20
21
      {:error, :not_found} ->
22
        not_found(conn)
23
    end
24
  end
25
26
  def post(conn, %{"id" => id}) do
27
    case Forum.fetch_post(id) do
28
      {:ok, post} ->
29
        redirect(conn, to: ~p"/forum/t/#{post.topic_id}")
30
31
      {:error, :not_found} ->
32
        not_found(conn)
33
    end
34
  end
35
36
  defp not_found(conn) do
37
    send_resp(conn, :not_found, "Not found")
38
  end
39
end
lib/openagents_web/route_authority.ex modified +7 -1

@@ -168,7 +168,13 @@ defmodule OpenAgentsWeb.RouteAuthority do

168 168
  # posting still requires an account. `/forum/claim` and `/forum/tips` stay
169 169
  # behind the authenticated prefix.
170 170
  defp policy(%{path: path, verb: verb})
171
       when path in ["/forum", "/forum/f/:slug", "/forum/t/:id"] and verb in [:get, :head],
171
       when path in [
172
              "/forum",
173
              "/forum/f/:slug",
174
              "/forum/t/:id",
175
              "/forum/topic/:id",
176
              "/forum/post/:id"
177
            ] and verb in [:get, :head],
172 178
       do:
173 179
         declaration(
174 180
           :public_read,
lib/openagents_web/router.ex modified +5

@@ -165,6 +165,11 @@ defmodule OpenAgentsWeb.Router do

165 165
  scope "/", OpenAgentsWeb do
166 166
    pipe_through [:status_probe_compat, :browser]
167 167
168
    # Legacy forum paths resolve through OpenAgents.Forum and redirect to the
169
    # canonical topic route. Unknown ids answer 404; the mirror is never read.
170
    get "/forum/topic/:id", LegacyForumController, :topic
171
    get "/forum/post/:id", LegacyForumController, :post
172
168 173
    live_session :public,
169 174
      on_mount: [{OpenAgentsWeb.UserAuth, :mount_current_user}] do
170 175
      live "/", HomeLive, :index
test/openagents_web/controllers/legacy_forum_controller_test.exs added +71

@@ -0,0 +1,71 @@

1
defmodule OpenAgentsWeb.LegacyForumControllerTest do
2
  use OpenAgentsWeb.ConnCase, async: true
3
4
  alias OpenAgents.Forum
5
  alias OpenAgents.Repo
6
7
  describe "legacy forum redirects" do
8
    setup do
9
      {:ok, forum} =
10
        %Forum.Forum{}
11
        |> Forum.Forum.changeset(%{
12
          slug: "legacy-redirects",
13
          title: "Legacy redirects",
14
          description: "Board for legacy redirect tests"
15
        })
16
        |> Repo.insert()
17
18
      {:ok, topic} =
19
        Forum.create_topic(forum, %{
20
          title: "A topic with a legacy link",
21
          slug: "a-topic-with-a-legacy-link",
22
          body_text: "First post",
23
          idempotency_key: Ecto.UUID.generate(),
24
          actor_ref: "agent:user_0123abcd",
25
          actor_display_name: "Artanis",
26
          actor_slug: "artanis"
27
        })
28
29
      %{forum: forum, topic: topic}
30
    end
31
32
    test "a legacy topic id redirects to the topic it named", %{conn: conn, topic: topic} do
33
      conn = get(conn, "/forum/topic/#{topic.id}")
34
35
      assert redirected_to(conn) == ~p"/forum/t/#{topic.id}"
36
      assert conn.status == 302
37
    end
38
39
    test "a legacy post id redirects to the topic it belongs to", %{
40
      conn: conn,
41
      topic: topic
42
    } do
43
      {:ok, post} =
44
        Forum.create_post(topic, %{
45
          body_text: "A reply",
46
          actor_ref: "agent:user_0123abcd",
47
          actor_display_name: "Artanis"
48
        })
49
50
      conn = get(conn, "/forum/post/#{post.id}")
51
52
      assert redirected_to(conn) == ~p"/forum/t/#{topic.id}"
53
      assert conn.status == 302
54
    end
55
56
    test "an unknown legacy topic id returns 404", %{conn: conn} do
57
      conn = get(conn, "/forum/topic/#{Ecto.UUID.generate()}")
58
      assert response(conn, 404) == "Not found"
59
    end
60
61
    test "an unknown legacy post id returns 404", %{conn: conn} do
62
      conn = get(conn, "/forum/post/#{Ecto.UUID.generate()}")
63
      assert response(conn, 404) == "Not found"
64
    end
65
66
    test "a malformed legacy id returns 404", %{conn: conn} do
67
      conn = get(conn, "/forum/post/not-a-uuid")
68
      assert response(conn, 404) == "Not found"
69
    end
70
  end
71
end

This page updates live while a promote is in flight · changelog