From d53a8c54351ccf5e0ecfd890abe53e13c65c60fa Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 06:52:17 +0000 Subject: Publish how many episodes shared each break, and plan the rest The server has computed and stored Partners since ad detection shipped -- state.json carries it on every break -- but breakRange never put it on the wire, so the phone's new tap-a-break popup could only ever say "the server did not say how many other episodes carried this audio". That count is the one piece of evidence a listener can weigh before judging a break, and the popup exists to show it. My brief for the popup asserted the server already sent it. It did not. The implementing agent checked rather than taking my word, which is why this is a two-line fix now instead of a wrong "why" line forever. Also lands the handover plan for the correction channel and the signature library, written before a context compaction, including the gesture design Peter specified for the notification button and the jump-size argument he correctly rejected. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B --- server/internal/api/api.go | 11 ++++++++++- server/internal/api/api_test.go | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) (limited to 'server/internal') diff --git a/server/internal/api/api.go b/server/internal/api/api.go index e50c94e..0aeef8a 100644 --- a/server/internal/api/api.go +++ b/server/internal/api/api.go @@ -74,6 +74,10 @@ type breakRange struct { StartMs int64 `json:"startMs"` EndMs int64 `json:"endMs"` Kind string `json:"kind,omitempty"` + // Partners is how many other episodes were found to carry this audio. The phone shows it + // when a break is tapped: it is the only evidence a listener can weigh before deciding + // whether the detector got this one right. + Partners int `json:"partners,omitempty"` } // Handler returns the routed, authenticated handler. @@ -164,7 +168,12 @@ func (s *Server) episode(w http.ResponseWriter, r *http.Request) { breaks := make([]breakRange, 0, len(found.Breaks)) for _, br := range found.Breaks { - breaks = append(breaks, breakRange{StartMs: br.StartMs, EndMs: br.EndMs, Kind: br.Kind}) + breaks = append(breaks, breakRange{ + StartMs: br.StartMs, + EndMs: br.EndMs, + Kind: br.Kind, + Partners: br.Partners, + }) } writeJSON(w, http.StatusOK, episodeResponse{ Status: "ready", diff --git a/server/internal/api/api_test.go b/server/internal/api/api_test.go index 028bbd8..d4d3ee6 100644 --- a/server/internal/api/api_test.go +++ b/server/internal/api/api_test.go @@ -347,3 +347,26 @@ func seedMirrored(t *testing.T, st *store.State, audioDir, contents string) { func episodeID(st *store.State) string { return st.Match("https://f", "g1", "", "").ID } + +// Partners -- how many other episodes carried the same audio -- is the one piece of evidence a +// listener can judge a break by, and the phone's popup exists to show it. The server has always +// computed and stored it; it simply never put it on the wire, so every break arrived claiming +// nothing was known about it. +func TestEpisodeResponseCarriesHowManyEpisodesSharedEachBreak(t *testing.T) { + s, st, _, audioDir := newServer(t) + seedMirrored(t, st, audioDir, "audio-bytes") + e := st.Match("https://f", "g1", "", "") + e.Breaks = []store.Break{{StartMs: 0, EndMs: 98_000, Kind: "ad", Partners: 6}} + + rec := get(t, s, "/api/episode?feed=https://f&guid=g1") + var got episodeResponse + if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { + t.Fatal(err) + } + if len(got.Breaks) == 0 { + t.Fatal("no breaks found") + } + if got.Breaks[0].Partners != 6 { + t.Errorf("partners %d, want 6", got.Breaks[0].Partners) + } +} -- cgit v1.2.3