Skip to content

Commit 8f40161

Browse files
committed
fix: type accuracy, confidence docs, and published-artifact verification (#1 #2 #4)
1 parent 81db750 commit 8f40161

6 files changed

Lines changed: 497 additions & 71 deletions

File tree

.github/workflows/release.yml

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,15 +434,111 @@ jobs:
434434
done
435435
npm publish "$tarball" --access public --provenance --tag "$publish_tag"
436436
437+
# The release job verifies a locally packed tarball. That cannot catch a
438+
# package that reaches the registry incomplete, which is exactly how 2.3.0
439+
# shipped without dist/ and wasm/ while every pre-publish gate passed. Install
440+
# the published version from the public registry in a clean directory and
441+
# exercise it the way a consumer would.
442+
verify-published:
443+
name: Verify the published npm package
444+
needs:
445+
- plan
446+
- publish-npm
447+
if: ${{ always() && needs.publish-npm.result == 'success' }}
448+
runs-on: ubuntu-latest
449+
permissions:
450+
contents: read
451+
steps:
452+
- uses: actions/setup-node@v6
453+
with:
454+
node-version: 24
455+
registry-url: https://registry.npmjs.org
456+
- name: Verify the registry artifact and run it as a consumer
457+
shell: bash
458+
env:
459+
RELEASE_TAG: ${{ needs.plan.outputs.tag }}
460+
run: |
461+
version="${RELEASE_TAG#v}"
462+
package="@deckflow/deckprobe@${version}"
463+
464+
# npm publishes are not immediately readable. Retry, then fail loudly.
465+
for attempt in $(seq 1 15); do
466+
if npm view "$package" version >/dev/null 2>&1; then
467+
echo "$package is readable from the registry"
468+
break
469+
fi
470+
if [[ "$attempt" == 15 ]]; then
471+
echo "$package never became readable from the registry" >&2
472+
exit 1
473+
fi
474+
echo "attempt $attempt: not readable yet, waiting"
475+
sleep 20
476+
done
477+
478+
# The declared entry points must actually be inside the published tarball.
479+
npm pack --dry-run --json "$package" > packed.json
480+
node -e '
481+
const packed = require("./packed.json")[0];
482+
const files = new Set(packed.files.map((file) => file.path));
483+
const required = [
484+
"dist/index.js",
485+
"dist/index.node.js",
486+
"dist/worker.js",
487+
"wasm/deckprobe_wasm.js",
488+
"wasm/deckprobe_wasm_bg.wasm",
489+
];
490+
const missing = required.filter((path) => !files.has(path));
491+
if (missing.length) {
492+
console.error(`registry tarball is missing: ${missing.join(", ")}`);
493+
process.exit(1);
494+
}
495+
console.log(`registry tarball carries all ${required.length} declared entry points (${packed.entryCount} files)`);
496+
'
497+
498+
# Install into a clean project, the way a consumer would.
499+
mkdir -p "$RUNNER_TEMP/consumer"
500+
cd "$RUNNER_TEMP/consumer"
501+
npm init -y >/dev/null
502+
npm install "$package" >/dev/null
503+
504+
# The Node entry point, its WASM, and the discovery contract.
505+
cat > check.mjs <<'CHECK'
506+
import assert from "node:assert/strict";
507+
import { writeFileSync } from "node:fs";
508+
import { formats, probeFile, version } from "@deckflow/deckprobe";
509+
510+
const expected = process.argv[2];
511+
assert.equal(await version(), expected, "runtime version disagrees with the published version");
512+
513+
const drivers = (await formats()).formats.map((entry) => entry.driver);
514+
assert.ok(drivers.includes("pdf"), "discovery did not report the pdf driver");
515+
516+
// A minimal valid PDF, so the check needs no fixture from the repository.
517+
writeFileSync("probe.pdf", "%PDF-1.7\n%%EOF\n");
518+
const report = await probeFile("probe.pdf", { targets: ["@header"] });
519+
assert.equal(report.schema_version, 2);
520+
assert.equal(report.input.source_kind, "local_file");
521+
assert.equal(report.driver.id, "pdf");
522+
console.log("node entry point, WASM, discovery, and probeFile all work from the registry copy");
523+
CHECK
524+
node check.mjs "$version"
525+
526+
# The CLI resolves through its optional platform package.
527+
npx --no-install deckprobe --version
528+
echo "published $package verified from the public registry"
529+
437530
announce:
438531
needs:
439532
- plan
440533
- host
441534
- publish-npm
535+
- verify-published
442536
# use "always() && ..." to allow us to wait for all publish jobs while
443537
# still allowing individual publish jobs to skip themselves (for prereleases).
444538
# "host" however must run to completion, no skipping allowed!
445-
if: ${{ always() && needs.host.result == 'success' && needs.publish-npm.result == 'success' }}
539+
# verify-published gates the announcement so a release cannot report success
540+
# while the artifact on the public registry is incomplete.
541+
if: ${{ always() && needs.host.result == 'success' && needs.publish-npm.result == 'success' && needs.verify-published.result == 'success' }}
446542
runs-on: "ubuntu-22.04"
447543
env:
448544
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,14 @@ Schema version 2 uses one JSON envelope on standard output. Top-level `status` i
461461
- the executed `path` and evidence `source`;
462462
- deterministic physical-byte, expanded-byte, and random-read counters for the whole probe.
463463

464+
`confidence` records how strong the evidence for a value is — `exact` when it was read from the
465+
authoritative structure, `high` when it came from a statistic the authoring application saved,
466+
`medium` when it was inferred from a proxy. The paired `confidence_score` is a **fixed constant per
467+
label** (`0.4`, `0.7`, `0.95`, `1.0`), not a calibrated probability: `0.95` does not mean the value
468+
is right 95% of the time. Likewise `partial` only means some requested target was unresolved — the
469+
other results are still valid, and it is not a verdict on the document's health. See
470+
[Reading confidence and partial results](docs/CLI-REFERENCE.md#reading-confidence-and-partial-results).
471+
464472
Wall-clock `elapsed_ms` is omitted by default so identical inputs and options produce byte-identical JSON. Add `--telemetry` when timing is needed.
465473

466474
Use `--strict` when unresolved targets should make the command exit non-zero.

docs/CLI-REFERENCE.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,86 @@ deckprobe -t slide_count,orientation \
239239

240240
An explicitly named target can be valid for the detected format but unavailable at the chosen level or confidence. If some other requested target can be planned, the report records the unavailable target under `execution.unresolved_targets`. If no requested target has an eligible path, DeckProbe exits with status `1` as an unsupported-target request.
241241

242+
## Reading confidence and partial results
243+
244+
### What the confidence labels mean
245+
246+
`confidence` describes how strong the evidence for one value is, as judged by the path that
247+
produced it. It is not a measured accuracy rate.
248+
249+
| Label | Score | What normally backs it |
250+
| --- | --- | --- |
251+
| `exact` | `1.0` | Read directly from the authoritative structure in the container |
252+
| `high` | `0.95` | A statistic the authoring application saved, such as the slide count in `docProps/app.xml`. Authoritative unless that application left it stale |
253+
| `medium` | `0.7` | Inferred from a proxy, such as counting `xl/worksheets/sheet*.xml` parts instead of reading the workbook's declared sheets |
254+
| `low` | `0.4` | Weak or indirect evidence |
255+
| `none` | `0.0` | Accompanies a result that carries no value |
256+
257+
**`confidence_score` is a fixed constant per label, not a calibrated probability.** `0.95` does not
258+
mean the value is correct 95% of the time on real-world files; no corpus measurement backs these
259+
numbers. Use them to order or threshold results, never to report an accuracy figure to a user.
260+
261+
### Report status versus target status
262+
263+
The report's own `status` is `ok` or `partial`. A result's `status` is one of eight values, and the
264+
two answer different questions.
265+
266+
`partial` means at least one requested target could not be resolved at the requested confidence. It
267+
says nothing about whether the document is damaged or unsafe — the remaining results are still
268+
valid.
269+
270+
```bash
271+
deckprobe -t slide_count,author --pretty deck.pptx
272+
```
273+
274+
```jsonc
275+
{
276+
"status": "partial", // because author could not be resolved
277+
"results": {
278+
"powerpoint.slide_count": {
279+
"status": "resolved", "value": 31,
280+
"confidence": "high", "path": "powerpoint.app_statistics",
281+
"source": "docProps/app.xml saved statistic"
282+
},
283+
"document.author": {
284+
"status": "unknown", // the path ran; the file records no author
285+
"confidence": "none"
286+
}
287+
},
288+
"execution": { "unresolved_targets": ["document.author"] }
289+
}
290+
```
291+
292+
The slide count here is perfectly good. Treating `partial` as a failure would discard it.
293+
294+
Contrast that with a structural target the format cannot answer at all:
295+
296+
```bash
297+
deckprobe -l d -t corrupted report.pdf # exits 1
298+
```
299+
300+
`corrupted` and `missing_assets` are declared for every modern format, but only the iWork drivers
301+
implement a path for them. Naming one on a PDF or OOXML file is an unsupported-target request, so
302+
DeckProbe exits `1` rather than returning a report. Use the `@quality` selector to get whatever the
303+
active driver actually supports.
304+
305+
| Result `status` | Carries `value` | Meaning |
306+
| --- | --- | --- |
307+
| `resolved` | yes | Obtained at or above the requested confidence |
308+
| `estimated` | yes | Obtained, but an estimate |
309+
| `unknown` | no | The path ran; the document does not record this fact. A normal answer, not an error |
310+
| `unsupported` | no | This format has no path for the target |
311+
| `invalid` | no | The document records something that fails validation |
312+
| `budget_exceeded` | no | A limit stopped this target specifically |
313+
| `failed` | no | The path errored |
314+
| `planned` | no | `--plan-only` only |
315+
316+
Distinguish `"value": null` on a `resolved` result — the field exists and is empty, which is an
317+
answer — from `status: "unknown"`, where the probe could not answer.
318+
319+
Use `--strict` when an unresolved target must fail the command; it exits `5` and still writes the
320+
full report.
321+
242322
## Input interpretation and format options
243323

244324
DeckProbe uses the normalized filename extension to select a format path, then verifies its signature and internal type. Renaming a PPTX to DOCX, for example, returns `MALFORMED_INPUT`. `-f`/`--input-format` adds another assertion; it does not force an unrelated parser onto the file:

0 commit comments

Comments
 (0)