commit 8f4ea0d892edc648c58520cf4c200eddc7e5bc30
parent 87cf6431b0597a1337bca77ffe95947d2e31b8d5
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 5 Oct 2026 18:59:23 -0400
reports: prepare and compose share one cited span (clamped to the recording's end), so a clip cut at the end is not stale
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 24 insertions(+), 16 deletions(-)
diff --git a/common/lib/evidenceClip-server.ts b/common/lib/evidenceClip-server.ts
@@ -47,7 +47,7 @@ import { mkdir, readFile, rename, rm, stat } from "node:fs/promises";
import path from "node:path";
import { execa } from "execa";
import { PUBLISH_MAX_FILE_BYTES } from "./builtExport";
-import { tmpPathFor, writeJsonAtomic } from "./jsonFile-server";
+import { readJsonFile, tmpPathFor, writeJsonAtomic } from "./jsonFile-server";
import { roundMomentSeconds } from "./citations/moments";
import type { CitationPad } from "./citations/schema";
import {
@@ -91,6 +91,24 @@ export function clampSpanToDuration(span: EvidenceSpan, duration: number | null
return { from: span.from, to: Number(duration.toFixed(3)) };
}
+// A record's duration (its metadata.info.json), or null when it is not recorded.
+export async function recordDuration(channelsDir: string, slug: string, id: string): Promise<number | null> {
+ const read = await readJsonFile(path.join(channelsDir, slug, "data", id, "metadata.info.json"));
+ const d = read.ok ? Number((read.value as { duration?: unknown } | null)?.duration) : NaN;
+ return Number.isFinite(d) && d > 0 ? d : null;
+}
+
+// The span a cited moment's clip is cut for: `evidenceSpan`, its end clamped to
+// the record's duration. Prepare and compose both use it, so they agree.
+export async function citedEvidenceSpan(
+ channelsDir: string,
+ slug: string,
+ id: string,
+ c: { start: number; end: number; pad?: CitationPad },
+): Promise<EvidenceSpan> {
+ return clampSpanToDuration(evidenceSpan(c), await recordDuration(channelsDir, slug, id));
+}
+
// The span a citation's clip covers: its moment's (rounded) start and end,
// widened by its pad, the start clamped at 0. Rounded to the millisecond so a
// float's last digits never change a hash.
diff --git a/common/publish/composeReports.ts b/common/publish/composeReports.ts
@@ -112,7 +112,7 @@ import {
type ReportDownloads,
type ReportPageView,
} from "../lib/report/views";
-import { evidenceSpan, isAudioOnlyPlatform, type EvidenceSpan } from "../lib/evidenceClip-server";
+import { citedEvidenceSpan, isAudioOnlyPlatform, type EvidenceSpan } from "../lib/evidenceClip-server";
import {
citedMoments,
loadSiteReports,
@@ -594,7 +594,7 @@ export async function resolveSiteReports(opts: ResolveSiteReportsOptions): Promi
// ship the old cut under the new moment's page.
const sidecar = await readJsonFile(path.join(cacheDir, entry.file.replace(/\.[^.]+$/, ".json")));
const cut = sidecar.ok ? (sidecar.value as { span?: EvidenceSpan }).span : undefined;
- const want = evidenceSpan({ start: m.moment.start, end: m.moment.end, pad: m.pad });
+ const want = await citedEvidenceSpan(paths.channelsDir, m.moment.channel, m.moment.id, { start: m.moment.start, end: m.moment.end, pad: m.pad });
if (cut && !sameSpan(cut, want)) {
missing("stale-media", `the clip was cut for ${cut.from}–${cut.to} s, the reports now cite ${want.from}–${want.to} s — prepare again`);
continue;
@@ -737,7 +737,7 @@ export async function resolveSiteReports(opts: ResolveSiteReportsOptions): Promi
}
const m = parseMomentKey(key) as SpanMoment;
const info = momentInfo.get(key)!;
- const span = evidenceSpan({ start: m.start, end: m.end, pad: info.pad });
+ const span = await citedEvidenceSpan(paths.channelsDir, m.channel, m.id, { start: m.start, end: m.end, pad: info.pad });
// The transcript the moment's first citation matched best (see the
// verification above), else the record's default cues.
const cues = momentCues.get(key) ?? records.get(`${c.channel}/${c.id}`)!.cues;
diff --git a/common/publish/reportMedia.ts b/common/publish/reportMedia.ts
@@ -56,8 +56,7 @@ import type { CitationPad } from "../lib/citations/schema";
import { parseReport } from "../lib/report/validate";
import type { Report } from "../lib/report/schema";
import {
- clampSpanToDuration,
- evidenceSpan,
+ citedEvidenceSpan,
isAudioOnlyPlatform,
prepareEvidenceClip,
widerPad,
@@ -259,10 +258,7 @@ export async function prepareReportMedia(opts: PrepareReportMediaOptions): Promi
continue;
}
if (m.moment.kind !== "span") continue;
- const span = clampSpanToDuration(
- evidenceSpan({ start: m.moment.start, end: m.moment.end, pad: m.pad }),
- await recordDuration(paths.channelsDir, slug, m.moment.id),
- );
+ const span = await citedEvidenceSpan(paths.channelsDir, slug, m.moment.id, { start: m.moment.start, end: m.moment.end, pad: m.pad });
const r = await prepareEvidenceClip({
channelsDir: paths.channelsDir,
slug,
@@ -373,9 +369,3 @@ export async function readReportMediaIndex(paths: Paths, siteId: string): Promis
return v as ReportMediaIndex;
}
-// A record's duration (metadata.info.json), or null when it is not recorded.
-async function recordDuration(channelsDir: string, slug: string, id: string): Promise<number | null> {
- const read = await readJsonFile(path.join(channelsDir, slug, "data", id, "metadata.info.json"));
- const d = read.ok ? Number((read.value as { duration?: unknown } | null)?.duration) : NaN;
- return Number.isFinite(d) && d > 0 ? d : null;
-}