import Link from "next/link"; import type { Provenance } from "@/lib/provenance"; import type { BuildStatus } from "@/lib/manifest"; import { fmtDur } from "@/lib/format"; import NoteField from "@/components/NoteField"; import CopyButton from "@/components/CopyButton"; import PlanDiffPanel from "@/components/PlanDiffPanel"; import type { NoteMap } from "@/lib/notes"; import { concentrationLine, shares } from "@/lib/concentration"; // Where a cut came from: the recipe that built it, and every clip it plays. // // Server-rendered on purpose. The largest plan here is ~880 notes, and handing // those to a client component would put them on the wire twice -- once as JSON // for the props and again as the HTML they turn into. // How many note rows to render by default. // // Not a cosmetic limit. Each row is six cells of Tailwind class strings and two // links, and every one of them is ALSO serialised into the RSC flight payload -- // so it lands on the wire roughly twice. Pokemon's 269 notes already make a // 2.1 MB page; mario-rpg's 880-note plan would be over 7 MB, on a force-dynamic // route that re-renders on every navigation. // // The cap is stated rather than silent, and `?notes=all` renders the lot. A // truncation you cannot see reads as "this is the whole plan", which is exactly // the wrong thing for a provenance table to imply. const ROW_LIMIT = 250; export default function ProvenancePanel({ song, cut, prov, build, planParam, notes, allNotes = false, }: { song: string; cut: string; prov: Provenance; build: BuildStatus; planParam: string | null; notes: NoteMap; allNotes?: boolean; }) { const href = (plan: string | null) => `/browse/${song}/${cut}${plan ? `?plan=${encodeURIComponent(plan)}` : ""}`; const shown = allNotes ? prov.rows : prov.rows.slice(0, ROW_LIMIT); const hidden = prov.rows.length - shown.length; return (
the recipe
{build.entry ? ( <> {build.stale && ( /* A recipe that no longer describes the file is the whole risk of keeping this state at all. Say it loudly, above the recipe, and keep showing the recipe -- it is still the best lead. */

{build.stale} — treat everything below as a lead, not as fact

)}
script
{build.entry.script ?? "—"}
{Object.keys(build.entry.env).length > 0 && ( <>
env
{Object.entries(build.entry.env) .map(([k, v]) => `${k}=${v}`) .join(" ")}
)} {build.entry.plans.length > 0 && ( <>
plans
{build.entry.plans.join(", ")}
)} {build.entry.assets.length > 0 && ( <>
assets
{build.entry.assets.map((a) => (
{a.path.split("/").slice(-1)[0]} {a.missing ? " — MISSING" : ""}
))}
)} {build.entry.note && ( <>
note
{build.entry.note}
)}
) : (

Not recorded. The builder for this song does not name the plan it used, and guessing would put a recipe you cannot trust next to one you can — pick the plan below instead. A future build records itself with{" "} video-dir.mjs --song {song} --rel ….

)}
clips from {prov.source ? prov.source.name : "nothing yet"} {prov.shiftP90 !== null && ( |shift| p90 {prov.shiftP90.toFixed(2)} st )} {prov.rows.length > 0 && ( {prov.rows.length} notes · {prov.videos.length} sources {prov.voices.length > 0 && ` · ${prov.voices.join(", ")}`} )}
{/* How concentrated the draw is. The counts were already computed for the table below; nothing here costs a read. A song whose top five episodes supply most of it is a portrait of five videos, and that is worth knowing before quoting the corpus figure. */} {prov.videos.length > 0 && (

{concentrationLine(shares(prov.videos.map((v) => v.clips)))} ·{" "} across every song

)} {prov.available.length > 0 && (
plan {prov.available.map((p) => ( {p.replace(/\.json$/, "")} ))} {planParam && ( clear )}
)} {/* Picking a plan and comparing two are the same question asked twice, so the comparison lives with the picker rather than on a page of its own. It fetches nothing until asked. */} {prov.rows.length === 0 ? (

No clips.csv here and no plan chosen — pick one above.

) : ( <>
{prov.videos.map((v) => ( ))}
source clips date first moment
{v.title || v.video} {v.clips} {v.date} {v.link ? ( {fmtDur(v.firstSrcStart)} ) : ( {fmtDur(v.firstSrcStart)} )}
every note ({prov.rows.length}) {/* Re-trimming a clip from here gives the moved window a NEW key, so any plan that references the old one no longer resolves against the palette. Worth saying beside the link rather than discovering after a rebuild. */}

re-trimming a clip in the deck gives it a new key — a plan built on the old window will no longer find it

{hidden > 0 && (

showing the first {shown.length} by song time — {hidden} not shown.{" "} show all {prov.rows.length}

)}
{shown.map((r, i) => ( ))}
at voice word shift pile clip
{r.songTime.toFixed(2)} {r.voice} {r.word || "—"} {r.shift.toFixed(2)} {r.pile || "—"} {r.link && ( archive )} open in deck {/* The song-time of the note, cited straight out of the arrangement -- no need to find it in a player first. */} {r.clipId}
)}
); }