From 26425f22a6b384e4f95cbd3363a8ca4c75cf922f Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Mon, 2 Nov 2026 07:42:12 +0000 Subject: Give the ad slot's length one home Four packages each wrote 15 seconds for the length of an advertising slot -- skips, transcript twice, and signature -- and every copy carried a comment saying it agreed with the others: "the same unit skips.Classify uses", "the same figure the length-gap rule uses", "one ad unit from the start". The agreement was real and deliberate, and recorded only in prose, so a corpus that revised the figure would have moved one copy and left three disagreeing silently. internal/advertising holds it once. Each site keeps its own name and its own explanation of what the unit means there, because those genuinely differ; only the number is shared. The consolidation on its own fixes the four copies that existed. The audit test refuses the fifth: it walks the server's production source and fails on the number written as a literal, in either of Go's spellings. Watched failing on both before being trusted. Same arrangement as safefetch/audit_test.go, and for the same reason -- the instance that was noticed was never the whole of it. No behaviour change: 366 server tests pass, same as before. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B --- server/internal/advertising/advertising.go | 25 +++++++++++ server/internal/advertising/audit_test.go | 71 ++++++++++++++++++++++++++++++ server/internal/signature/shadow.go | 13 ++++-- server/internal/skips/kinds.go | 11 ++--- server/internal/transcript/ads.go | 9 ++-- server/internal/transcript/fits.go | 13 +++--- 6 files changed, 124 insertions(+), 18 deletions(-) create mode 100644 server/internal/advertising/advertising.go create mode 100644 server/internal/advertising/audit_test.go (limited to 'server') diff --git a/server/internal/advertising/advertising.go b/server/internal/advertising/advertising.go new file mode 100644 index 0000000..d56f500 --- /dev/null +++ b/server/internal/advertising/advertising.go @@ -0,0 +1,25 @@ +// Package advertising holds what is known about the shape of inserted advertising, for the +// packages that have to recognise it. +// +// It exists because one measured figure -- the length of an ad slot -- had been written out four +// times, in skips, transcript (twice) and signature, each copy carrying a comment pointing at the +// others: "the same unit skips.Classify uses", "the same figure the length-gap rule uses", "one +// ad unit from the start". The agreement was real and deliberate, and recorded only in prose, so +// a corpus that revised the figure would have moved one copy and left three. +package advertising + +// UnitMs is the length of one advertising slot. +// +// Advertising is sold in units of roughly fifteen seconds -- the owner's own observation, borne +// out by the corpus -- and three separate rules depend on that same fact: +// +// - a served file exceeding its declared length by less than one unit cannot carry a real +// insertion; it is the feed being approximate, the way encoders disagree about a file's +// length by a fraction of a second; +// - anything briefer than one unit is a mention rather than a break, and skipping it would +// clip the programme; +// - one unit from the start of a guessed break is far more audio than a landmark fingerprint +// needs, and is the part most certainly advertising rather than programme. +// +// Changing this changes all three, which is the point of it being one constant. +const UnitMs int64 = 15_000 diff --git a/server/internal/advertising/audit_test.go b/server/internal/advertising/audit_test.go new file mode 100644 index 0000000..f8d8e73 --- /dev/null +++ b/server/internal/advertising/audit_test.go @@ -0,0 +1,71 @@ +package advertising + +import ( + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +/* +The ad slot's length is written once. + +This package exists because the figure had been written out four times -- in skips, in transcript +twice, and in signature -- each copy carrying a comment saying it agreed with the others. The +agreement held only as long as somebody read all four comments before changing one. + +Consolidating them fixes the four copies that existed. This refuses the fifth: a production file +that writes the number instead of naming the constant. The same arrangement as +safefetch/audit_test.go, and for the same reason -- the instance that was noticed was never the +whole of it. +*/ + +// literal matches the slot length written as a number, in either of Go's spellings. +var literal = regexp.MustCompile(`\b15_?000\b`) + +// allowed names the files that may write it, and why. +var allowed = map[string]string{ + // This package is where the figure lives. + filepath.Join("internal", "advertising", "advertising.go"): "defines it", +} + +func TestTheAdSlotLengthIsNotWrittenTwice(t *testing.T) { + root, err := filepath.Abs("../..") + if err != nil { + t.Fatalf("root: %v", err) + } + + err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") { + return err + } + // A test states its own fixtures: an episode 15 seconds long is the case being + // described, not a second definition of the unit. + if strings.HasSuffix(path, "_test.go") { + return nil + } + relative, err := filepath.Rel(root, path) + if err != nil { + return err + } + if _, ok := allowed[relative]; ok { + return nil + } + raw, err := os.ReadFile(path) + if err != nil { + return err + } + for _, line := range strings.Split(string(raw), "\n") { + if literal.MatchString(line) { + t.Errorf("%s writes the ad slot length as a literal (%s) -- use "+ + "advertising.UnitMs, so a revised figure moves every rule that depends on it", + relative, strings.TrimSpace(line)) + } + } + return nil + }) + if err != nil { + t.Fatalf("walk: %v", err) + } +} diff --git a/server/internal/signature/shadow.go b/server/internal/signature/shadow.go index 928743a..fe856a7 100644 --- a/server/internal/signature/shadow.go +++ b/server/internal/signature/shadow.go @@ -1,6 +1,10 @@ package signature -import "context" +import ( + "context" + + "github.com/terst/listen/server/internal/advertising" +) // Audio supplies decoded audio at [SampleRate]. type Audio interface { @@ -70,9 +74,10 @@ type Proposal struct { // // An uncorrected end is wherever the 25-second jump happened to land, not where the advertising // stopped, so the tail of that range may be programme -- and a signature carrying programme is -// the one thing this must never build. One ad unit from the start is far more audio than a -// landmark fingerprint needs to be distinctive, and it is the part most certainly advertising. -const GuessedEndSeedMs = int64(15_000) +// the one thing this must never build. One advertising slot from the start is far more audio +// than a landmark fingerprint needs to be distinctive, and it is the part most certainly +// advertising. +const GuessedEndSeedMs = advertising.UnitMs // Report is what one shadow run saw. type Report struct { diff --git a/server/internal/skips/kinds.go b/server/internal/skips/kinds.go index 66d4b21..f38f040 100644 --- a/server/internal/skips/kinds.go +++ b/server/internal/skips/kinds.go @@ -1,5 +1,7 @@ package skips +import "github.com/terst/listen/server/internal/advertising" + // BreakKind says what a published break actually is, so advertising and a show's own furniture // can be told apart -- by the phone, which skips them on different terms, and by anyone reading // the accounting. @@ -17,11 +19,10 @@ const ( const ( // adUnitMs is how far served may exceed declared before the excess is read as an insertion. - // Advertising is sold in units of roughly fifteen seconds -- the owner's own observation, and - // borne out by the corpus -- so a served length exceeding declared by less than one such unit - // cannot be a real inserted advertisement: it is the feed being approximate, the same way - // encoders disagree about a file's length by a fraction of a second. - adUnitMs = 15_000 + // One advertising slot: a gap smaller than that cannot be a real insertion, it is the feed + // being approximate, the same way encoders disagree about a file's length by a fraction of + // a second. + adUnitMs = advertising.UnitMs // edgeWindowMs is how close to either end a break must sit to be the opening or the closing // rather than a break cue. Two minutes: Jon Stewart's opening runs 98 seconds and Search // Engine's begins at 2:00. diff --git a/server/internal/transcript/ads.go b/server/internal/transcript/ads.go index f9f9a12..069909b 100644 --- a/server/internal/transcript/ads.go +++ b/server/internal/transcript/ads.go @@ -3,6 +3,8 @@ package transcript import ( "regexp" "strings" + + "github.com/terst/listen/server/internal/advertising" ) /* @@ -39,10 +41,9 @@ const ( // with the product description between them; 25 gives that room without reaching the // programme on either side. bridgeMs = 25_000 - // minCandidateMs is the shortest thing worth calling an advertisement. Advertising is sold - // in units of roughly fifteen seconds, the same figure the length-gap rule uses, and - // anything briefer is a mention rather than a break -- skipping one would clip the show. - minCandidateMs = 15_000 + // minCandidateMs is the shortest thing worth calling an advertisement. One advertising slot: + // anything briefer is a mention rather than a break, and skipping it would clip the show. + minCandidateMs = advertising.UnitMs // minKinds is how many different kinds of marker must appear. Two, because one kind // repeated says one thing: a host saying "sponsor" four times while describing a // sponsorship deal is the programme, which is the same trap the toc=false chapter rule diff --git a/server/internal/transcript/fits.go b/server/internal/transcript/fits.go index c22958b..de4d74c 100644 --- a/server/internal/transcript/fits.go +++ b/server/internal/transcript/fits.go @@ -1,13 +1,16 @@ package transcript -import "fmt" +import ( + "fmt" + + "github.com/terst/listen/server/internal/advertising" +) const ( // insertionMs is how far a served file may exceed its declared length before the excess is - // read as inserted advertising rather than as the feed being approximate. Fifteen seconds, - // the same unit skips.Classify uses and for the same reason: advertising is sold in units of - // roughly that length, so a smaller gap cannot be a real insertion. - insertionMs = 15_000 + // read as inserted advertising rather than as the feed being approximate. One advertising + // slot, the same judgement skips.Classify makes and for the same reason. + insertionMs = advertising.UnitMs // tailMs is how much of a file may follow the last spoken word before the silence has to be // explained. Closing music runs to fifteen seconds in this corpus (LOW's "Part 5: Bane" // ends 15.2s before its file does); the shortest advertisement does not fit in sixty. -- cgit v1.2.3