Skip to content

Commit b59d3dd

Browse files
committed
🔖 Bump version to 0.6.4
Three commits since v0.6.3 on this branch, no public API change. Container-gated OPF checks, encoding-aware XML decoding, and three CSS path defects are the user-visible fixes. SchematronValidator and XMLParser were deleted -- both unreachable in every commit since they were written -- dropping fontoxpath, slimdom and slimdom-sax-parser and leaving css-tree, fflate and libxml2-wasm as the runtime dependencies. The parity rig moves out of session scratchpads and into scripts/parity/, so every figure in PROJECT_STATUS is now reproducible with npm run parity. docs/md is regenerated with npm run docs:md, which had drifted well past this release: the copies under _media/ still carried the pre-91b7d25 PROJECT_STATUS and the Schematron claims corrected in 3e7c564, and five API entities added since the last regen had no page at all (parseCustomMessages, ResolvedEpubCheckOptions, ValidationMode, EPUB_VERSIONS, and the ValidationContext fields hasContainer and xmlParseFailures). docs/html stays out -- it is gitignored. Verified before tagging: typecheck, lint and format clean; 1415 tests pass, 14 skipped; packaging suite green against a fresh build; npm run parity:check reports 0 regressed, 0 improved across 763 fixtures. The stale suite total in PROJECT_STATUS (1435) is corrected to the measured 1429.
1 parent 4cc10c3 commit b59d3dd

35 files changed

Lines changed: 823 additions & 515 deletions

CHANGELOG.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77
## [Unreleased]
88

9+
## [0.6.4] - 2026-08-07
10+
11+
### Added
12+
13+
- **The parity measurement rig is now part of the repository** (`scripts/parity/`, `npm run parity`). Every parity figure in `PROJECT_STATUS.md` was previously produced by scripts living in throwaway scratchpads — the numbers could be read but not reproduced or contradicted. The corpus definition, the Java invocation, and the comparison rules are now versioned, and results are cached by content hash so a full 763-fixture run against EPUBCheck 5.3.0 completes in seconds. `test/parity/baseline.json` records the last measured state; `npm run parity:check` fails CI on an unrecorded regression, and `npm run parity:update` re-records it deliberately.
14+
915
### Fixed
1016

