Skip to content

Commit 2e5d064

Browse files
committed
release: v2.7.0 batch Word triage
1 parent 699146e commit 2e5d064

19 files changed

Lines changed: 450 additions & 178 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
All notable changes to DeckProbe are documented here.
44

5+
## [Unreleased]
6+
7+
## [2.7.0] - 2026-09-22
8+
9+
### Added
10+
11+
- JSONL error envelopes now echo the input record's original `path` and optional
12+
caller-provided `id` for reliable batch attribution.
13+
- Word deep probes can estimate missing word and character statistics from
14+
`word/document.xml` when those targets explicitly accept medium confidence.
15+
16+
### Changed
17+
18+
- Keep the npm package lightweight by omitting source maps and TypeScript source
19+
files from published tarballs, with a packaging regression check.
20+
- Report absent optional Word saved statistics as resolved `null` values with a
21+
`MISSING_OPTIONAL_STATISTIC` diagnostic; page counts remain unestimated.
22+
523
## [2.6.0] - 2026-09-09
624

725
### Added
@@ -13,13 +31,6 @@ All notable changes to DeckProbe are documented here.
1331
- Exact `powerpoint.smartart_data_part_count` inventory using existing Content Types/ZIP indexes.
1432
- Native/WASM parity, input cap, cancellation/deadline and semantic SmartArt regression checks.
1533

16-
## [Unreleased]
17-
18-
### Changed
19-
20-
- Keep the npm package lightweight by omitting source maps and TypeScript source
21-
files from published tarballs, with a packaging regression check.
22-
2334
## [2.5.0] - 2026-09-03
2435

2536
### Added

‎Cargo.lock‎

