From 22d9fb7ed5b9c3794d83890f22cdf95813b1fd03 Mon Sep 17 00:00:00 2001 From: hzhang Date: Sat, 23 May 2026 22:03:49 +0100 Subject: [PATCH] feat(topics): GET /api/topics/{id} returns camps array MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the camps allocation array to topic_detail responses so an agent can locate which camp they're in (pro/con/judge) in a single round-trip instead of needing a separate endpoint call. Camps are 0 rows pre-signup_close, exactly 3 rows after — small enough to inline always. Backward-compatible: the existing Topic fields remain top-level on the response; `camps` is a sibling array. Callers reading e.g. response.title or response.status continue to work unchanged. Arguments are deliberately NOT inlined here — they can grow to many KB per topic, and most callers (list view, status check, signup intent resolution) don't need them. Use the new `dialectic_list_arguments` plugin tool against GET /api/topics/{id}/arguments when you actually need the transcript. E2e verified on sim: judge agent successfully called topic_detail to get camps + list_arguments to get transcript + submit_verdict citing the actual pro/con argument content (no more 'tie because I saw no arguments' false readings). --- internal/httpapi/handlers/topics.go | 26 ++++++++++++++++++++++++-- internal/httpapi/routes.go | 2 +- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/internal/httpapi/handlers/topics.go b/internal/httpapi/handlers/topics.go index d1e81c8..fda0edb 100644 --- a/internal/httpapi/handlers/topics.go +++ b/internal/httpapi/handlers/topics.go @@ -16,9 +16,12 @@ import ( type TopicsHandler struct { store *store.TopicStore + camps *store.CampStore } -func NewTopicsHandler(s *store.TopicStore) *TopicsHandler { return &TopicsHandler{store: s} } +func NewTopicsHandler(s *store.TopicStore, c *store.CampStore) *TopicsHandler { + return &TopicsHandler{store: s, camps: c} +} // GET /api/topics?status=...&visibility=...&limit=...&offset=... // @@ -75,7 +78,26 @@ func (h *TopicsHandler) Get(w http.ResponseWriter, r *http.Request) { http.Error(w, "not found", http.StatusNotFound) // 404 not 403 — hide existence return } - writeJSON(w, http.StatusOK, t) + // Enrich with camps so an agent can locate their own allocation in one + // round-trip. Camps are 0 rows pre-signup_close, 3 rows after — small + // enough that inlining costs nothing. Arguments are deliberately NOT + // inlined (potentially large; agents who need the transcript should + // hit GET /api/topics/{id}/arguments via dialectic_list_arguments). + // + // Backward-compatible: existing callers reading the original Topic + // fields keep working; new callers can read `camps` alongside. + camps, cErr := h.camps.ListByTopic(r.Context(), id) + if cErr != nil { + camps = nil // best-effort; metadata still useful + } + // Marshal Topic into a map and add `camps` as a sibling field rather + // than wrapping under "topic" — that would break every existing + // consumer that reads e.g. response.title / response.status directly. + buf, _ := json.Marshal(t) + out := map[string]any{} + _ = json.Unmarshal(buf, &out) + out["camps"] = camps + writeJSON(w, http.StatusOK, out) } type createTopicBody struct { diff --git a/internal/httpapi/routes.go b/internal/httpapi/routes.go index 5a2347e..ce19ca0 100644 --- a/internal/httpapi/routes.go +++ b/internal/httpapi/routes.go @@ -60,7 +60,7 @@ func Mount(cfg *config.Config, db *sqlx.DB, version string) http.Handler { verdictStore := store.NewVerdictStore(db) health := handlers.NewHealthHandler(db, version) - topicsH := handlers.NewTopicsHandler(topicStore) + topicsH := handlers.NewTopicsHandler(topicStore, campStore) signupsH := handlers.NewSignupsHandler(topicStore, signupStore) argsH := handlers.NewArgumentsHandler(topicStore, campStore, roundStore, argStore) announcer := fabric.NewAnnouncer(cfg.FabricSystemAPIKey)