disc: gate composite bumper limb on a complete chapter table (#45)

A BD-J disc (Alita) has no authoritative static playlist chain, so main-
feature selection is a structural failsafe. Its chaptered feature 00800 is
a SUPERSET of an un-chaptered body subset 00703 that is 0.97 of its size,
so the size-only seamless bumper limb fired and demoted the real feature,
ripping the body instead.

Gate ONLY the bumper limb on is_complete_feature_presentation(q): the
wrapped/subset title must carry a real chapter table (>= MIN_FEATURE_CHAPTERS
marks spanning >= CHAPTER_SPAN_MIN_FRAC of its runtime). The concat limb
(much-shorter cut) is untouched. This is the ABSOLUTE-threshold predicate;
a one-line swap to the RELATIVE form (p.chapters.len() <= q.chapters.len())
is documented at the call site and in the predicate doc, pending a hoard
sweep of SM3's real chapter counts.

Also add lowest playlist_id as the final canonical_title_order tie-break so
equal-size seamless siblings (00800-00808) pick 00800 deterministically.

Fixtures re-based on the real chapter discriminator (not a size coincidence)
and new tests cover both topologies (Alita superset-feature, SM3 subset-
feature) plus the un-chaptered-subset keep-superset case.
This commit is contained in:
Matthew Jackson
2026-09-20 19:32:39 -07:00
parent f4ea6ea129
commit 827eaf0452
+249 -28
View File
@@ -514,6 +514,30 @@ const SEAMLESS_BUMPER_SIZE_FRAC: f64 = 0.90;
/// Authoring hint: honoured only when the title runs at least this fraction of the
/// longest probable-video title's duration.
const AUTHORING_MIN_DURATION_FRAC: f64 = 0.5;
/// Seamless-branch guard (issue #45): the minimum number of chapter marks a title
/// must carry to look like a COMPLETE feature presentation (a real feature has a
/// full chapter table; a bare body/branch subset carries one mark or none).
const MIN_FEATURE_CHAPTERS: usize = 2;
/// Seamless-branch guard (issue #45): a complete feature's chapter marks span at
/// least this fraction of its own runtime (start-to-last), distinguishing a real
/// chapter table from a couple of clustered marks.
const CHAPTER_SPAN_MIN_FRAC: f64 = 0.5;
/// Issue #45: does `q` look like a COMPLETE feature presentation — a real chapter
/// table (≥ [`MIN_FEATURE_CHAPTERS`] marks) spanning ≥ [`CHAPTER_SPAN_MIN_FRAC`]
/// of its runtime? A bare body subset (Alita's 00703 = one mark) fails; a genuine
/// feature (Alita's 00800 = 37 marks) passes. Gates the composite bumper limb so a
/// chaptered feature is not demoted below its own un-chaptered body subset. This is
/// the ABSOLUTE-threshold form; to switch to the RELATIVE form (only if a hoard
/// sweep finds an SM3-style decoy whose chaptered member is the SUPERSET) replace
/// the call site with `p.chapters.len() <= q.chapters.len()`. See docs/disc-mod.md.
fn is_complete_feature_presentation(q: &DiscTitle) -> bool {
q.chapters.len() >= MIN_FEATURE_CHAPTERS
&& q.duration_secs > 0.0
&& (q.chapters.last().map(|c| c.time_secs).unwrap_or(0.0)
- q.chapters.first().map(|c| c.time_secs).unwrap_or(0.0))
>= CHAPTER_SPAN_MIN_FRAC * q.duration_secs
}
/// Union a set of extents into sorted, merged, disjoint `(start_lba,
/// sector_count)` ranges — the pure, testable core of
@@ -2227,6 +2251,10 @@ impl Disc {
// (a full-audio main vs a stereo-only twin) — prefer lossless-multichannel.
.then_with(|| b.duration_secs.total_cmp(&a.duration_secs))
.then_with(|| Self::audio_richness(b).cmp(&Self::audio_richness(a)))
// Final determinism (issue #45): among equal-size seamless siblings
// (Alita 00800-00808, same body + different logo) pick the LOWEST
// playlist_id. Inert where an earlier key separates the pair.
.then_with(|| a.playlist_id.cmp(&b.playlist_id))
}
/// The keys [`Self::main_feature_order`] layers ON TOP of
@@ -2306,13 +2334,14 @@ impl Disc {
&& qj.len() < pi.len()
&& q.has_probable_video()
&& q.size_bytes as f64 >= WRAPPER_MIN_SIZE_FRAC * p.size_bytes as f64
// Seamless-branch guard (issue #45): wrapper only
// if Q is a much SHORTER cut (concat) or nearly P's
// SIZE-equal (bumper). See docs/disc-mod.md.
// Seamless-branch guard (issue #45): wrapper only if Q is
// a much SHORTER cut (concat) OR nearly P's SIZE AND a
// complete feature (bumper) — see is_complete_feature_presentation.
&& (q.duration_secs
< SEAMLESS_SHORTER_CUT_DURATION_FRAC * p.duration_secs
|| q.size_bytes as f64
>= SEAMLESS_BUMPER_SIZE_FRAC * p.size_bytes as f64)
|| (q.size_bytes as f64
>= SEAMLESS_BUMPER_SIZE_FRAC * p.size_bytes as f64
&& is_complete_feature_presentation(q)))
&& qj.is_subset(pi)
}
});
@@ -4516,6 +4545,34 @@ mod tests {
}
}
/// A BD title as [`bd_title`], plus a chapter table of `n_chapters` marks
/// evenly spanning `dur` (first at 0, last at `dur`) — models a COMPLETE
/// feature presentation for the issue #45 seamless-branch guard. `n_chapters`
/// must be ≥ 2 for the marks to span any runtime.
fn bd_title_chaptered(
playlist: &str,
id: u16,
dur: f64,
size: u64,
clip_ids: &[&str],
n_chapters: usize,
) -> DiscTitle {
let chapters = (0..n_chapters)
.map(|i| Chapter {
time_secs: if n_chapters <= 1 {
0.0
} else {
i as f64 * dur / (n_chapters - 1) as f64
},
name: (i + 1).to_string(),
})
.collect();
DiscTitle {
chapters,
..bd_title(playlist, id, dur, size, clip_ids)
}
}
/// Same, but with an empty STN stream list — models a title whose FIRST
/// PlayItem is a non-video bumper (`has_video()` is false; `streams` reflect
/// PlayItem 0 only).
@@ -4532,9 +4589,9 @@ mod tests {
}
}
// Regression for a real UHD decoy hang: 00245.mpls is a WRAPPER
// COMPOSITE (bumper+feature+outro) that is LONGEST/LARGEST; must be
// demoted STRUCTURALLY, not by stream count. See docs/disc-mod.md.
// Regression for a real UHD decoy hang: 00245.mpls is a WRAPPER COMPOSITE
// (bumper+feature+outro), LONGEST/LARGEST; demoted STRUCTURALLY by the chapter
// table (issue #45) — wrapped feature 00001 is chaptered, the decoy is not.
#[test]
fn main_feature_order_demotes_wrapper_composite_decoy() {
let decoy = bd_title_no_stn_video(
@@ -4544,7 +4601,7 @@ mod tests {
78_427_502_592,
&["00339", "00001", "00336"],
);
let feature = bd_title("00001.mpls", 1, 8038.4, 76_525_627_392, &["00001"]);
let feature = bd_title_chaptered("00001.mpls", 1, 8038.4, 76_525_627_392, &["00001"], 12);
let extra = bd_title("00248.mpls", 248, 794.2, 60_000_000_000, &["00248"]);
let capacity = 88_000_000_000u64;
@@ -4583,7 +4640,9 @@ mod tests {
78_000_000_000,
&["dead", "00001"],
);
let feature = bd_title("00001.mpls", 1, 8000.0, 76_000_000_000, &["00001"]);
// The wrapped feature carries the real chapter table (issue #45), so the
// bumper limb correctly detects 00245 as its wrapper composite.
let feature = bd_title_chaptered("00001.mpls", 1, 8000.0, 76_000_000_000, &["00001"], 12);
let small = vec![decoy.clone(), feature.clone()];
assert!(
Disc::rank_titles(&small, None, None)[0].composite,
@@ -4614,7 +4673,9 @@ mod tests {
78_427_502_592,
&["00001", "00336"],
);
let feature = bd_title("00001.mpls", 1, 8038.4, 76_525_627_392, &["00001"]);
// The wrapped feature carries the real chapter table (issue #45); the
// video-bearing wrapper carries none, so it stays demoted.
let feature = bd_title_chaptered("00001.mpls", 1, 8038.4, 76_525_627_392, &["00001"], 12);
let capacity = 88_000_000_000u64;
let mut titles = vec![composite, feature];
@@ -4648,29 +4709,32 @@ mod tests {
);
}
/// Issue #45 (SAFETY): a genuine seamless-branch FEATURE (00800, 4 clips,
/// 2h03) is a SUPERSET of its single-clip body decoy (00700, 1h50, ≥50 %
/// size). The old size-only composite gate mis-demoted 00800 as a "play-all
/// wrapper" and ripped the shorter decoy. The duration-shape guard keeps
/// 00800: its body subset runs almost as long (1h50 vs 2h03, ~0.89) yet is
/// markedly smaller (0.55) — the hallmark of seamless branching, not
/// concatenation.
/// Issue #45 (SAFETY): the REAL Alita BD-J numbers. Feature 00800 (4 clips,
/// 7317 s, 58.61 GB, 37 chapter marks) is a SUPERSET of its single-clip body
/// 00703 ([00688], 6951 s, 56.88 GB, 1 mark). The body is 0.97 of the feature's
/// size, so the size-only bumper limb fires (0.97 ≥ 0.90) while the body runs
/// over 0.85 of the feature so the concat limb does NOT — the OLD gate demoted
/// 00800 and ripped the body. The chapter-table guard keeps 00800: its body
/// subset carries one mark (< MIN_FEATURE_CHAPTERS), not a complete feature.
#[test]
fn seamless_branch_feature_not_demoted_issue_45() {
// 00800: real feature = body + intro/credits/seamless branch segments.
let feature = bd_title(
// 00800: real feature = body + intro/credits/seamless branch segments,
// with a full 37-mark chapter table.
let feature = bd_title_chaptered(
"00800.mpls",
800,
7380.0, // 2h03
60_000_000_000,
&["00800", "00801", "00802", "00803"],
7317.0,
58_610_000_000,
&["00687", "00688", "00674", "00689"],
37,
);
// 00700: single body clip, a PROPER subset, 1h50 (~89 % of 00800's
// duration) and 55 % of its size — enough to trip the old size gate.
let decoy = bd_title("00700.mpls", 700, 6600.0, 33_000_000_000, &["00800"]);
// 00703: single body clip, a PROPER subset, 6951 s (~0.95 of 00800's
// duration → concat limb inert) and 56.88 GB (~0.97 of its size → old
// bumper limb would fire) — one chapter mark, not a feature.
let decoy = bd_title_chaptered("00703.mpls", 703, 6951.0, 56_880_000_000, &["00688"], 1);
let capacity = 88_000_000_000u64;
// The body decoy is a proper subset with ≥50 % size — the OLD gate
// The body subset is a proper subset with ≥90 % size — the OLD gate
// flagged 00800 composite on that alone. It must no longer be flagged.
let ranks = Disc::rank_titles(&[feature.clone(), decoy.clone()], None, None);
assert!(
@@ -4719,6 +4783,161 @@ mod tests {
);
}
/// Issue #45 (Alita BD-J): the disc has NO authoritative static playlist
/// chain (First-Play is a BD-J Xlet, index title's playlist has an empty
/// TableOfAccessiblePlayLists), so selection is a pure structural failsafe.
/// The chaptered feature 00800 is a SUPERSET of its un-chaptered body subset
/// 00703 that is 0.97 of its size — the size-only bumper limb would demote
/// it. The chapter-table guard keeps 00800 (its body carries one mark).
#[test]
fn alita_bdj_body_subset_does_not_demote_chaptered_feature_issue_45() {
let feature = bd_title_chaptered(
"00800.mpls",
800,
7317.0,
58_610_000_000,
&["00687", "00688", "00674", "00689"],
37,
);
let body = bd_title_chaptered("00703.mpls", 703, 6951.0, 56_880_000_000, &["00688"], 1);
let capacity = 88_000_000_000u64;
let ranks = Disc::rank_titles(&[feature.clone(), body.clone()], None, None);
assert!(
!ranks[0].composite,
"the chaptered feature must not be flagged a wrapper composite of its body"
);
// No hint, no nav — the BD-J disc gives neither; selection is structural.
let mut titles = vec![body, feature];
Disc::sort_titles_by_main_feature(&mut titles, capacity, None, None);
assert_eq!(
titles[0].playlist_id, 800,
"the chaptered feature is selected, not its un-chaptered body subset"
);
}
/// Issue #45 (secondary determinism): the nine feature variants 00800-00808
/// share the body clip 00688 and differ only by opening logo, so they are
/// equal in size/duration/audio — the lowest-playlist_id final tie-break
/// deterministically picks 00800. All nine rank above the un-chaptered body
/// subset 00703, and none is demoted as a composite of it.
#[test]
fn alita_full_family_ranks_above_body() {
let mut titles: Vec<DiscTitle> = (0..9)
.map(|k| {
let id = 800 + k as u16;
bd_title_chaptered(
&format!("008{k:02}.mpls"),
id,
7317.0,
58_610_000_000,
&[&format!("logo{k}"), "00688"],
37,
)
})
.collect();
// The bare body: a proper subset of every family member, one chapter mark,
// 0.97 of their size (would trip the old bumper limb).
titles.push(bd_title_chaptered(
"00703.mpls",
703,
6951.0,
56_880_000_000,
&["00688"],
1,
));
let capacity = 88_000_000_000u64;
let ranks = Disc::rank_titles(&titles, None, None);
assert!(
ranks.iter().all(|r| !r.composite),
"no equal-size seamless sibling nor its body subset is a composite"
);
Disc::sort_titles_by_main_feature(&mut titles, capacity, None, None);
assert_eq!(
titles[0].playlist_id, 800,
"the lowest-id feature variant is the deterministic pick among equals"
);
assert_eq!(
titles.last().unwrap().playlist_id,
703,
"the smaller body subset ranks below the whole feature family"
);
}
/// Issue #45 (SM3 topology, opposite of Alita): the real chaptered feature
/// 00001 is the SUBSET; the decoy 00245 = [bumper][feature][outro] is the
/// SUPERSET. Because the subset IS a complete feature presentation (real
/// chapter table), the bumper limb still fires and the wrapper stays demoted.
#[test]
fn bumper_wrapper_with_chaptered_subset_still_demoted() {
let wrapper = bd_title(
"00245.mpls",
245,
8350.0,
78_000_000_000,
&["00339", "00001", "00336"],
);
// 0.98 of the wrapper's size (bumper limb) and a full chapter table.
let feature = bd_title_chaptered("00001.mpls", 1, 8038.0, 76_500_000_000, &["00001"], 24);
let capacity = 88_000_000_000u64;
let ranks = Disc::rank_titles(&[wrapper.clone(), feature.clone()], None, None);
assert!(
ranks[0].composite,
"a bumper wrapper of a CHAPTERED feature subset is still a composite"
);
assert!(
!ranks[1].composite,
"the chaptered feature subset is standalone"
);
let mut titles = vec![wrapper, feature];
Disc::sort_titles_by_main_feature(&mut titles, capacity, None, None);
assert_eq!(
titles[0].playlist_id, 1,
"the chaptered feature wins; its bumper wrapper is demoted"
);
}
/// Issue #45: when the bumper-size subset carries NO chapter table it is not
/// a complete feature, so the bumper limb does not fire and the SUPERSET is
/// kept — the exact mechanism that saves Alita's 00800 over its body 00703.
#[test]
fn un_chaptered_bumper_subset_keeps_superset() {
// Superset feature, chaptered.
let superset = bd_title_chaptered(
"00800.mpls",
800,
7317.0,
58_610_000_000,
&["00687", "00688", "00674", "00689"],
30,
);
// Subset: 0.97 of the superset's size (bumper limb size), but NO chapters.
let subset = bd_title("00703.mpls", 703, 6951.0, 56_880_000_000, &["00688"]);
assert!(
subset.chapters.is_empty(),
"the body subset carries no marks"
);
let capacity = 88_000_000_000u64;
let ranks = Disc::rank_titles(&[superset.clone(), subset.clone()], None, None);
assert!(
!ranks[0].composite,
"an un-chaptered bumper-size subset must not demote its superset"
);
let mut titles = vec![subset, superset];
Disc::sort_titles_by_main_feature(&mut titles, capacity, None, None);
assert_eq!(
titles[0].playlist_id, 800,
"the superset feature is kept, not the un-chaptered body subset"
);
}
/// The disc's own authoring-designated feature wins even when it is NOT the
/// longest/largest title — a size-independent signal a decoy cannot forge.
#[test]
@@ -4784,7 +5003,9 @@ mod tests {
78_000_000_000,
&["00339", "00001", "00336"],
);
let feature = bd_title("00001.mpls", 1, 8000.0, 76_000_000_000, &["00001"]);
// The wrapped feature carries the real chapter table (issue #45), so the
// streamless wrapper 00245 is detected as a composite and demoted.
let feature = bd_title_chaptered("00001.mpls", 1, 8000.0, 76_000_000_000, &["00001"], 12);
let capacity = 88_000_000_000u64;
let mut titles = vec![streamless, feature];