labels: per-parser confidence + highest-confidence-wins registry

Replaces 'first-match-wins by array order' with 'highest-confidence-
wins, array order tiebreaker'. Removes the arbitrariness when more
than one parser can claim a disc (e.g. one with both
bluray_project.bin and playlists.xml).

New types in labels::mod:
  pub enum Confidence { Medium, High }
  pub struct ParseResult { labels: Vec<StreamLabel>, confidence }
  ParseResult::high(labels) / ::medium(labels) constructors

Parser signature change: every parse() now returns
Option<ParseResult> instead of Option<Vec<StreamLabel>>. Updated all
six parsers in lockstep:
  paramount: High (fully structured XML)
  criterion: High (fully structured XML)
  pixelogic: High by default, Medium when an unknown token component
             is encountered (the skip-unknown path now propagates the
             coverage gap to the caller instead of silently degrading)
  ctrm:      High (structured key-value)
  dbp:       High (anchor scan with vocab routing)
  deluxe:    still returns None pending Phase D — signature aligned

Registry behavior:
  extract() iterates all detect-positive parsers, picks highest
  Confidence with non-empty labels. Equal confidence falls to array
  order (deterministic). Same selection logic in analyze().

LabelAnalysis grew a confidence: Option<Confidence> field so the
diagnostic surface (freemkv-tools labels-analyze) exposes which
confidence tier the selected parser claimed. labels-analyze JSON
and labels-corpus-check structural diff both gained the field.

Precommit (cargo +1.86 fmt + clippy + test) green.
This commit is contained in:
MattJackson
2026-05-10 16:01:20 -07:00
parent 4226a53e73
commit 7c6b0f82ab
7 changed files with 195 additions and 80 deletions
+13 -7
View File
@@ -4,7 +4,7 @@
//! When both exist, language_streams.txt provides structured types while
//! menu_base.prop provides stream number → button name mapping.
use super::{LabelPurpose, LabelQualifier, StreamLabel, StreamLabelType, vocab};
use super::{LabelPurpose, LabelQualifier, ParseResult, StreamLabel, StreamLabelType, vocab};
use crate::sector::SectorReader;
use crate::udf::UdfFs;
use std::collections::HashMap;
@@ -14,7 +14,7 @@ pub fn detect(udf: &UdfFs) -> bool {
|| super::jar_file_exists(udf, "language_streams.txt")
}
pub fn parse(reader: &mut dyn SectorReader, udf: &UdfFs) -> Option<Vec<StreamLabel>> {
pub fn parse(reader: &mut dyn SectorReader, udf: &UdfFs) -> Option<ParseResult> {
// Try language_streams.txt first (richer structured data)
let ls_labels = parse_language_streams(reader, udf);
@@ -22,12 +22,18 @@ pub fn parse(reader: &mut dyn SectorReader, udf: &UdfFs) -> Option<Vec<StreamLab
let mb_labels = parse_menu_base(reader, udf);
// If we have both, merge: language_streams for structure, menu_base for names
match (ls_labels, mb_labels) {
(Some(ls), Some(mb)) => Some(merge(ls, mb)),
(Some(ls), None) => Some(ls),
(None, Some(mb)) => Some(mb),
(None, None) => None,
let labels = match (ls_labels, mb_labels) {
(Some(ls), Some(mb)) => merge(ls, mb),
(Some(ls), None) => ls,
(None, Some(mb)) => mb,
(None, None) => return None,
};
if labels.is_empty() {
return None;
}
// High confidence: both language_streams.txt and menu_base.prop
// are structured key-value formats with documented types.
Some(ParseResult::high(labels))
}
fn merge(ls: Vec<StreamLabel>, mb: Vec<StreamLabel>) -> Vec<StreamLabel> {