1117
- **Package document checks that need an OCF container no longer run in single-file mode.** Java splits `OPFChecker.check()` in two: with a container it runs `checkPackage()`, without one only `checkContent()` (`OPFChecker.java:90-100`, guarded by `Preconditions.checkState(context.container.isPresent())`). Four checks living on the `checkPackage()` side were running unconditionally here, each reporting a target it had no container to resolve against: `RSC-007w` on `link` hrefs (12 fixtures), `RSC-026` container leaks (4), `OPF-030` (3), and `OPF-031`/`OPF-032` on the guide. `ValidationContext` now carries `hasContainer`, mirroring `context.container.isPresent()`. The neighbouring checks that Java reaches from `checkContent()` are deliberately left alone — `OPF-067` shares a loop with `RSC-007w` but comes from `checkLinkedResources()` (`OPFChecker30.java:81`), and the `RSC-017` guide check comes from `opf.sch`; gating either would have traded a false positive for a new gap. Over Java's standalone fixtures, `--mode opf` exact agreement rose 90.8% → 93.7% and the total 93.5% → 94.6%. Packaged-corpus results are unchanged, as intended. Verdict agreement dips 98.6% → 97.8% on `--mode opf`: four fixtures agreed only because the spurious `RSC-026` happened to make them invalid, and removing it exposes an unimplemented `PKG-009` (see Known Issues).
1218
- **XML documents are decoded by encoding, and a parse failure is now fatal.** Two compounding defects: every XML byte stream was decoded with `new TextDecoder()` (UTF-8), so a UTF-16 package document became mojibake; and `parseOPF` is regex-based and cannot fail, so that mojibake silently produced an empty `PackageDocument` defaulting to version 3.0. One bad byte sequence therefore surfaced as a cascade of unrelated structural errors instead of a single fatal. Java sniffs the encoding for *reporting only* (`RSC-027`/`RSC-028`) and hands raw bytes to SAX, letting the parser detect the encoding itself (`XMLParser.java:141-165`); a `SAXException` becomes a fatal `RSC-016` with the version left unresolved. This port now does the same — `decodeXmlBytes` decodes per the BOM (including UTF-32, which `TextDecoder` does not support), the RelaxNG pass parses from bytes rather than re-encoded text, and `checkXmlWellFormed` gates the regex parser. Where the failure occurs before any markup is read, validation stops with `OPF-001`, matching Java's unresolved version. Across the 758-fixture corpus this moved verdict agreement 95.3% → 95.5% and error/warning ID agreement 85.5% → 86.1%; `--mode opf` exact agreement over Java's standalone fixtures rose 89.4% → 90.8%.
1319
- **`sniffXmlEncoding` now matches Java's magic-table order.** Java tests its UTF-16 table before its UCS-4 table, so a UTF-32LE BOM (`FF FE 00 00`) matches on its two-byte prefix and is reported as UTF-16. This port deliberately checked UCS-4 first, which is arguably more correct but disagrees with the oracle.
1420
- **The version peek on the container path decoded as UTF-8**, leaving the same mojibake bug alive for the `OCFChecker`-equivalent version resolution in `src/ocf/container.ts` even after the package-document path was fixed.
21+
- **The parity harness exit code is set before the `--json` return.** `cli.ts` documents its contract as "0 clean, 1 a real result (regression, crash), 2 you invoked it wrong", but `corpus --json` and `standalone --json` returned early, before the assignment, and so always exited 0 — including when every fixture crashed. The rows carried the failures either way, so a reader of the JSON was never misled; a wrapper gating on the exit code was. `npm run parity:check` has no `--json` path, which is why CI never caught it.
22+
- **Manifest hrefs are lifted before matching CSS font references.** `checkFontType` resolved an `@font-face` `url()` to a container-relative path and compared it against `item.href`, which is relative to the package document. The two agree only when the OPF sits at the container root, so for essentially every real EPUB the lookup never matched and the second `CSS-007` — the one reporting the media type the *manifest* declares — could not fire. Every other manifest comparison in the codebase already lifts the href via `resolveManifestHref(opfDir, item.href)`; this call site was the one that did not. Both halves are now canonicalized the same way — `resolveManifestHref` decodes percent-encoding and normalizes to NFC, so the CSS side does too, via the `resolvePath(base, tryDecodeUriComponent(href)).normalize('NFC')` form used in `ncx/`, `skm/` and `opf/`. Parity is unchanged across 763 fixtures: no fixture combines a subdirectory OPF with a `@font-face` pointing at a non-blessed manifest media type, nor percent-encodes a font url, which is why the corpus cannot see this in either direction and the cover is unit tests.
23+
- **Relative-path resolution inside CSS no longer leaves `./`, `../` or empty segments unnormalized.** `CSSValidator` carried its own private path resolver, near-identical to the shared one but disagreeing on three inputs: a base file at the container root left `./font.ttf` and `../fonts/f.ttf` unnormalized, and a doubled slash (`a//b.ttf`) kept its empty segment. Any of those leaks into `manifest.find(item => item.href === resolved)` and silently skips `CSS-007`. The shared `resolvePath` (now in `src/util/path.ts`) runs the segment walk unconditionally and drops empty segments, subsuming both resolvers rather than picking one. No fixture reaches these inputs, so parity could not have caught it; `src/util/path.test.ts` pins the contract instead.
1524

1625
### Changed
1726

