DC-001: report unresolved slide relationships as damage

This commit is contained in:
Vincent Cloutier committed 2026-11-07 16:12:41 -05:00
1 parent fc35ad67c6
commit 26c1ac83dc
5 files changed
+71 -3

No files matched your search

+5
View File
@@ -1,5 +1,9 @@
# DeckCraft roadmap
> **2026-11-07 update:** missing slide relationships are reported as lost slides,
> and the partial deck opens as a repaired copy. Synthetic 13-slide coverage added;
> the exact cause of the field report #1910 remains unconfirmed. Scores unchanged.
**Stage: alpha** · next: beta, ~20 points and ~120–210 h away
> **Last reviewed:** 2026-11-04 · **Last updated:** 2026-11-04 · **Change:** trivial (Ogg Opus recovery includes trailing packets without granules in its reported duration; parity scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
@@ -154,6 +158,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| Date | What landed |
|---|---|
| 2026-11-07 | PPTX slide-list entries with missing relationships are reported in lostSlides instead of silently omitted. The engine opens the partial deck as a repaired copy with no original save path. Synthetic 13-slide regression; does not establish the cause of #1910's field report. |
| 2026-11-04 | Ogg Opus recovery keeps trailing packets without granules in the total duration, matching their recovered positions; valid end trimming stays covered |
| 2026-11-03 | Web: the `?host=parent` bridge (as in GridCraft, WordCraft and PdfCraft) lets a same-origin page that frames DeckCraft send a presentation and store its saves; Save writes an opened .pptx back as .pptx under its own name, as on the desktop |
| 2026-11-02 | Slide show on Linux and the BSDs: Presenter View stays off when winit lists exactly one display, as in PowerPoint; if it lists none, the setting is kept. On one display the presenter window covered the show, and the hidden window stalled every frame: slide changes took up to ~1.7 s under Hyprland (#1156) |
+12 -3
View File
@@ -474,7 +474,7 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec<String>), PptxE
}
let pres_part = Part::new(&pkg, &pres_path, &pres_doc);
let (master_paths, slide_entries) = master_and_slide_paths(pres_el, &pres_part);
let (master_paths, slide_entries) = master_and_slide_paths(pres_el, &pres_part, &mut lost_slides);
let (docs, max_id) = load_part_docs(&pkg, &master_paths, &slide_entries);
let mut imp = Imp {
@@ -527,7 +527,7 @@ pub fn import_checked(bytes: &[u8]) -> Result<(Presentation, Vec<String>), PptxE
/// Masters, layouts and slides to read, in file order and without duplicates: (master paths,
/// (file slide id, slide path)).
fn master_and_slide_paths(pres_el: &El, pres_part: &Part) -> (Vec<String>, Vec<(u32, String)>) {
fn master_and_slide_paths(pres_el: &El, pres_part: &Part, lost_slides: &mut Vec<String>) -> (Vec<String>, Vec<(u32, String)>) {
let master_paths: Vec<String> = pres_el
.child("sldMasterIdLst")
.map(|l| l.children_named("sldMasterId").filter_map(|m| pres_part.rels.target(m.attr("r:id").unwrap_or(""))).map(String::from).collect())
@@ -539,7 +539,16 @@ fn master_and_slide_paths(pres_el: &El, pres_part: &Part) -> (Vec<String>, Vec<(
.map(|l| {
l.children_named("sldId")
.take(10_000)
.filter_map(|s| Some((s.u32("id").unwrap_or(0), pres_part.rels.target(s.attr("r:id").unwrap_or(""))?.to_string())))
.filter_map(|s| {
let rid = s.attr("r:id").unwrap_or("");
let Some(path) = pres_part.rels.target(rid) else {
// A missing relationship still represents a lost slide. Report it so
// the engine opens a repaired copy instead of overwriting the original.
lost_slides.push(format!("slide {} (missing relationship {rid})", s.u32("id").unwrap_or(0)));
return None;
};
Some((s.u32("id").unwrap_or(0), path.to_string()))
})
.collect()
})
.unwrap_or_default();
+36
View File
@@ -288,6 +288,42 @@ fn unreadable_slide_is_reported() {
assert_eq!(deckcraft_pptx::import(&broken).expect("import").slides.len(), 1);
}
/// Missing slide relationships must not turn a damaged deck into a clean partial load (#1910).
#[test]
fn missing_slide_relationships_are_reported() {
let mut p = defaults::new_presentation(None);
let layout = p.slides[0].layout;
for _ in 1..13 {
let slide = defaults::new_slide(&mut p, layout).expect("slide");
p.slides.push(std::sync::Arc::new(slide));
}
let mut es = entries(&deckcraft_pptx::export(&p).expect("export"));
let pres = es.iter_mut().find(|(n, _)| n == "ppt/presentation.xml").expect("presentation");
let xml = String::from_utf8(pres.1.clone()).expect("utf8");
let doc = deckcraft_pptx::parse_xml(xml.as_bytes()).expect("xml");
let slides = doc.root.child("sldIdLst").expect("slide list");
let mut broken = xml.clone();
for slide in slides.children_named("sldId").skip(6) {
let rid = slide.attr("r:id").expect("relationship id");
broken = broken.replace(&format!("r:id=\"{rid}\""), &format!("r:id=\"missing-{rid}\""));
}
pres.1 = broken.into_bytes();
let damaged = rezip(&es);
let (q, lost) = deckcraft_pptx::import_checked(&damaged).expect("import");
assert_eq!(q.slides.len(), 6);
assert_eq!(lost.len(), 7, "every omitted slide must be reported: {lost:?}");
let loaded = deckcraft_engine::cmd::file::loaded_file("Original.pptx", &damaged).expect("load");
let mut session = deckcraft_engine::Session::new();
let token = session.stage_loaded(loaded);
let result = session.execute("file.adopt", &serde_json::json!({"path": "Original.pptx", "token": token})).expect("adopt");
assert_eq!(result["lostSlides"].as_array().expect("lost slides").len(), 7);
assert!(session.doc().expect("doc").path.is_none(), "Save must not overwrite the damaged source");
assert!(session.doc().expect("doc").title.ends_with("(Repaired)"));
let (q, lost) = deckcraft_pptx::import_checked(&deckcraft_pptx::export(&p).expect("export")).expect("import");
assert_eq!(q.slides.len(), 13);
assert!(lost.is_empty());
}
/// Many slides naming one notes part: it is read for the first slide only, not copied onto every
/// slide that names it.
#[test]
+13
View File
@@ -1,5 +1,15 @@
# File-format parity with PowerPoint
> **2026-11-07 update:** missing slide relationships participate in damage reporting
> and repaired-copy handling. Fidelity estimates unchanged.
A listed slide whose relationship is missing is included in `lostSlides`, as an
unreadable slide part already was. A synthetic 13-slide test verifies that all
seven omitted slides are reported and that the engine opens a repaired copy
without the original save path. This prevents a normal Save from overwriting the
damaged source with the partial deck. It does not recover missing slide contents
or establish the cause of the field report #1910.
> **Last reviewed:** 2026-11-02 · **Last updated:** 2026-11-03 · **Change:** minor (cell anchorCtr, vertOverflow/horzOverflow, compatLnSpc, normalizeH, thaiDist, table rtl, run altLang and reflection transforms round-trip; Reuse Slides brings referenced custom table styles; previous: minor (native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: seven owned PPTX corpus files, embedded fonts, EMF/WMF clip regions, threaded comments (#70), chart trendlines and workbooks (#335), SmartArt written as a diagram, tags and custom XML kept, picture export DPI; duplicated rows and headers folded; previous: trivial, lines in scaled groups, #233)) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
Every format PowerPoint reads or writes, DeckCraft's support, fidelity and how it is tested.
@@ -117,6 +127,9 @@ the round trip even though DeckCraft doesn't model it.
## Revision history
- 2026-11-07: Report missing slide relationships and protect the damaged source;
no fidelity score change.
| Date | Change | Summary |
|---|---|---|
| 2026-11-02 | minor | Optional table RTL direction is read and written as `a:tblPr/@rtl`; native and two PPTX saves preserve true, false and absent values with distinguishable column text |
+5
View File
@@ -1,5 +1,10 @@
# Where DeckCraft falls short of PowerPoint
Missing slide relationships now produce a lost-slide report and an untitled
repaired copy, covered by a synthetic 13-slide regression. This narrows silent
partial-load risk; unexplained slide loss in #1910 remains unconfirmed, and lost
contents are not reconstructed.
> **Last reviewed:** 2026-10-30 · **Last updated:** 2026-11-04 · **Change:** trivial (workflow-breaking regression fixed: Ctrl+C/X/V use the system clipboard again; #1414 to re-test on Windows; previous: trivial, run alternate language and reflection transforms survive saves; reused slide contents scale to the destination, picker thumbnails remain a gap; previous: trivial (locked No Fill and no-outline click-through regression coverage added; No Fill hit-testing follows resolved fill instead of the presence of a style (#1621); scores unchanged)) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
Every known shortfall, one entry each, ranked by how much it stops someone who presents with