Lines changed: 12 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ members = [
1515
resolver = "2"
1616

1717
[workspace.package]
18-
version = "2.6.0"
18+
version = "2.7.0"
1919
edition = "2024"
2020
license = "Apache-2.0"
2121
repository = "https://github.com/deckflow/deckprobe"

‎README.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,13 @@ compact schema-v2 result for each non-empty input line:
190190

191191
```sh
192192
printf '%s\n' \
193-
'{"path":"report.pdf"}' \
194-
'{"path":"deck.pptx"}' | deckprobe --jsonl -t @summary
193+
'{"id":"report","path":"report.pdf"}' \
194+
'{"id":"deck","path":"deck.pptx"}' | deckprobe --jsonl -t @summary
195195
```
196196

197+
When a valid JSONL record fails, the error envelope echoes its optional `id`
198+
and original `path` under `input`.
199+
197200
## Use from a coding agent
198201

199202
DeckProbe ships an [Agent Skill](https://agentskills.io) that teaches an agent the
@@ -462,7 +465,7 @@ path; request those explicitly or through `@structure`.
462465
| Driver | Profiles | Current inspection depth |
463466
|---|---|---|
464467
| PDF | `.pdf` | Header and Info metadata, page/object counts, xref type, signatures, links, attachments, JavaScript, forms, annotations, and XMP presence. |
465-
| Word | `.docx`, `.docm`, `.dotx`, `.dotm` | OPC metadata, saved statistics, exact paragraph/table structure, security signals, comments, and image assets. |
468+
| Word | `.docx`, `.docm`, `.dotx`, `.dotm` | OPC metadata, saved statistics, opt-in medium-confidence deep text estimates, exact paragraph/table structure, security signals, comments, and image assets. |
466469
| Excel | `.xlsx`, `.xlsm`, `.xltx`, `.xltm`, `.xlsb` | OPC metadata, worksheets/names/visibility, shared strings, tables, charts, pivots, security signals, and image assets; XLSB identity only. |
467470
| PowerPoint | `.pptx`, `.pptm`, `.ppsx`, `.ppsm`, `.potx`, `.potm` | OPC metadata, slides/hidden slides, masters/layouts/notes, slide size, security signals, charts, comments, images, and media. |
468471
| Legacy Office | `.doc`, `.dot`, `.xls`, `.xlt`, `.ppt`, `.pps`, `.pot` | Validated CFB main streams, SummaryInformation metadata, macros, embedded-object signals, and core Word/Excel/PowerPoint statistics. |

‎crates/deckprobe-cli/src/main.rs‎

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ struct Cli {
5757
long,
5858
conflicts_with_all = ["input", "pretty", "stdin_name"],
5959
help_heading = "Input interpretation",
60-
long_help = "Read JSONL records from stdin. Each non-empty line is either a JSON string containing a local path, an object {\"path\":\"...\"}, or an object {\"name\":\"report.pdf\",\"data_base64\":\"...\"}. Output is always compact JSONL and processing continues after a per-record error."
60+
long_help = "Read JSONL records from stdin. Each non-empty line is either a JSON string containing a local path, an object {\"path\":\"...\"}, or an object {\"name\":\"report.pdf\",\"data_base64\":\"...\"}. Object records may include a caller-defined \"id\", which is echoed with the original path on errors. Output is always compact JSONL and processing continues after a per-record error."
6161
)]
6262
jsonl: bool,
6363

@@ -561,13 +561,39 @@ fn read_stdin_document(options: &ProbeOptions) -> deckprobe_core::Result<Vec<u8>
561561
enum JsonlInput {
562562
Path(String),
563563
Record {
564+
id: Option<serde_json::Value>,
564565
path: Option<PathBuf>,
565566
name: Option<String>,
566567
#[serde(alias = "base64")]
567568
data_base64: Option<String>,
568569
},
569570
}
570571

572+
#[derive(Debug, Default, Serialize)]
573+
struct JsonlInputContext {
574+
#[serde(skip_serializing_if = "Option::is_none")]
575+
id: Option<serde_json::Value>,
576+
#[serde(skip_serializing_if = "Option::is_none")]
577+
path: Option<String>,
578+
}
579+
580+
impl JsonlInput {
581+
fn context(&self) -> JsonlInputContext {
582+
match self {
583+
Self::Path(path) => JsonlInputContext {
584+
id: None,
585+
path: Some(path.clone()),
586+
},
587+
Self::Record { id, path, .. } => JsonlInputContext {
588+
id: id.clone(),
589+
path: path
590+
.as_ref()
591+
.map(|value| value.to_string_lossy().into_owned()),
592+
},
593+
}
594+
}
595+
}
596+
571597
fn run_jsonl(cli: &Cli, options: &ProbeOptions) -> deckprobe_core::Result<u8> {
572598
let stdin = std::io::stdin();
573599
let mut exit_code = 0;
@@ -577,12 +603,11 @@ fn run_jsonl(cli: &Cli, options: &ProbeOptions) -> deckprobe_core::Result<u8> {
577603
if line.trim().is_empty() {
578604
continue;
579605
}
580-
let outcome = serde_json::from_str::<JsonlInput>(&line)
581-
.map_err(|error| {
582-
DeckProbeError::InvalidRequest(format!(
583-
"JSONL line {line_number} is invalid: {error}"
584-
))
585-
})
606+
let parsed = serde_json::from_str::<JsonlInput>(&line).map_err(|error| {
607+
DeckProbeError::InvalidRequest(format!("JSONL line {line_number} is invalid: {error}"))
608+
});
609+
let context = parsed.as_ref().ok().map(JsonlInput::context);
610+
let outcome = parsed
586611
.and_then(|input| jsonl_source(input, options, cli.max_input_bytes))
587612
.and_then(|source| probe(source, options.clone()));
588613
match outcome {
@@ -601,7 +626,17 @@ fn run_jsonl(cli: &Cli, options: &ProbeOptions) -> deckprobe_core::Result<u8> {
601626
}
602627
Err(error) => {
603628
exit_code = exit_code.max(error_exit_code(&error));
604-
write_json(&error_report(&error), false)?;
629+
let mut report = error_report(&error);
630+
if let Some(context) =
631+
context.filter(|value| value.id.is_some() || value.path.is_some())
632+
{
633+
report["input"] = serde_json::to_value(context).map_err(|error| {
634+
DeckProbeError::Parser(format!(
635+
"cannot serialize JSONL input context: {error}"
636+
))
637+
})?;
638+
}
639+
write_json(&report, false)?;
605640
}
606641
}
607642
}
@@ -620,6 +655,7 @@ fn jsonl_source(
620655
max_input_bytes,
621656
)?)),
622657
JsonlInput::Record {
658+
id: _,
623659
path: Some(path),
624660
name,
625661
data_base64: None,
@@ -629,6 +665,7 @@ fn jsonl_source(
629665
max_input_bytes,
630666
)?)),
631667
JsonlInput::Record {
668+
id: _,
632669
path: None,
633670
name: Some(name),
634671
data_base64: Some(data),

‎crates/deckprobe-engine/src/lib.rs‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,27 @@ fn merge_results(
779779
message: format!("no path satisfies {target} at requested level/confidence"),
780780
})
781781
.collect::<Vec<_>>();
782+
diagnostics.extend(
783+
results
784+
.iter()
785+
.filter(|(_, result)| {
786+
result.path == "word.app_statistics"
787+
&& result.status == TargetStatus::Resolved
788+
&& result.value.as_ref().is_some_and(Value::is_null)
789+
})
790+
.map(|(target, _)| Diagnostic {
791+
level: "info".to_owned(),
792+
code: "MISSING_OPTIONAL_STATISTIC".to_owned(),
793+
message: if target == "word.page_count" {
794+
"word.page_count is not recorded in docProps/app.xml; exact pagination requires a Word-compatible layout engine"
795+
.to_owned()
796+
} else {
797+
format!(
798+
"{target} is not recorded in docProps/app.xml; the document is still structurally valid"
799+
)
800+
},
801+
}),
802+
);
782803
if !plan_only {
783804
for (target, result) in results
784805
.iter()
@@ -803,6 +824,16 @@ fn merge_results(
803824
)
804825
.to_ascii_lowercase(),
805826
});
827+
} else if result.status == TargetStatus::Invalid
828+
&& result.path == "word.app_statistics"
829+
{
830+
diagnostics.push(Diagnostic {
831+
level: "warning".to_owned(),
832+
code: "INVALID_OPTIONAL_STATISTIC".to_owned(),
833+
message: format!(
834+
"{target} is present in docProps/app.xml but is not a valid non-negative integer"
835+
),
836+
});
806837
} else {
807838
diagnostics.push(Diagnostic {
808839
level: "warning".to_owned(),

‎crates/deckprobe-format-word/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ rust-version.workspace = true
99
[dependencies]
1010
deckprobe-core = { path = "../deckprobe-core" }
1111
deckprobe-format-ooxml = { path = "../deckprobe-format-ooxml" }
12+
quick-xml.workspace = true
1213
serde_json.workspace = true
13-

0 commit comments

Comments
 (0)