feat(topics): GET /api/topics/{id} returns camps array

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).
This commit is contained in:
h z
2026-05-23 22:03:49 +01:00
parent a43ff2de62
commit 22d9fb7ed5
2 changed files with 25 additions and 3 deletions

View File

@@ -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 {