diff --git a/crates/sylpheed-cli/src/main.rs b/crates/sylpheed-cli/src/main.rs index e847cb20..877ef2df 100644 --- a/crates/sylpheed-cli/src/main.rs +++ b/crates/sylpheed-cli/src/main.rs @@ -38,7 +38,6 @@ use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; use colored::*; use indicatif::{ProgressBar, ProgressStyle}; -use tracing::info; use sylpheed_formats::vfs::{identify_format, GameAssets}; use sylpheed_formats::{IdxdObject, PakArchive}; @@ -572,6 +571,11 @@ fn print_geometry(b: &sylpheed_formats::ui_layout::UiBuild, bytes: &[u8]) { } } +// 8 parameters against a threshold of 7 — a plain function, unlike the Bevy +// systems in the viewer, so this one is real if mild. Left as-is because the +// arguments are the CLI flags this subcommand takes; grouping them into a +// struct is a change to the command surface, not a lint fix. +#[allow(clippy::too_many_arguments)] fn cmd_screen_render( pak: &Path, output: &Path, @@ -892,7 +896,7 @@ fn cmd_sniff(dir: &Path, unknown_only: bool) -> Result<()> { println!(); println!("{}", "Format Summary:".bold()); let mut summary: Vec<_> = counts.into_iter().collect(); - summary.sort_by(|a, b| b.1.cmp(&a.1)); + summary.sort_by_key(|&(_, count)| std::cmp::Reverse(count)); for (fmt, count) in summary { println!( " {:>6} .{}", @@ -1034,7 +1038,7 @@ fn cmd_mesh_info(file: &Path) -> Result<()> { let nv = sub.positions.len(); let mut referenced = vec![false; nv]; let (mut degen, mut oob, mut imax) = (0usize, 0usize, 0u32); - for tri in sub.indices.chunks_exact(3) { + for tri in sub.indices.as_chunks::<3>().0 { let (a, b, c) = (tri[0], tri[1], tri[2]); imax = imax.max(a).max(b).max(c); if a == b || b == c || a == c { @@ -1056,7 +1060,9 @@ fn cmd_mesh_info(file: &Path) -> Result<()> { }; let mut maxedges: Vec = sub .indices - .chunks_exact(3) + .as_chunks::<3>() + .0 + .iter() .map(|t| edge(t[0], t[1]).max(edge(t[1], t[2])).max(edge(t[0], t[2]))) .collect(); maxedges.sort_by(|a, b| a.partial_cmp(b).unwrap()); @@ -1220,7 +1226,9 @@ fn cmd_mesh_render( let med = { let mut e: Vec = sub .indices - .chunks_exact(3) + .as_chunks::<3>() + .0 + .iter() .filter(|t| (t[0] as usize) < n && (t[1] as usize) < n && (t[2] as usize) < n) .map(|t| { let d = |a: u32, b: u32| { @@ -1254,7 +1262,7 @@ fn cmd_mesh_render( (p[2] - center[2]) * scale * mirror[2] + cell[2], ] }; - for tri in sub.indices.chunks_exact(3) { + for tri in sub.indices.as_chunks::<3>().0 { let (a, b, c) = (tri[0] as usize, tri[1] as usize, tri[2] as usize); if a < n && b < n && c < n { if span_only || span_hide { @@ -1440,7 +1448,9 @@ fn decode_to_rgba8(tex: &sylpheed_formats::texture::X360Texture) -> Result().0; + let dst = rgba.as_chunks_mut::<4>().0; + for (px, out) in src.iter().zip(dst) { out[0] = px[1]; // R out[1] = px[2]; // G out[2] = px[3]; // B @@ -1690,12 +1700,12 @@ fn cmd_pak_textures(pak: &Path, output: &Path, verbose: bool) -> Result<()> { let mut idx = 0usize; while let Some(pos) = payload[off..] .windows(4) - .position(|w| w == &t8ad::T8AD_MAGIC) + .position(|w| w == t8ad::T8AD_MAGIC) { let start = off + pos; let next = payload[start + 4..] .windows(4) - .position(|w| w == &t8ad::T8AD_MAGIC) + .position(|w| w == t8ad::T8AD_MAGIC) .map(|p| start + 4 + p) .unwrap_or(payload.len()); emit_t8ad( diff --git a/crates/sylpheed-export/src/audio.rs b/crates/sylpheed-export/src/audio.rs index 1323f053..598778c8 100644 --- a/crates/sylpheed-export/src/audio.rs +++ b/crates/sylpheed-export/src/audio.rs @@ -109,6 +109,8 @@ pub struct BgmSpec { pub loop_start_s: Option, #[serde(default)] pub loop_end_s: Option, + // Deserialised to model the sidecar schema, not read in Rust. + #[allow(dead_code)] #[serde(default)] pub loop_end_why: Option, #[serde(default)] diff --git a/crates/sylpheed-export/src/main.rs b/crates/sylpheed-export/src/main.rs index 58c5a8d3..ef982b7e 100644 --- a/crates/sylpheed-export/src/main.rs +++ b/crates/sylpheed-export/src/main.rs @@ -158,6 +158,9 @@ fn load_names(authored: &Path) -> Result { #[derive(serde::Deserialize)] struct File { archives: NameMap, + // Deserialised to model the on-disc schema, not read in Rust. + // Removing it would silently change what this struct accepts. + #[allow(dead_code)] #[serde(default)] also_export: AlsoExport, } @@ -275,7 +278,7 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> { // `video/` that this run did not claim, so a movie that stops being exported // still stops existing. if out.exists() { - for entry in std::fs::read_dir(&out).context("clear the output tree")? { + for entry in std::fs::read_dir(out).context("clear the output tree")? { let entry = entry?; if entry.file_name() == "video" { continue; @@ -288,7 +291,7 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> { .with_context(|| format!("clear {}", entry.path().display()))?; } } - std::fs::create_dir_all(&out)?; + std::fs::create_dir_all(out)?; let archive = "dat/GP_TITLE.pak"; let pak = disc.join(archive); @@ -315,7 +318,7 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> { None => (format!("build_{entry:02}"), "index", None), }; let ex = screen::export_build( - &out, + out, archive, *entry, build_idx, diff --git a/crates/sylpheed-formats/src/audio.rs b/crates/sylpheed-formats/src/audio.rs index 90bd0ee0..ed98b87e 100644 --- a/crates/sylpheed-formats/src/audio.rs +++ b/crates/sylpheed-formats/src/audio.rs @@ -226,9 +226,10 @@ fn parse_riff_wave(bytes: &[u8]) -> Option { match codec { AudioCodec::Pcm | AudioCodec::PcmFloat => { if let (Some(d), true) = (data_bytes, channels > 0 && bits > 0) { + // `bits` under 8 makes the frame size zero, so this stays + // fallible: `checked_div` states that once, where it happens. let frame = channels as u64 * (bits as u64 / 8); - if frame > 0 { - let spc = d / frame; + if let Some(spc) = d.checked_div(frame) { info.samples_per_channel = Some(spc); if rate > 0 { info.duration_secs = Some(spc as f32 / rate as f32); @@ -275,11 +276,15 @@ impl GameAudio { let samples: Vec = match (info.codec, bits) { (AudioCodec::Pcm, 8) => data.iter().map(|&b| (b as f32 - 128.0) / 128.0).collect(), (AudioCodec::Pcm, 16) => data - .chunks_exact(2) + .as_chunks::<2>() + .0 + .iter() .map(|c| i16::from_le_bytes([c[0], c[1]]) as f32 / 32768.0) .collect(), (AudioCodec::Pcm, 24) => data - .chunks_exact(3) + .as_chunks::<3>() + .0 + .iter() .map(|c| { let v = ((c[2] as i32) << 16) | ((c[1] as i32) << 8) | c[0] as i32; let v = (v << 8) >> 8; // sign-extend 24→32 @@ -287,7 +292,9 @@ impl GameAudio { }) .collect(), (AudioCodec::PcmFloat, 32) => data - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])) .collect(), _ => return Err(AudioError::UnsupportedPcm { tag: 0, bits }), diff --git a/crates/sylpheed-formats/src/game_data.rs b/crates/sylpheed-formats/src/game_data.rs index 08dcbbab..de613537 100644 --- a/crates/sylpheed-formats/src/game_data.rs +++ b/crates/sylpheed-formats/src/game_data.rs @@ -1131,7 +1131,9 @@ pub fn load_squadrons(pak: &PakArchive) -> Vec { let Some(f) = records.record(id) else { continue }; let slots: Vec<&str> = f.positional.iter().map(|(_, v)| v.as_str()).collect(); let members = slots - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .map(|m| SquadronMember { unit: m[0].to_string(), message_set: text(Some(m[1])), diff --git a/crates/sylpheed-formats/src/hash.rs b/crates/sylpheed-formats/src/hash.rs index 7303f1ed..82ac0843 100644 --- a/crates/sylpheed-formats/src/hash.rs +++ b/crates/sylpheed-formats/src/hash.rs @@ -71,7 +71,7 @@ pub fn name_hash(name: &str) -> u32 { // through untouched, including any high-bit bytes, which are then sign-extended). let mut bytes = name.as_bytes().to_vec(); for byte in &mut bytes { - if (b'A'..=b'Z').contains(byte) { + if byte.is_ascii_uppercase() { *byte += 0x20; } } diff --git a/crates/sylpheed-formats/src/ixud.rs b/crates/sylpheed-formats/src/ixud.rs index 6589705a..fd4db12f 100644 --- a/crates/sylpheed-formats/src/ixud.rs +++ b/crates/sylpheed-formats/src/ixud.rs @@ -119,7 +119,7 @@ pub fn parse(bytes: &[u8]) -> Option { fn utf16be_tokens(bytes: &[u8]) -> Vec { let mut tokens = Vec::new(); let mut cur = String::new(); - for pair in bytes.chunks_exact(2) { + for pair in bytes.as_chunks::<2>().0 { let u = u16::from_be_bytes([pair[0], pair[1]]); match char::from_u32(u as u32) { Some(c) if !c.is_control() => cur.push(c), diff --git a/crates/sylpheed-formats/src/mesh.rs b/crates/sylpheed-formats/src/mesh.rs index 84fa4fbf..a2da4384 100644 --- a/crates/sylpheed-formats/src/mesh.rs +++ b/crates/sylpheed-formats/src/mesh.rs @@ -700,7 +700,7 @@ impl Xbg7Model { let (vc, ic) = r.markers[0]; let sig = (r.decl.stride, vc, ic); let floor = last_by_sig.get(&sig).map_or(0, |o| o + 1); - if m.meshes[0].vbuf_offset.map_or(false, |o| o < floor) { + if m.meshes[0].vbuf_offset.is_some_and(|o| o < floor) { if let Some(alt) = anchor_pool_mesh( bytes, starts, @@ -1339,6 +1339,13 @@ fn vertex_run_starts(bytes: &[u8], data_base: usize, stride: usize) -> Vec bytes.len() - || vc.checked_mul(decls[i].stride).map_or(true, |b| vb + b > bytes.len()) + || vc.checked_mul(decls[i].stride).is_none_or(|b| vb + b > bytes.len()) { break; } @@ -1784,7 +1798,7 @@ fn anchor_grouped_meshes( } let (degen, wind) = index_run_quality(bytes, ib_k, vb_k, ick, &decls[kmax]); let cand = (degen, -wind, pad); - if best.map_or(true, |b| cand < b) { + if best.is_none_or(|b| cand < b) { best = Some(cand); } if pad_first_match() { @@ -1797,7 +1811,7 @@ fn anchor_grouped_meshes( if meshes.len() == n { return meshes; } - if partial.as_ref().map_or(true, |p| meshes.len() > p.len()) { + if partial.as_ref().is_none_or(|p| meshes.len() > p.len()) { partial = Some(meshes); } } @@ -1929,7 +1943,7 @@ fn find_index_marker(desc: &[u8]) -> Option<(usize, usize)> { while rel + 40 <= desc.len() { let a = be32(desc, rel); let c = be32(desc, rel + 4); - if c >= 3 && c % 3 == 0 && c < 400_000 && a == c * 2 { + if c >= 3 && c.is_multiple_of(3) && c < 400_000 && a == c * 2 { return Some((rel, c as usize)); } rel += 4; @@ -1951,7 +1965,7 @@ fn all_index_markers(desc: &[u8]) -> Vec<(usize, usize)> { while rel + 8 <= desc.len() { let a = be32(desc, rel); let c = be32(desc, rel + 4); - if c >= 3 && c % 3 == 0 && c < 400_000 && a == c * 2 && rel >= 32 { + if c >= 3 && c.is_multiple_of(3) && c < 400_000 && a == c * 2 && rel >= 32 { let vc = be32(desc, rel - 32) as usize; if (3..=200_000).contains(&vc) { out.push((vc, c as usize)); @@ -1979,7 +1993,7 @@ fn all_vertex_decls(desc: &[u8]) -> Vec { while rel + 8 <= desc.len() { let a = be32(desc, rel); let c = be32(desc, rel + 4); - if c >= 3 && c % 3 == 0 && c < 400_000 && a == c * 2 && rel >= 32 { + if c >= 3 && c.is_multiple_of(3) && c < 400_000 && a == c * 2 && rel >= 32 { let vc = be32(desc, rel - 32) as usize; if (3..=200_000).contains(&vc) { match parse_decl_at(desc, rel + 8) { @@ -2086,9 +2100,8 @@ fn submesh_records(desc: &[u8]) -> Vec<(usize, usize)> { let t = be32(desc, rel + 12); if (3..=65535).contains(&a) && z == 0 - && c >= 3 - && c <= 200_000 - && c % 3 == 0 + && (3..=200_000).contains(&c) + && c.is_multiple_of(3) && (1..=64).contains(&t) { out.push((a as usize, c as usize)); @@ -2151,7 +2164,7 @@ fn topology_report( let mut agree = 0usize; let mut counted = 0usize; let mut edges: HashMap<(u32, u32), u32> = HashMap::new(); - for t in indices.chunks_exact(3) { + for t in indices.as_chunks::<3>().0 { let (a, b, c) = (t[0] as usize, t[1] as usize, t[2] as usize); if a == b || b == c || a == c { degen += 1; @@ -2330,7 +2343,7 @@ pub fn submesh_albedos(bytes: &[u8], resource_name: &str) -> Vec<(usize, usize, if (3..=65535).contains(&vtx) && z == 0 && idx >= 3 - && idx % 3 == 0 + && idx.is_multiple_of(3) && idx < 400_000 && tail > 0 && tail < 0x10_0000 @@ -2423,7 +2436,7 @@ pub fn material_groups(bytes: &[u8], resource_name: &str, target: usize) -> Vec< if (1..=70000).contains(&vtx) && tail == 4 && cnt >= 3 - && cnt % 3 == 0 + && cnt.is_multiple_of(3) && cnt < 400_000 && (off as usize) < 4_000_000 { diff --git a/crates/sylpheed-formats/src/movie_manifest.rs b/crates/sylpheed-formats/src/movie_manifest.rs index 30c5fb5e..8fe955be 100644 --- a/crates/sylpheed-formats/src/movie_manifest.rs +++ b/crates/sylpheed-formats/src/movie_manifest.rs @@ -2,7 +2,7 @@ //! table (statically reversed). Beyond the movie→voice binding, it defines the //! whole cutscene *structure*: which video (+ subtitle, + on-screen text overlay, //! + voice) plays at each mission phase, and of what kind (story intro, in-mission -//! phase cutscene, resupply, …). +//! phase cutscene, resupply, …). //! //! ## On-disc structure (`dat/tables.pak`, entry hash `0x5b983a08`) //! @@ -339,12 +339,10 @@ fn ascii_runs(bytes: &[u8], min: usize) -> Vec { for &b in bytes { if (0x20..=0x7e).contains(&b) { cur.push(b as char); + } else if cur.len() >= min { + out.push(std::mem::take(&mut cur)); } else { - if cur.len() >= min { - out.push(std::mem::take(&mut cur)); - } else { - cur.clear(); - } + cur.clear(); } } if cur.len() >= min { diff --git a/crates/sylpheed-formats/src/movie_voice.rs b/crates/sylpheed-formats/src/movie_voice.rs index a28572cc..3a042d34 100644 --- a/crates/sylpheed-formats/src/movie_voice.rs +++ b/crates/sylpheed-formats/src/movie_voice.rs @@ -96,7 +96,7 @@ pub fn find_descriptor_before(buf: &[u8], before: usize) -> Option { let mut o = (before - 1).min(cap) & !3; loop { let id = be(o); - if id >= 1 && id < ID_MAX && be(o + 4) == DESC_MARK && be(o + DESC_REPEAT) == id { + if (1..ID_MAX).contains(&id) && be(o + 4) == DESC_MARK && be(o + DESC_REPEAT) == id { return Some(o); } if o < 4 { diff --git a/crates/sylpheed-formats/src/ship_capture.rs b/crates/sylpheed-formats/src/ship_capture.rs index b1d50279..5dac0ec8 100644 --- a/crates/sylpheed-formats/src/ship_capture.rs +++ b/crates/sylpheed-formats/src/ship_capture.rs @@ -287,13 +287,13 @@ pub fn parse_drawlog(text: &str) -> Vec { let mut pos: Vec<[f32; 3]> = Vec::new(); let mut consts: Vec<(usize, [f64; 4])> = Vec::new(); - let mut flush = |base: u32, - size: u32, - stride: u32, - pos: &mut Vec<[f32; 3]>, - consts: &[(usize, [f64; 4])], - seen: &mut std::collections::HashSet, - out: &mut Vec| { + let flush = |base: u32, + size: u32, + stride: u32, + pos: &mut Vec<[f32; 3]>, + consts: &[(usize, [f64; 4])], + seen: &mut std::collections::HashSet, + out: &mut Vec| { let pos = std::mem::take(pos); if base == 0 || stride == 0 || !seen.insert(base) { return; @@ -426,7 +426,7 @@ pub fn correlate( let Some((score, mirrored)) = pos_validate(d, &key.ref_pos) else { continue; // positions disagree — not this part }; - if best.map_or(true, |(_, s, _)| score > s) { + if best.is_none_or(|(_, s, _)| score > s) { best = Some((d, score, mirrored)); } } diff --git a/crates/sylpheed-formats/src/slb.rs b/crates/sylpheed-formats/src/slb.rs index 9b198b22..01355daa 100644 --- a/crates/sylpheed-formats/src/slb.rs +++ b/crates/sylpheed-formats/src/slb.rs @@ -517,7 +517,7 @@ pub fn to_xma_riff_best(slb: &[u8]) -> Option> { while let Some(di) = find(slb, b"data", i) { let declared = le32(slb, di + 4).unwrap_or(0) as usize; let usable = declared.min(slb.len().saturating_sub(di + 8)); - if best.map_or(true, |(_, b)| usable > b) { + if best.is_none_or(|(_, b)| usable > b) { best = Some((di, usable)); } i = di + 4; diff --git a/crates/sylpheed-formats/src/texture.rs b/crates/sylpheed-formats/src/texture.rs index 4d791bb4..b3dcfdf3 100644 --- a/crates/sylpheed-formats/src/texture.rs +++ b/crates/sylpheed-formats/src/texture.rs @@ -587,19 +587,19 @@ pub fn apply_endian_swap(data: &mut [u8], endianness: u8) { match endianness { 1 => { // k8in16 — swap the two bytes of each 16-bit half. - for c in data.chunks_exact_mut(2) { + for c in data.as_chunks_mut::<2>().0 { c.swap(0, 1); } } 2 => { // k8in32 — reverse each 32-bit word. - for c in data.chunks_exact_mut(4) { + for c in data.as_chunks_mut::<4>().0 { c.reverse(); } } 3 => { // k16in32 — swap the two 16-bit halves of each 32-bit word. - for c in data.chunks_exact_mut(4) { + for c in data.as_chunks_mut::<4>().0 { c.swap(0, 2); c.swap(1, 3); } @@ -626,8 +626,8 @@ fn decode_surface( } else { // Linear layout — copy only the mip-0 slice. let block_size = format.block_size() as u32; - let bw = ((width + block_size - 1) / block_size).max(1); - let bh = ((height + block_size - 1) / block_size).max(1); + let bw = width.div_ceil(block_size).max(1); + let bh = height.div_ceil(block_size).max(1); let needed = bw as usize * bh as usize * format.bytes_per_block(); if raw_data.len() < needed { return Err(TextureError::BufferTooSmall { needed, have: raw_data.len() }); @@ -651,7 +651,7 @@ fn decode_surface( match format { X360TextureFormat::Dxt1 => swap_bc_block_dwords(&mut linear_data), X360TextureFormat::Dxt3 | X360TextureFormat::Dxt5 => { - for block in linear_data.chunks_exact_mut(16) { + for block in linear_data.as_chunks_mut::<16>().0 { swap_bc_block_dwords(&mut block[8..16]); } } @@ -666,8 +666,8 @@ fn decode_surface( /// rounded up to the 4 KiB subresource alignment (`kTextureSubresourceAlignmentBytes`). fn tiled_face_stride(width: u32, height: u32, format: X360TextureFormat) -> usize { let bs = format.block_size() as u32; - let bw = ((width + bs - 1) / bs).max(1); - let bh = ((height + bs - 1) / bs).max(1); + let bw = width.div_ceil(bs).max(1); + let bh = height.div_ceil(bs).max(1); let pitch_aligned = align_up(bw, STORAGE_ALIGN_BLOCKS).max(MACRO_TILE_BLOCKS); let height_aligned = align_up(bh, STORAGE_ALIGN_BLOCKS).max(MACRO_TILE_BLOCKS); let surface = pitch_aligned as usize * height_aligned as usize * format.bytes_per_block(); @@ -682,7 +682,7 @@ fn tiled_face_stride(width: u32, height: u32, format: X360TextureFormat) -> usiz /// after the byte-level endian swap. Any trailing bytes that don't fill a full /// 8-byte group are left untouched. pub fn swap_bc_block_dwords(data: &mut [u8]) { - for unit in data.chunks_exact_mut(8) { + for unit in data.as_chunks_mut::<8>().0 { // [d0 d1 d2 d3 | d4 d5 d6 d7] → [d4 d5 d6 d7 | d0 d1 d2 d3] let (lo, hi) = unit.split_at_mut(4); lo.swap_with_slice(hi); @@ -742,8 +742,8 @@ pub fn detile( let bpb_log2 = (bpb as u32).trailing_zeros(); // Visible dimensions in blocks, and the padded storage pitch/height. - let blocks_wide = ((width + block_size - 1) / block_size).max(1); - let blocks_tall = ((height + block_size - 1) / block_size).max(1); + let blocks_wide = width.div_ceil(block_size).max(1); + let blocks_tall = height.div_ceil(block_size).max(1); let pitch_aligned = align_up(blocks_wide, STORAGE_ALIGN_BLOCKS).max(MACRO_TILE_BLOCKS); let height_aligned = align_up(blocks_tall, STORAGE_ALIGN_BLOCKS).max(MACRO_TILE_BLOCKS); diff --git a/crates/sylpheed-formats/src/ui_layout.rs b/crates/sylpheed-formats/src/ui_layout.rs index 1154a092..2a2ac43b 100644 --- a/crates/sylpheed-formats/src/ui_layout.rs +++ b/crates/sylpheed-formats/src/ui_layout.rs @@ -910,7 +910,7 @@ pub fn compose( // A dim backdrop stands in for the PRMD dim-quad + the live 3D scene behind // an in-mission screen. let mut canvas = vec![0u8; (w as usize) * (h as usize) * 4]; - for px in canvas.chunks_exact_mut(4) { + for px in canvas.as_chunks_mut::<4>().0 { px.copy_from_slice(&opts.backdrop); } let mut drawn = Vec::new(); diff --git a/crates/sylpheed-formats/src/vfs.rs b/crates/sylpheed-formats/src/vfs.rs index 28749f20..cd91c9c6 100644 --- a/crates/sylpheed-formats/src/vfs.rs +++ b/crates/sylpheed-formats/src/vfs.rs @@ -237,7 +237,9 @@ pub fn decode_text(bytes: &[u8]) -> (String, &'static str) { fn decode_utf16(bytes: &[u8], big_endian: bool) -> String { let units: Vec = bytes - .chunks_exact(2) + .as_chunks::<2>() + .0 + .iter() .map(|c| { if big_endian { u16::from_be_bytes([c[0], c[1]]) diff --git a/crates/sylpheed-formats/src/xiso.rs b/crates/sylpheed-formats/src/xiso.rs index 64bf0699..ea96b29f 100644 --- a/crates/sylpheed-formats/src/xiso.rs +++ b/crates/sylpheed-formats/src/xiso.rs @@ -53,7 +53,7 @@ impl XisoReader { info!( "Opened XISO: root directory table at sector {}", - { let s = volume.root_table.region.sector; s } + { volume.root_table.region.sector } ); Ok(Self { volume, file: wrapper }) } diff --git a/crates/sylpheed-viewer/src/asset_loader.rs b/crates/sylpheed-viewer/src/asset_loader.rs index fe7ce075..2c521da2 100644 --- a/crates/sylpheed-viewer/src/asset_loader.rs +++ b/crates/sylpheed-viewer/src/asset_loader.rs @@ -101,7 +101,7 @@ pub fn x360_texture_to_bevy_image(tex: X360Texture) -> Result { let opaque = matches!(tex.format, X360TextureFormat::X8R8G8B8); let mut out = tex.data; - for px in out.chunks_exact_mut(4) { + for px in out.as_chunks_mut::<4>().0 { let (a, r, g, b) = (px[0], px[1], px[2], px[3]); px[0] = r; px[1] = g; diff --git a/crates/sylpheed-viewer/src/iso_loader.rs b/crates/sylpheed-viewer/src/iso_loader.rs index 9755aa24..3fffdfa5 100644 --- a/crates/sylpheed-viewer/src/iso_loader.rs +++ b/crates/sylpheed-viewer/src/iso_loader.rs @@ -2035,6 +2035,10 @@ fn decode_audio_wav(video: &Path, wav: &Path) -> Result<(), String> { // ── Video decode + audio (main thread) ──────────────────────────────────────── +/// `(pts, rgba_bytes)` as the reader thread forwards them from `ffmpeg`. +#[cfg(not(target_arch = "wasm32"))] +type FrameRx = mpsc::Receiver<(f32, Vec)>; + /// Spawn an `ffmpeg` process decoding to raw RGBA on stdout, plus a reader /// thread that chunks it into frames and forwards `(pts, bytes)` over a bounded /// channel. Optional `-ss start` seeks the input; output pts re-base to 0, so we @@ -2046,7 +2050,7 @@ fn spawn_video_decoder( h: u32, fps: f32, start: f32, -) -> Result<(Child, mpsc::Receiver<(f32, Vec)>), String> { +) -> Result<(Child, FrameRx), String> { let mut cmd = Command::new("ffmpeg"); cmd.arg("-v").arg("error"); if start > 0.0 { @@ -2177,6 +2181,12 @@ fn scrub_worker( /// Polls the mpsc channel, updating `IsoState`, `FileBrowserState`, and /// `PendingFileBytes` as messages arrive. +// Bevy system: every parameter is a `Res`/`ResMut`/`EventWriter` the +// scheduler injects. The count is the framework's dependency list, not a +// signature anyone calls by hand, and it cannot be reduced without +// bundling into a `SystemParam` struct. clippy's general heuristic does +// not know about the idiom. +#[allow(clippy::too_many_arguments)] #[cfg(not(target_arch = "wasm32"))] fn poll_loader_channel( channels: Res, @@ -2557,7 +2567,7 @@ fn pick_albedo_index(model_name: &str, tex_names: &[String]) -> Option { #[cfg(not(target_arch = "wasm32"))] fn compute_smooth_normals(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32; 3]> { let mut acc = vec![Vec3::ZERO; positions.len()]; - for tri in indices.chunks_exact(3) { + for tri in indices.as_chunks::<3>().0 { let (a, b, c) = (tri[0] as usize, tri[1] as usize, tri[2] as usize); if a >= positions.len() || b >= positions.len() || c >= positions.len() { continue; @@ -3106,7 +3116,7 @@ fn prepare_models_impl( // Append the slice's triangles, re-emitting vertices per index // (no dedup) so each material buffer stays self-contained. A // reflected instance swaps two corners to keep front faces out. - for tri in sub.indices[(*off).min(end)..end].chunks_exact(3) { + for tri in sub.indices[(*off).min(end)..end].as_chunks::<3>().0 { let corners = if reflect { [tri[0], tri[2], tri[1]] } else { @@ -3405,6 +3415,12 @@ fn apply_prepared_xpr( /// Consumes a staged pack, freeing the previous texture/text previews and /// populating `PakView` for the master-detail browser. +// Bevy system: every parameter is a `Res`/`ResMut`/`EventWriter` the +// scheduler injects. The count is the framework's dependency list, not a +// signature anyone calls by hand, and it cannot be reduced without +// bundling into a `SystemParam` struct. clippy's general heuristic does +// not know about the idiom. +#[allow(clippy::too_many_arguments)] #[cfg(not(target_arch = "wasm32"))] fn apply_pak( mut pending: ResMut, @@ -4076,7 +4092,7 @@ fn build_game_snapshot(source: &SourceKind) -> Option { Some(CharRow { name, faction: c.faction.unwrap_or_default(), faces: c.faces.len() }) }) .collect(); - characters.sort_by(|a, b| (a.faction.clone(), a.name.clone()).cmp(&(b.faction.clone(), b.name.clone()))); + characters.sort_by_key(|a| (a.faction.clone(), a.name.clone())); // Combat rosters, keyed by stage where the table self-identifies. let rosters = gd::load_unit_rosters(&main); @@ -4742,7 +4758,7 @@ fn build_ship_model( *nrm = rot(&p.m, nrm); } if det < 0.0 { - for tri in sub.indices.chunks_exact_mut(3) { + for tri in sub.indices.as_chunks_mut::<3>().0 { tri.swap(1, 2); } } @@ -4911,7 +4927,7 @@ fn handle_cutscene_cues_request( // The naming convention is the obvious route and it is wrong often // enough to matter -- resolving the region is the only reading that // yields the right audio. - let voice = (|| { + let voice = { use sylpheed_formats::{hash::name_hash, media, PakArchive}; let vlang = match lang.pak_code() { "jpn" => sylpheed_formats::slb::VoiceLang::Japanese, @@ -4934,7 +4950,7 @@ fn handle_cutscene_cues_request( named_range, region, }) - })(); + }; let _ = sender.send(IsoLoaderMsg::CutsceneCuesLoaded { generation, diff --git a/crates/sylpheed-viewer/src/lib.rs b/crates/sylpheed-viewer/src/lib.rs index e91719b1..48607390 100644 --- a/crates/sylpheed-viewer/src/lib.rs +++ b/crates/sylpheed-viewer/src/lib.rs @@ -50,7 +50,7 @@ pub fn run() { DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "Project Sylpheed: Arc of Deception — Asset Viewer".into(), - resolution: (1280.0, 720.0).into(), + resolution: (1280.0_f32, 720.0_f32).into(), ..default() }), ..default() diff --git a/crates/sylpheed-viewer/src/ui.rs b/crates/sylpheed-viewer/src/ui.rs index f7bbec08..17f74919 100644 --- a/crates/sylpheed-viewer/src/ui.rs +++ b/crates/sylpheed-viewer/src/ui.rs @@ -157,6 +157,12 @@ struct UiEvents<'w> { cutscenes: EventWriter<'w, RequestCutscenes>, } +// Bevy system: every parameter is a `Res`/`ResMut`/`EventWriter` the +// scheduler injects. The count is the framework's dependency list, not a +// signature anyone calls by hand, and it cannot be reduced without +// bundling into a `SystemParam` struct. clippy's general heuristic does +// not know about the idiom. +#[allow(clippy::too_many_arguments)] fn draw_viewer_ui( mut contexts: EguiContexts, mut viewer: ResMut, @@ -656,7 +662,7 @@ fn draw_viewer_ui( ui.strong("Notes"); ui.end_row(); - let mut row = |ui: &mut egui::Ui, fmt: &str, c, status: &str, notes: &str| { + let row = |ui: &mut egui::Ui, fmt: &str, c, status: &str, notes: &str| { ui.label(fmt); ui.colored_label(c, status); ui.label(notes); @@ -1236,7 +1242,7 @@ fn draw_video_player( // Subtitle + voice controls (right-aligned): language, CC, and Voice. ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let before = subs.lang; - egui::ComboBox::from_id_source("subtitle_lang") + egui::ComboBox::from_id_salt("subtitle_lang") .selected_text(subs.lang.label()) .show_ui(ui, |ui| { for lang in SubLang::ALL { diff --git a/docs/agents/HANDOFF-2026-09-06.md b/docs/agents/HANDOFF-2026-09-06.md new file mode 100644 index 00000000..4a98ea61 --- /dev/null +++ b/docs/agents/HANDOFF-2026-09-06.md @@ -0,0 +1,397 @@ +# Handoff — 2026-09-06 + +**For a fresh session on a different machine.** Written to be read cold: it +assumes you know nothing about what happened, and it says what is *established* +versus what is *someone's claim*. + +--- + +## 1. What this project is + +Two things, and they are easy to confuse: + +* **The long game** — *Project Sylpheed: Arc of Deception — Reborn*, a clean-room + native port of an Xbox 360 game in Rust + Bevy. Values and behaviour come from + the original by **observation and static RE only** — never copied decompiled + code. The oracle is **the real game running in Xenia Canary**, never any + renderer of ours. +* **The work of 2026-09-04/06, which is what this document is about** — moving + the project's *working surface* onto a self-hosted Gitea, and getting CI to + produce an answer for the first time. + +If you only read one other file, read [`PROTOCOL.md`](PROTOCOL.md). + +## 2. The actors — four, and only two are constrained + +| | what it is | identity | constrained by the gate? | +|---|---|---|---| +| **the human** | directs everything; the only approver and merger | `fabi` | n/a — is the gate | +| **the Pi agent** | supervisor, runs on the Pi beside Gitea | `sylph-pi `, **no Gitea account** | ❌ has `gitea admin`; can mint tokens, edit rules | +| **you** (this session) | runs on the x86_64 desktop, holds the push credential | commits as the human ⚠️ | ❌ same carve-out, different mechanism | +| **the two loop agents** | Decoder (disc→meaning) and Port (disc→playable) | `sylph-decoder`, `sylph-port`, Write not Admin | ✅ | + +⚠️ **The looping agents are stopped and must stay stopped** until their images +carry the Gitea MCP and their issues are ready. Starting them early gives them a +brief telling them to read notifications and open issues with no tool that can. + +📌 The two *supervising* agents are the ones the gate does not bind. That is +written into `GITEA-SETUP.md` Phase 2 deliberately. Neither has a distinct Gitea +identity; both operate through the human's credential or an unlinked git author. +**That is a known, unresolved wart**, not an oversight. + +## 3. The machines + +| | | | +|---|---|---| +| **desktop** `fabi-Hyrican-PC` | x86_64, 12 core, 15 GB | agent containers, the repo clone, **the only push credential** | +| **the Pi** `raspberrypi.fritz.box` | aarch64, on the LAN | **runs Gitea** (published through a VPS), the CI runner, and the supervising agent | +| **Gitea** `git.mc02.dev` | 1.25.5 | resolves to a hosted address — that says nothing about the origin, which is the Pi | + +**File transfer between them is by hand.** The Pi agent has no push credential, +so its work arrives as `git bundle` over `scp`, which the human runs. This is the +weakest link in the setup: four round trips on 2026-09-04, each needing a +password twice, and once a guessed filename that was wrong. A `write:repository` +token on the Pi scoped to `pi/*` branches would remove it — **a deliberate +decision, deliberately not taken yet.** + +⚠️ **`CARGO_BUILD_JOBS=4`** and limited `-j`. A full-parallel build has +OOM-crashed the desktop. One emulator process at a time; Canary runs muted. + +## 4. Where things stand + +### Merged to `main` +Nothing since `59649824`. **`main` is protected** — `enable_push=false`, 1 +approval required, merge and approvals both whitelisted to `fabi` only. Verified +behaviourally: a real push was refused with `pre-receive hook declined`, as the +repository owner. + +### Open pull requests + +| | head | what | why not merged | +|---|---|---|---| +| **#10** | `agents/gitea-mcp` @ `a3d99ada` | the Gitea surface: MCP wiring, `gitea-protect`, the runbook, two PROTOCOL rules | waiting on the human. Merge-on-merits: docs and tooling, no `.rs` | +| **#14** | `fix/clippy-lints` @ `d8807c4f` | 73 clippy lints → 0 across four crates, `Closes #13` | same | + +🔴 **Both need the admin override to merge.** `fabi` authored them and is the +only whitelisted approver, and Gitea bars self-approval — so they can never reach +one approval. `block_admin_merge_override` is `false` *deliberately* so that door +stays open. Do not tick it. + +### Open issues + +Nine work items, all `state/approved`, awaiting the loop agents: + +``` +#1 F1 measure (decoder) ←blocks— #2 F1 implement (port) +#3 F2 disc gain (decoder) ←blocks— #4 F2 playback gains (port) +#5 F3 title cue (decoder) +#6 OPTIONS re-propose (port) +#8 F5/F6 findings (decoder) ←blocks— #7 F5/F6 port work (port) +#9 f6-out-of-sample residue (decoder) +``` + +Four infrastructure issues: + +* **#11** `state/proposed` — WASM. **Work exists past its shape; see §6.** +* **#12** `state/proposed` — rustfmt: 774 hunks, deliberately deferred. +* **#13** `state/approved` — clippy; `#14` closes it. +* **#15** `state/proposed` — the lint gate floats `@stable`. + +### Unmerged branches + +``` +agents/gitea-mcp 11 → PR #10 +fix/clippy-lints 16 → PR #14 +auto/frame-blend-draw-path 495 the Decoder's corpus — returns via #8 +auto/port-p6-audio 366 the Port's work — returns via #7 +``` + +The last two are the reason `#7` depends on `#8`: `port/scripts/boot.gd` cites +`docs/re/` pages that exist on **neither** its own branch nor `main`. +`tools/port/check-citations --for-merge` counts **19** such citations. + +## 5. CI — it produced its first answer on 2026-09-05 + +Before that: **23 runs cancelled, 2 waiting, zero successes.** The workflow +described GitHub's hosted fleet (`windows-latest`, `macos-latest`, and a +`--target x86_64` cross-compile) on a one-runner aarch64 instance, so the run +never reached a terminal state — the checks were *unfinished*, not red. + +Now, three jobs, all terminal: + +| job | state | | +|---|---|---| +| **Native — linux** | **green**, three runs running | `check` `build` `test` `clippy` all pass on aarch64; **207 passed, 0 failed, 14 ignored, 30 suites** | +| WASM — Web | red | **#11** | +| Formatting | red | **#12** — 774 hunks, identical on `main` | + +The workspace being portable to ARM was unknown before this and is now +established. The disk exhaustion that broke the test link is retired: 46 GB +reclaimed, `/` at 55%. + +⚠️ **`/var/lib/docker` is still on the Pi's 117 GB SD card while a 916 GB SSD sits +at 16%.** This will recur. The fix is `data-root` in `daemon.json` plus a Docker +restart, and it wants a moment when every container going down is fine. + +## 6. 🔴 What is in flight and NOT pushed + +**The WASM work.** The Pi agent has `ba6c5da`, bundle at +`/tmp/sylph-wasm-compile.bundle` **on the Pi**, base `d8807c4`. It makes +`Check WASM compile` exit 0. #11 turned out to be **three stacked blockers**, +each invisible until the previous was gone: + +1. `getrandom` needs `--cfg getrandom_backend="wasm_js"` **and** the feature — + its own error says either alone is insufficient; +2. `sylpheed-formats` declared `tokio` as a **normal** dependency it never used, + dragging `tokio/full` → `net` → `mio`, which does not build for wasm32; +3. `bevy_egui` needs `--cfg web_sys_unstable_apis`. + +**Two things the human must decide before it lands:** + +* **#11 is `state/proposed` and this goes past its stated shape.** It was written + around the getrandom error alone. The tokio removal is a change to another + crate, not CI config — look at that one specifically. +* **It will not turn the job green.** It unblocks two steps that have never run: + `jetli/trunk-action@v0.5.0` and `trunk build --release`. The action's bundled + `dist/index.js` names `x86_64-unknown-linux-gnu` once and `aarch64` **never**, + so it will fetch an x86_64 binary onto the aarch64 runner. Upstream *does* + publish `trunk-aarch64-unknown-linux-gnu.tar.gz` — so this is "the action + cannot find it", not "trunk is unavailable on arm". Replacing the install step + means picking a version and a method: a decision, not a slip-in. + +## 7. The method lessons — the most transferable part + +One failure shape recurred **five times in two days**, across two agents and the +human's assistant. Every instance is: **a property inferred from something +adjacent to it, rather than tested directly.** + +| what was inferred | from what | how it failed | +|---|---|---| +| `main` is protected | the settings page | merging ignores the push whitelist; both agents could have approved each other | +| the desktop can't reach Gitea | `curl` being refused | that was a *permission prompt*, not the network | +| the tool creates 12 labels | `grep -c '^mklabel'` | one match was the function **definition** | +| Gitea is not on the Pi | a DNS lookup | it is, published through a VPS | +| CI's clippy == mine | **identical rustfmt output** | rustfmt is output-stable by design; clippy moves lints between groups | + +The last one is the sharpest. `rustfmt 1.8.0` and `1.9.0`, nine months apart, +both produce **774** hunks on this tree — so formatting parity carries *no* +information about which clippy ran. From it I concluded CI's green must be cached +or ungated ("the frozen splash again"). It was neither: `collapsible_else_if` is +`warn` on 1.92.0 and **`allow`** on 1.98.1, which is what the runner has. + +> **The check that worked, every time, was counting the same thing with the same +> tool against a baseline.** + +Three rules now in `PROTOCOL.md`, each earned: + +* **A finding reaches `main` before the code that cites it.** +* **A check may only soften against a condition it can test** — *can this branch + tell the difference between "not yet" and "no longer"?* +* **If you are writing the softening in the same commit as the check, the thing + you want is an issue, not a flag.** + +And the ordering constraint that is not obvious: **the cheap-looking fix for #12 +is the expensive one.** A whole-tree reformat before #7 and #8 return would put a +conflict in every file of 861 commits and make the reviews those items exist to +enable unreadable. Measured: 133 files carrying 81% of the debt cannot collide; +the real blocker is **21 files**. Land #7/#8 first, then sweep once. + +## 8. Resuming — concrete first steps + +```bash +git fetch origin +git switch fix/clippy-lints # PR #14's head, d8807c4f +python3 tools/gitea-protect --verify # expect: protection holds. rc=0 +``` + +Then, in order of what is actually blocking: + +1. **Merge #10 and #14** — human, with the admin override. Everything else is + downstream: the loop agents clone `main`, which has none of this. +2. **Decide #11's scope**, then fetch `/tmp/sylph-wasm-compile.bundle` from the + Pi and push it. +3. **Decide the trunk-action replacement** (§6). +4. **Then Phase 7** of [`GITEA-SETUP.md`](GITEA-SETUP.md): start the **decoder + alone**, watch one full iteration — notifications polled, a PR rather than a + bare push, no merge button — before starting the port. + +### Credentials, all on the desktop, all `chmod 600` + +``` +~/.sylph-git-credentials write:repository — the push credential +~/.sylph-gitea-token-decoder the decoder agent's, four scopes +~/.sylph-gitea-token-port the port agent's, four scopes +~/.sylph-claude-token long-lived Claude auth for the containers +``` + +`~/.sylph-gitea-api-token` (`write:issue`) lives **on the Pi**, because that is +where `tools/gitea-setup` runs. Never paste any of them into chat. + +⚠️ **This desktop was updated to `rustc 1.98.1` on 2026-09-06** to match the +runner. Anything verified here *before* that ran on 1.92.0 — the fmt counts (774) +and test counts (207/0/14) matched CI exactly and therefore carry; a clippy +result from before does not. + +## 9. Standing constraints + +* **Never commit game content**, under any directory name. 545 MB reached a + branch on 2026-09-04 under a name the ignore list did not happen to mention. + `.gitignore` now describes the *shape* (`/export*/`, media by extension). +* **Agents never merge**, never push to `main`, never rewrite history. One + force-push was authorised, once, to fix commit authorship before merge — a + recorded exception, not a precedent. +* **The Explorer shows static data only** — the ISO, the embedded PE, savegames. + Never anything generated by a Sylpheed run. +* **Never self-screenshot**; ask the human to capture. Ask before a + watch-and-verify `--ui` launch. +* **Never judge emulator crash or stability from a Bash-launched run.** + +## 10. The second machine — measured on `fabi-MS-7C37`, 2026-09-06 + +**§§1–9 were written on `fabi-Hyrican-PC`.** This section was written on the +other desktop, and every line is a command run *here*. Nothing above is carried +across untested — §7 is the reason. + +Everything §§4–5 say about the **server** checks out exactly: 13 open issues, +#10 and #14 open and `mergeable`, Gitea 1.25.5. What does not transfer is the +**box**. + +### What is different here, and what it costs + +| § | says | here | +|---|---|---| +| 8 | four `~/.sylph-*` credentials, `chmod 600` | **none exist.** `~/.git-credentials` holds a `fabi@git.mc02.dev` token that reads `branch_protections` — an endpoint both agent tokens are refused on — so it is not an agent token. Whether it can *push* is untested | +| 8.2 | fetch `/tmp/sylph-wasm-compile.bundle` from the Pi | **`raspberrypi.fritz.box` does not resolve here**, and there is no host key for it. `ba6c5da` is unreachable from this machine | +| 8 | "updated to `rustc 1.98.1` … to match the runner" | that was the *other* desktop. Here `stable` = **1.90.0**, with a `1.92.0` also installed | +| 5 | the agent images | no `sylph-decoder` / `sylph-port` image on this box | + +So of §8's four steps, only **1** (the human's merges) and **3** (the +trunk-action decision, an edit to `ci.yml`) can be done from here. **2 and 4 +cannot, at all.** + +### 🔴 `git clone` of this repository does not work + +Three attempts died on `GnuTLS recv error (-9)` / `Recv failure: Connection +reset by peer`, at 135 MB, 4.6 MB and 73 MB. A default clone fetches **every** +branch, and `auto/port-p6-audio`'s history still carries the 545 MB of game +content §9 describes — gone from the tree, still in the pack. + +What works, in seconds: + +```bash +git -c http.version=HTTP/1.1 clone --filter=blob:none \ + https://git.mc02.dev/fabi/Sylpheed.git +``` + +Blobs fault in on demand. The tree is **108 MB in 1 007 files** (98 MB of it +`docs/re/captures/`), and it was verified byte-for-byte against +`git ls-tree -r -l` — worth doing, because the clone printed *"checkout failed"* +partway and then recovered silently. ⚠️ One cost: `check-citations` reaches into +peer branches, so on a blobless clone it faults blobs over that same flaky link +and takes minutes rather than seconds. + +### The numbers that carry, and the one that does not + +Expected values taken from §§4–5 *before* running, per R2: + +| | expected | measured here | | +|---|---|---|---| +| `gitea-protect --verify` | protection holds | **holds**, 10/10, rc=0 | ✅ carries | +| `cargo fmt --all -- --check` | 774 hunks | **774**, across **154 files** | ✅ carries | +| `check-citations --for-merge` | 19 | **19**, rc=1 | ✅ carries | +| `cargo test --workspace` | 207 / 0 / 14, 30 suites | **207 / 0 / 14, 30** | ✅ carries — see below | +| `cargo clippy --workspace -- -D warnings` | *not comparable* | **exit 101** | 🔴 diverges | + +154 files corroborates rather than adds: §7's split of the fmt debt into "133 +that cannot collide" and "21 that are the real blocker" sums to exactly it. + +`gitea-protect --verify` needs `SYLPH_GIT_CREDENTIALS=~/.git-credentials` here, +since its default is one of the four missing files. + +### 🔴 The test count carries — and that is what is wrong with it + +It matched to the unit. It should not be read as the two runs having done the +same thing, because they did not. + +**15 files** under `crates/sylpheed-formats/tests/*_disc.rs` resolve the disc +through a `disc_root()` whose second branch is a **hardcoded absolute path** — +`/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, +Europe) (En,Ja)`. So `unset SYLPHEED_DISC` does **not** disable them: that +directory exists on this box, and the disc suites *ran*. + +| | CI (job 794) | here | +|---|---|---| +| test **execution** wall time | **2.4 s**, slowest suite 0.29 s | **1 936 s**, `mesh_consistency_disc` alone **1 220 s** | +| tally | 207 / 0 / 14, 30 suites | **identical** | + +Identical because the skip path is `eprintln!("SKIP: …")` **plus an early return +from a test that still passes**. A skipped disc test and a fully exercised one +both score `1 passed`. And the message is invisible either way — `cargo test` +captures a passing test's stderr, so *neither* log contains a `SKIP:` line. The +absence of one proves nothing; only the clock separated these two runs. + +So ask the question this project keeps having to ask — **what would this check +still report if the corpus were entirely absent?** — and the answer is +`207 / 0 / 14`. + +Two consequences, and the first is good news: + +* this run is **strictly stronger evidence than CI's**: 207 passed with the disc + corpus actually exercised, on x86_64, at `593b378`. +* `SYLPHEED_DISC` looks like the control and is not one. Whether the disc suites + run is a property of *the machine's directory layout*, invisible in the command + and in the output. Worth an issue — it is the `.gitignore` lesson again, + **naming an instance instead of the condition.** + +📌 **A sixth instance for §7, and it is mine.** I inferred "the counts cannot +match" from "the fallback resolves" — an adjacent property, never tested — and +wrote it into this section before the run finished. The run returned 207 / 0 / 14. +The correction was the same as every other time in that table: run it, and count. + +### 🔴 The clippy gate genuinely disagrees between the two toolchains + +This is **#15 ceasing to be theoretical**, and it needs stating carefully, +because it is §7's lesson 5 arriving from the other side. + +Both sides measured with the same command, both versions read rather than assumed: + +* **the runner** — `rustc 1.98.1 (48a229cea 2026-09-01)`, read out of job 794's + own log. `cargo clippy --workspace -- -D warnings` finishes in 9.26 s with no + lint. Native is green on runs **206, 207, 208 and 209** — four consecutive, + not three. +* **here** — `rustc 1.90.0` / `clippy 0.1.90`. The same command exits **101**, on + exactly one lint: + +``` +error: parameter is only used in recursion + --> crates/sylpheed-formats/src/vfs.rs:85:10 + = note: `-D clippy::only-used-in-recursion` implied by `-D warnings` +``` + +Without `-D warnings` it is a warning and clippy exits 0 — so the disagreement +sits exactly at the gate. + +**What this does not mean.** It does not mean #14 is wrong, and it does not mean +CI's green is cached or ungated. That is the inference §7 records as the sharpest +of its five failures, and the evidence points the other way: the runner's log +shows the step running, on this code, clean. `d8807c4` already collapsed one +`else { if }` *"so both toolchains agree"* — this is the same class, one lint on. + +What it establishes is narrower and more useful: **the gate's verdict depends on +which stable happened to be current**, and there is now a named reproducible case +rather than an argument. That is #15's evidence. + +📌 For whoever picks up #13/#14: *"clippy is clean"* is not a property of the +tree, it is a property of the tree **and** a toolchain. Until #15 pins one, say +which one you ran. + +### Refutation attempted, and survived + +Per the adversarial duty — §6's claim that `sylpheed-formats` declares `tokio` as +a normal dependency **it never uses**. It *is* referenced, in `ship.rs` and +`xiso.rs`, which looked like a refutation. It is not: every one of those sits +inside a `#[cfg(test)]` module (`ship.rs:497`, `xiso.rs:178`). The library's +non-test code does not use tokio, so moving it to `dev-dependencies` is sound and +the `examples/` targets keep compiling. **Claim survives** — recorded because a +survived challenge is stronger than an unchallenged one, not because it changed +anything. diff --git a/docs/agents/PROTOCOL.md b/docs/agents/PROTOCOL.md index 8f812bdf..54786235 100644 --- a/docs/agents/PROTOCOL.md +++ b/docs/agents/PROTOCOL.md @@ -167,7 +167,7 @@ neither its own branch nor `main`. ## Checks that were kind once -Two rules that look unrelated and are the same failure. +Three instances now, and they are the same failure. **A check may only soften against a condition it can test.** @@ -189,6 +189,45 @@ a collaborator exists, so the "yet" was never needed. stays. That is why they survive review, and why the smell is worth naming: *leniency with an expiry date nobody set.* +### The third instance was authored dirty, not decayed into + +The two above were **correct when written**. The third was not, and it is worth +separating because it arrives by a different route and is caught at a different +moment. + +CI's `Clippy` step turned out never to have run — the toolchain shipped without +the component, so `cargo clippy -- -D warnings` died on *"not installed"* on +every commit in the repo's history. Fixing that is two lines. But the tree is +not clippy-clean: the build already emits ~13 rustc warnings that `-D warnings` +promotes to errors. So the fix and the first red result arrive together, and the +first draft paired the two-line fix with `continue-on-error: true` and a comment +saying *delete this line once the debt is paid* — which is precisely an expiry +date nobody set. It was reverted within the hour, on reading #12's own closing +line ruling the same shape out for rustfmt. + +The difference that matters: + +| | first two | third | +|---|---|---| +| when it was wrong | became wrong later | wrong on the first commit | +| what caused it | the world moved | the tree was already dirty | +| what catches it | auditing old allowances | noticing the impulse at the keyboard | + +**This is the default way a check gets written when the tree is not clean yet.** +Not a rare slip — the ordinary shape of the first draft. Whenever a real check +goes in against a tree that does not yet pass it, the softening is *right there*, +it looks like pragmatism, and it comes with a sincere comment promising removal. +The mechanical test still catches it after the fact. The earlier tell is this: + +> **If you are writing the softening in the same commit as the check, the thing +> you want is an issue, not a flag.** + +A red check that measures something is worth more than a green one that measures +nothing, and it is worth strictly more than a green one that *used to* measure +something. Land the check gating, let it be red, and scope the debt where it can +be read, argued with and closed — #12 for rustfmt, #13 for clippy. An issue has +the expiry date the flag never gets. + `share put --note "…" --for port` records the sender, the time, **the commit they were on**, and whether their tree was dirty. A capture with no provenance is not evidence, it is a picture.