Editorial audit: per-category airtime figures and per-block tallies (read-only)
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 2m15s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m21s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-03 23:29:32 +00:00
co-authored by Claude Opus 5.5
parent 981dae64a6
commit f0804b6469
3 changed files with 228 additions and 0 deletions
@@ -52,6 +52,7 @@ class ScheduleAuditTest {
val md = StringBuilder("# Schedule audit replay (read-only)\n\nFour weeks from each channel's committed plan, with today's rules, on copies of the box's real pools. " +
"\"Committed\" is the plan on the box now; \"New\" is what today's rules book after it.\n")
val allPlans = mutableMapOf<Int, List<ScheduleEntry>>()
val json = JSONObject()
for (programming in SmartChannels) {
val number = programming.number
val pf = File(dir, "schedule-$number.json"); val sf = File(dir, "episodes-$number.json")
@@ -98,6 +99,18 @@ class ScheduleAuditTest {
})
md.append(".\n")
}
// For the editorial audit: every title's airtime and starts, old week against new days, and by hour of day.
val titleOf = series.associate { it.id to it.title }
fun tally(es: List<ScheduleEntry>) = JSONObject().also { o -> es.groupBy { it.seriesId }.forEach { (id, xs) ->
o.put(titleOf[id] ?: guideTitleParts(xs.first().media.item.title).first, JSONObject().put("minutes", xs.sumOf { it.media.durationMs } / 60_000).put("starts", xs.size)
.put("hours", org.json.JSONArray(xs.map { java.time.Instant.ofEpochMilli(it.startMs).atZone(zone).let { l -> l.dayOfWeek.value * 100 + l.hour } }.distinct().sorted()))) } }
json.put(number.toString(), JSONObject().put("name", programming.name).put("pool", org.json.JSONArray(series.map { JSONObject().put("title", it.title).put("files", it.episodes.size).put("minutes", it.episodes.sumOf { e -> e.durationMs } / 60_000) }))
.put("committed", tally(committed.entries)).put("steady", tally(steady)).put("committedEnd", committedEnd).put("film", filmChannel)
.put("parts", org.json.JSONArray(programming.dayParts.map { p -> JSONObject().put("from", p.fromHour).put("to", p.toHour).put("days", org.json.JSONArray(p.days.map { it.name })).put("titles", org.json.JSONArray(p.titles)).put("group", p.group).put("key", p.key).put("onTime", p.onTime) }))
.put("searches", org.json.JSONArray(programming.searches)).put("finishTogether", programming.finishTogether).put("shuffleEpisodes", programming.shuffleEpisodes)
.put("mixed", org.json.JSONArray(series.filter(programming::mixes).map { it.title }))
.put("committedReasons", org.json.JSONArray(committed.entries.map { it.why.substringBefore(';') }.distinct()))
.put("byPart", org.json.JSONArray(programming.dayParts.map { part -> tally(steady.filter { e -> java.time.Instant.ofEpochMilli(e.startMs).atZone(zone).let { l -> part.covers(l.hour, l.dayOfWeek) } }) })))
// Fallbacks and deferrals the planner recorded.
val fallbacks = steady.count { "fallback" in it.why }; val noBlock = steady.count { it.why.startsWith("no block") }
if (fallbacks + noBlock > 0) md.append("- **Planner fallbacks (3 weeks):** $fallbacks fallback, $noBlock outside any block.\n")
@@ -135,6 +148,7 @@ class ScheduleAuditTest {
flat.values.forEach { es -> es.sortedBy { it.second.startMs }.zipWithNext().forEach { (a, b) -> if (a.first != b.first && b.second.startMs < a.second.endMs) clashes += "${a.second.media.item.title.take(40)} on ${a.first} and ${b.first} at ${java.time.Instant.ofEpochMilli(b.second.startMs).atZone(zone).toLocalDateTime()}" } }
md.append("\n## Across channels\n\nThe same file on two channels at once (replay ignores the live clash check, so new bookings may show some): ${clashes.size}${if (clashes.isNotEmpty()) "\n\n" + clashes.take(20).joinToString("\n") { "- $it" } else ""}\n")
File(dir, "schedule-audit.md").writeText(md.toString())
File(dir, "schedule-audit.json").writeText(json.toString(1))
println("audit written: ${File(dir, "schedule-audit.md")}")
}
}