1827
- **Fixture placeholder images are now real images.** `build-fixtures.sh` wrote a 22-byte JPEG (SOI + APP0 + EOI, no SOF segment) and a bare 8-byte PNG signature as stand-ins for images the Java source fixtures don't ship. Neither carries dimensions, so `ImageIO` cannot decode them and EPUBCheck reports `PKG-021` on every fixture containing one — 20 of them, 9 flipping the verdict, all of which read as coverage gaps in this port. They were an artifact of the scaffolding, not a validator defect: rebuilding `map-valid.epub` with a real 1×1 image makes EPUBCheck 5.3.0 report nothing. The stubs now live in `test/fixtures/assets/` behind a `create_stub_image` helper, and `test/fixtures/repair-stub-images.sh` fixed the 22 EPUBs built before the change, updating entries in place so the stored-first `mimetype` and every other entry stay byte-identical. Packaged-corpus agreement: verdict 95.5% → 96.7%, error/warning ID set 86.1% → 88.3%, exact 65.8% → 67.2%. The GIF and WebP stubs were already complete images and needed no change.
1928
- **The ported EPUB 2 single-document `OPF-030` expectation was corrected.** Java's scenario has the assertion commented out with `# FIXME this error should be detected and reported in single-document mode` (`opf-package-document.feature:80-83`); the port transcribed the commented line as a live expectation. The packaged scenario (`opf-publication.feature:52`) keeps `OPF-030` and still passes.
2029
- **`test/fixtures/invalid/content/xml-encoding-utf32-BOM-error.epub` repaired.** Its package document had been re-encoded to UTF-32LE-with-BOM when the fixture was packaged; Java's source fixture is UTF-32BE-without-BOM. The two are different tests — the LE-with-BOM form trips the sniffer-order quirk above and yields `RSC-027` plus a fatal, where Java's own feature file specifies `RSC-028` alone. Both engines now agree with that expectation on the repaired bytes.
30+
- **Container-path helpers are shared rather than pasted.** `dirname` appeared 28 times as an inline `p.includes('/') ? p.substring(0, p.lastIndexOf('/')) : ''`, and `basename` in two more spellings; `resolvePath` and `tryDecodeUriComponent` lived in `src/opf/validator.ts` and were imported by content, ncx and skm, inverting the layering. They now live in `src/util/path.ts` and `src/references/url.ts`. No Node APIs are used — container paths are always POSIX and relative to the container root, and the browser build cannot import `node:path`.
31+
- **Each namespace has one name, and invariant allocations are hoisted.** `src/content/validator.ts` named the same namespace three ways: module constants, 32 function-local prefix-map aliases, and 93 inline object literals. Two `Set`s were also rebuilt per call — one per `<script>` element, one per image. `CORE_IMAGE_MEDIA_TYPES` now holds the five core types with `IMAGE_MEDIA_TYPES` derived from it, keeping the `OPF-051` tolerance for `image/jpg` visible rather than buried in a copy.
32+
- **The `@id` node set is walked once per document.** Four checks each ran their own `.//*[@id]` descendant walk over identical results — three on every XHTML document, two on every SVG. The walk now happens once and the list is passed to the checks that need it. Measured end to end over three real EPUBs this is 1–2%, at or near noise; it is kept for the deduplication rather than the speed.
33+
34+
### Removed
35+
36+
- **`SchematronValidator` and `XMLParser`/`XMLWalker` deleted, dropping three runtime dependencies.** Neither module was ever imported outside its own test file, in any commit — `SchematronValidator` could not have worked regardless, since `fontoxpath` does not implement the XPath 2.0 functions (`matches`, `tokenize`) the EPUB schemas are built on. The rules it was meant to evaluate ship as hand-ported TypeScript in `src/content/validator.ts` and `src/opf/validator.ts`, and always did. Removing it drops `fontoxpath`, `slimdom` and `slimdom-sax-parser` (plus `saxes`, `prsc` and `xspattern` transitively) — six packages no shipped code path could reach, already tree-shaken out of `dist/`, so the cost was borne entirely at install time by every consumer. **Runtime dependencies are now `css-tree`, `fflate` and `libxml2-wasm`.** The four `.sch` files went with them (11.3 KB gzipped into the bundle serving no reader); they are byte-identical to `../epubcheck/src/main/resources/com/adobe/epubcheck/schema/30/`, which `CONTRIBUTING.md` already requires as a sibling checkout. Parity over all 763 fixtures is unchanged: 0 regressed, 0 improved.
2137

2238
## [0.6.3] - 2026-08-06
2339

@@ -700,7 +716,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
700716
- No media overlays validation
701717
- No script detection/validation
702718

703-
[Unreleased]: https://github.com/likecoin/epubcheck-ts/compare/v0.6.3...HEAD
719+
[Unreleased]: https://github.com/likecoin/epubcheck-ts/compare/v0.6.4...HEAD
720+
[0.6.4]: https://github.com/likecoin/epubcheck-ts/compare/v0.6.3...v0.6.4
704721
[0.6.3]: https://github.com/likecoin/epubcheck-ts/compare/v0.6.2...v0.6.3
705722
[0.6.2]: https://github.com/likecoin/epubcheck-ts/compare/v0.6.1...v0.6.2
706723
[0.6.1]: https://github.com/likecoin/epubcheck-ts/compare/v0.6.0...v0.6.1

PROJECT_STATUS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,9 @@ The standalone corpus is every `.opf`/`.xhtml`/`.svg` under `../epubcheck/src/te
9898

9999
| Category | Tests | Passed | Skipped |
100100
|----------|-------|--------|---------|
101-
| Unit Tests | 488 | 486 | 2 |
101+
| Unit Tests | 482 | 480 | 2 |
102102
| Integration Tests | 947 | 935 | 12 |
103-
| **Total** | **1435** | **1421** | **14** |
103+
| **Total** | **1429** | **1415** | **14** |
104104

105105
Unit tests include 17 for the parity harness itself (`test/unit/parity.test.ts`) — cache keying, the ID-set vs ID-count distinction, and the location metric. The harness gates CI, so a silent bug there would not make a check wrong; it would make every check unverifiable while still printing a confident percentage.
106106

bin/epubcheck.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import type {
2222
// Dynamic import to support both ESM and CJS builds
2323
const { EpubCheck, EPUB_VERSIONS, MessageId, toJSONReport } = await import('../dist/index.js');
2424

25-
const VERSION = '0.6.3';
25+
const VERSION = '0.6.4';
2626
const VALID_MODES: ReadonlySet<ValidationMode> = new Set([
2727
'exp',
2828
'opf',

0 commit comments

Comments
 (0)