# Phase 8: 分页质量、缓存与性能采样 - Research
**Researched:** 2026-05-23
**Domain:** iOS/Swift EPUB reader - pagination caching, quality improvement, performance sampling
**Confidence:** HIGH
## Summary
Phase 8 builds on Phase 6 (page geometry) and Phase 7 (custom attribute closure) to address three concerns: pagination caching, pagination quality for complex image-heavy chapters, and performance diagnostics.
**Critical finding:** Several capabilities assumed to be "not yet integrated" in CONTEXT.md and ARCHITECTURE-CONTEXT.md are actually already implemented:
- `RDEPUBTextBookBuilder.build()` already calls `content.rd_paginatedFrames(size:)` (not `ss_pageRanges`). The integration is DONE.
- CSS `` inlining is already implemented via `inlineLinkedStyleSheets()` in `RDEPUBTextRendererSupport`.
- The 5-layer CSS cascade is already implemented via `makeStyleSheetLayers()` producing `RDEPUBTextStyleSheetPackage` layers.
- `RDEPUBTextPage.metadata` is already populated with `RDEPUBTextPageMetadata` from `frame.metadata`.
**What actually needs building:**
1. **Pagination cache** (QUAL-01): Serialize `RDEPUBTextBook` to disk, keyed by layout parameters
2. **Image/attachment rules** (QUAL-03): Formalize sizing, dark mode, and page-break-around-image rules
3. **Performance sampling** (QUAL-04): Instrument render/paginate/build timings
4. **Pagination quality** (QUAL-02): Implement `avoidPageBreakInside` line-removal, orphan/widow control, image fit enforcement
**Primary recommendation:** Focus Phase 8 on cache serialization (the largest new code), performance instrumentation (straightforward), and incremental pagination quality improvements. Do NOT re-implement CSS `` inlining or 5-layer cascade -- they already work.
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Pagination cache storage | API / Backend (file system) | -- | Cache is local disk persistence, no UI involvement |
| Cache key generation | API / Backend | -- | Pure computation from layout parameters |
| Performance timing | API / Backend | -- | CFAbsoluteTimeGetCurrent at build/render points |
| Image sizing rules | API / Backend (renderer) | Browser/Client (display) | Rules applied during DTCoreText rendering |
| avoidPageBreakInside enforcement | API / Backend (layouter) | -- | CoreText line-level calculation |
| Orphan/widow control | API / Backend (layouter) | -- | CoreText line-level calculation |
| Pagination diagnostic output | API / Backend | Browser/Client (logging) | Diagnostic data flows to console/log |
## User Constraints (from CONTEXT.md)
### Locked Decisions
- **D-01 [P0]:** Integrate `RDEPUBTextLayouter` into `RDEPUBTextBookBuilder` -- ALREADY DONE (see finding above)
- **D-02 [P0]:** Expose `metadata: RDEPUBTextPageMetadata` on `RDEPUBTextPage` -- ALREADY DONE
- **D-03 [P0]:** Pagination cache by `bookID + fontSize + lineHeightMultiple + contentInsets`, cache full `RDEPUBTextBook` to disk
- **D-04 [P0]:** CSS `` inline processing -- ALREADY DONE
- **D-05 [P0]:** 5-layer CSS cascade -- ALREADY DONE
- **D-06 [P1]:** Image/attachment processing rules with diagnostics
- **D-07 [P1]:** Performance sampling -- output stable sampling data
### Claude's Discretion
- Cache storage format (file system plist/JSON vs SQLite) -- implementer decides
- CSS cascade replace.css content -- reference WXRead but don't copy exactly
- Performance sampling threshold values -- determine at implementation time
### Deferred Ideas (OUT OF SCOPE)
- CoreText direct drawing migration (v1.2/v2.0)
- Font system (embedded font set, future version)
- Character-level position precision (P2)
- Chapter data model cohesion (P2)
- TTS / DRM / Pencil / multi-column layout (P3)
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| QUAL-01 | Cache mechanism for layout frames/pagination results to avoid redundant full-layout | Cache key design from WXRead `WRChapterPageCount.currentCacheKeyWithBookId:`, serialization strategy for `RDEPUBTextBook` |
| QUAL-02 | Improve pagination quality for complex image-heavy chapters -- reduce bad breaks, orphan/widow lines, image whitespace issues, code/table truncation | WXRead `avoidPageBreakInsideByRemovingLastLinesIfNeeded` pattern, `WRCoreTextLayoutConfig` widow/orphan settings |
| QUAL-03 | Image/attachment sizing rules, dark mode adaptation, page background info with verifiable rules | WXRead `wr-vertical-center-style`, `DTPageBreakInsideAvoid`, existing `prepareHTMLElementForReaderRendering` |
| QUAL-04 | No significant degradation of first-screen time, repagination time, or interaction fluidity; output stable diagnostic/sampling data | Performance sampling points at render/paginate/build, `CFAbsoluteTimeGetCurrent` pattern |
## Standard Stack
### Core (no new external dependencies)
This phase uses only existing project dependencies and Apple frameworks:
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| Foundation | iOS SDK | NSKeyedArchiver, JSONEncoder, FileManager | System framework, no alternative |
| CoreText | iOS SDK | CTFramesetter, CTFrame line inspection | Already used by RDEPUBTextLayouter |
| DTCoreText | (existing) | HTML-to-NSAttributedString | Already integrated |
| CryptoKit / CommonCrypto | iOS SDK | SHA256 for cache key hashing | System framework |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| os.signpost | iOS 15+ | Performance instrumentation with Instruments integration | If structured performance logging desired |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| NSKeyedArchiver for cache | JSONEncoder | JSON is human-readable but can't handle NSAttributedString natively; NSKeyedArchiver handles it but produces opaque binary |
| File system cache | SQLite | File system is simpler for whole-object caching; SQLite adds complexity for no gain when caching complete objects |
| SHA256 cache key | Simple string concatenation | SHA256 produces fixed-length keys safe for filenames; raw string could exceed filesystem limits |
**Installation:** No new packages needed.
## Package Legitimacy Audit
No external packages are installed in this phase. All dependencies are existing project dependencies or Apple system frameworks.
| Package | Registry | Age | Downloads | Source Repo | slopcheck | Disposition |
|---------|----------|-----|-----------|-------------|-----------|-------------|
| (none) | -- | -- | -- | -- | -- | N/A |
## Architecture Patterns
### Current Data Flow (already working)
```
RDEPUBReaderController.paginate(restoreLocation:)
-> RDEPUBTextBookBuilder.build(parser:publication:pageSize:style:)
-> for each spine item:
-> RDEPUBTextRendererSupport.makeChapterRenderRequest(...)
-> inlineLinkedStyleSheets() [CSS inlining - DONE]
-> makeStyleSheetLayers() [5-layer cascade - DONE]
-> injectPaginationSemanticMarkers() [semantic markers - DONE]
-> renderer.renderChapter(request:) [DTCoreText render]
-> content.rd_paginatedFrames(size:) [4-level semantic pagination - DONE]
-> RDEPUBTextPage with metadata [metadata exposure - DONE]
-> RDEPUBTextBook(chapters:, pages:)
-> applyTextBook(restoreLocation:)
```
### Recommended Cache Integration Point
```
RDEPUBReaderController.paginate(restoreLocation:)
-> compute cacheKey from bookID + layout params
-> if cached RDEPUBTextBook exists for key:
-> applyTextBook(cachedBook, restoreLocation:) [CACHE HIT - skip build]
-> else:
-> builder.build(...) [existing flow]
-> save RDEPUBTextBook to cache [CACHE WRITE]
-> applyTextBook(textBook, restoreLocation:)
```
### Recommended Project Structure
```
Sources/RDReaderView/EPUBTextRendering/
├── RDEPUBTextBookBuilder.swift # existing - add cache integration
├── RDEPUBTextBookCache.swift # NEW - cache key, read/write, eviction
├── RDEPUBTextLayouter.swift # existing - add avoidPageBreakInside enforcement
├── RDEPUBTextLayoutFrame.swift # existing - no changes needed
├── RDEPUBTextPaginationSupport.swift # existing - no changes needed
├── RDEPUBTextRenderer.swift # existing - no changes needed
├── RDEPUBTextRendererSupport.swift # existing - add image rule formalization
├── RDEPUBDTCoreTextRenderer.swift # existing - no changes needed
├── RDEPUBTextPerformanceSampler.swift # NEW - timing instrumentation
└── RDPlainTextBookBuilder.swift # existing - no changes needed
```
### Pattern 1: Cache Key Generation
**What:** Generate a deterministic cache key from layout parameters that change pagination results.
**When to use:** Before every `builder.build()` call.
**Example:**
```swift
// Reference: WXRead WRChapterPageCount.currentCacheKeyWithBookId:
// WXRead encodes: bookId + fontSize + lineSpacing + pageWidth + pageHeight
struct RDEPUBTextBookCacheKey {
let bookID: String
let fontSize: CGFloat
let lineHeightMultiple: CGFloat
let contentInsets: UIEdgeInsets
let pageSize: CGSize
var stringValue: String {
let raw = "\(bookID)|\(fontSize)|\(lineHeightMultiple)|\(contentInsets.top)|\(contentInsets.left)|\(contentInsets.bottom)|\(contentInsets.right)|\(pageSize.width)|\(pageSize.height)"
// SHA256 for safe filename
let data = Data(raw.utf8)
let hash = SHA256.hash(data: data)
return hash.map { String(format: "%02x", $0) }.joined()
}
}
```
**Source:** [VERIFIED: Doc/WXRead/decompiled/WRChapterPageCount.m lines 64-90]
### Pattern 2: avoidPageBreakInside Line Removal
**What:** When a block marked `avoidPageBreakInside` would cross a page boundary, remove trailing lines from the current page to push the entire block to the next page.
**When to use:** During `RDEPUBTextLayouter.adjustedRange()` after the 4-level semantic break detection.
**Example:**
```swift
// Reference: WXRead WRCoreTextLayoutFrame.avoidPageBreakInsideByRemovingLastLinesIfNeeded
// If the proposed page end falls inside an avoidPageBreakInside block:
// 1. Find the block's start location
// 2. If block start >= minimumEnd, break at block start
// 3. If block start < minimumEnd (block is too large), fall through to frameLimit
```
**Source:** [VERIFIED: Doc/WXRead/decompiled/WRCoreTextLayoutFrame.h line 280]
### Pattern 3: Performance Sampling
**What:** Measure wall-clock time for render, paginate, and full-build operations.
**When to use:** At the entry/exit of `builder.build()`, `renderer.renderChapter()`, and `content.rd_paginatedFrames()`.
**Example:**
```swift
struct RDEPUBTextPerformanceSample {
var chapterHref: String
var renderDuration: TimeInterval // DTHTMLAttributedStringBuilder
var paginateDuration: TimeInterval // CTFramesetter pagination
var totalDuration: TimeInterval // end-to-end
var attributedStringLength: Int
var pageCount: Int
var cacheHit: Bool
}
// Measurement pattern:
let start = CFAbsoluteTimeGetCurrent()
// ... operation ...
let duration = CFAbsoluteTimeGetCurrent() - start
```
**Source:** [ASSUMED] -- standard iOS performance measurement pattern
### Anti-Patterns to Avoid
- **Storing NSAttributedString via JSONEncoder:** NSAttributedString is not Codable. Use NSKeyedArchiver or store the raw HTML + re-render parameters instead.
- **Cache key that includes theme colors:** Theme (dark/light) changes CSS output but the pagination structure (NSRanges) is the same for the same font/size/insets. However, the attributed string content differs. Cache should store the full RDEPUBTextBook including styled content, so theme must be part of the key OR cache should store structure only and re-render content.
- **Measuring on main thread:** All build/paginate work happens on `DispatchQueue.global(qos: .userInitiated)`. Timing must be captured there, not dispatched back to main.
- **Infinite cache growth:** Must implement eviction (LRU or size-based) to prevent disk space issues.
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| SHA256 hashing | Custom hash function | CryptoKit or CommonCrypto | Cryptographic correctness, no collision risk |
| File system cache directory | Custom path logic | `FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)` | Standard iOS cache location, auto-managed by OS |
| Thread-safe cache access | Manual locks | `DispatchQueue` or `NSLock` | Standard concurrency pattern |
| Orphan/widow detection | Line counting heuristics | CTFrame line origins + paragraph range analysis | CoreText provides exact line positions |
**Key insight:** The hardest part of this phase is NOT the algorithms -- it's serializing `RDEPUBTextBook` which contains `NSAttributedString` (not Codable) and `NSRange` (not natively Codable in Swift). The serialization strategy is the most consequential design decision.
## Common Pitfalls
### Pitfall 1: NSAttributedString Serialization
**What goes wrong:** Attempting to JSONEncode an `RDEPUBTextBook` fails silently because `NSAttributedString` and `NSRange` are not Codable.
**Why it happens:** `RDEPUBTextPage.content` is `NSAttributedString`, and `RDEPUBTextPageMetadata` contains `[NSRange]` fields.
**How to avoid:** Use `NSKeyedArchiver` for the whole object, OR decompose into Codable parts (store HTML + parameters, re-render on load). The second approach is more resilient to code changes but slower on cache load.
**Warning signs:** `JSONEncoder.encode()` returns nil or throws.
### Pitfall 2: Cache Invalidation on Code Changes
**What goes wrong:** After updating the pagination engine code, cached results are stale but the cache key hasn't changed.
**Why it happens:** Cache key only encodes user-facing parameters (font, size, insets), not code version.
**How to avoid:** Include a schema version number in the cache key. Bump the version when pagination logic changes.
**Warning signs:** Pagination quality doesn't improve after code changes until cache is manually cleared.
### Pitfall 3: NSRange Codable in Metadata
**What goes wrong:** `RDEPUBTextPageMetadata` conforms to `Codable` but contains `NSRange` fields (`blockRange`, `attachmentRanges`) which don't have native Codable conformance.
**Why it happens:** `NSRange` is an Objective-C struct, not a Swift Codable type.
**How to avoid:** Add `Codable` conformance for `NSRange` via an extension that encodes as `{"location": Int, "length": Int}`, or use `NSKeyedArchiver` for the whole metadata.
**Warning signs:** Compiler error or runtime crash when encoding metadata.
### Pitfall 4: Image Pages Crossing Boundaries
**What goes wrong:** A large image that doesn't fit on the current page causes the page to have excessive whitespace or the image gets truncated.
**Why it happens:** CoreText treats `NSTextAttachment` as inline content and will break the line at the attachment, but doesn't check if the image height exceeds remaining page space.
**How to avoid:** Before pagination, check attachment heights against remaining page space. If an image won't fit, break the page before the image paragraph.
**Warning signs:** Pages with only 1-2 lines of text followed by a large gap.
### Pitfall 5: Cache Thread Safety
**What goes wrong:** Concurrent reads/writes to the cache directory cause file corruption or crashes.
**Why it happens:** `builder.build()` runs on a background queue, and multiple `paginate()` calls can overlap (pagination token pattern handles cancellation but not serialization).
**How to avoid:** Use a serial dispatch queue for all cache operations, or use `NSFileCoordinator` for file-level coordination.
**Warning signs:** Intermittent crashes on `NSKeyedUnarchiver.unarchiveObject`.
## Code Examples
### Cache Key Construction
```swift
// Based on WXRead WRChapterPageCount.currentCacheKeyWithBookId:
// Source: Doc/WXRead/decompiled/WRChapterPageCount.m lines 64-90
import CryptoKit
struct RDEPUBTextBookCacheKey: Hashable {
let bookID: String
let fontSize: CGFloat
let lineHeightMultiple: CGFloat
let contentInsets: UIEdgeInsets
let pageSize: CGSize
let schemaVersion: Int // bump when pagination logic changes
var filename: String {
let components = [
bookID,
String(format: "%.1f", fontSize),
String(format: "%.2f", lineHeightMultiple),
String(format: "%.0f", contentInsets.top),
String(format: "%.0f", contentInsets.left),
String(format: "%.0f", contentInsets.bottom),
String(format: "%.0f", contentInsets.right),
String(format: "%.0f", pageSize.width),
String(format: "%.0f", pageSize.height),
"v\(schemaVersion)"
]
let raw = components.joined(separator: "_")
let hash = SHA256.hash(data: Data(raw.utf8))
return hash.map { String(format: "%02x", $0) }.joined() + ".cache"
}
}
```
### NSKeyedArchiver Serialization for RDEPUBTextBook
```swift
// NSAttributedString supports NSCoding via NSKeyedArchiver
// RDEPUBTextBook/Chapter/Page need NSCoding conformance or wrapper
// Option A: Store as NSCoding-compatible wrapper
final class RDEPUBTextBookArchive: NSObject, NSCoding {
let chapters: [RDEPUBTextChapterArchive]
init(chapters: [RDEPUBTextChapterArchive]) { self.chapters = chapters }
required init?(coder: NSCoder) {
guard let chapters = coder.decodeObject(forKey: "chapters") as? [RDEPUBTextChapterArchive] else { return nil }
self.chapters = chapters
}
func encode(with coder: NSCoder) {
coder.encode(chapters, forKey: "chapters")
}
}
// Each RDEPUBTextChapterArchive stores:
// - attributedContent as NSData (NSKeyedArchiver)
// - page ranges as [[location, length]]
// - metadata as JSON Data
```
### Performance Sample Collection
```swift
// No existing pattern in codebase -- standard iOS approach
final class RDEPUBTextPerformanceSampler {
struct Sample {
let chapterHref: String
let renderDuration: TimeInterval
let paginateDuration: TimeInterval
let pageCount: Int
let attributedStringLength: Int
let cacheHit: Bool
}
private(set) var samples: [Sample] = []
func record(_ sample: Sample) {
samples.append(sample)
print("[PERF] \(sample.chapterHref): render=\(String(format: "%.1f", sample.renderDuration*1000))ms paginate=\(String(format: "%.1f", sample.paginateDuration*1000))ms pages=\(sample.pageCount) cache=\(sample.cacheHit ? "HIT" : "MISS")")
}
func summary() -> String {
let totalRender = samples.reduce(0) { $0 + $1.renderDuration }
let totalPaginate = samples.reduce(0) { $0 + $1.paginateDuration }
let hitRate = samples.filter(\.cacheHit).count
return "[PERF] chapters=\(samples.count) render=\(String(format: "%.0f", totalRender*1000))ms paginate=\(String(format: "%.0f", totalPaginate*1000))ms cacheHits=\(hitRate)/\(samples.count)"
}
}
```
## State of the Art
| Old Approach (assumed) | Current Approach (actual) | When Changed | Impact |
|------------------------|---------------------------|--------------|--------|
| `ss_pageRanges` for pagination | `rd_paginatedFrames` via `RDEPUBTextLayouter` | Phase 7 | 4-level semantic break engine already active |
| No CSS `` handling | `inlineLinkedStyleSheets()` in renderer support | Phase 7 or earlier | EPUB external stylesheets already resolved |
| Flat CSS injection | 5-layer cascade via `RDEPUBTextStyleSheetPackage` | Phase 7 | default/replace/dark/epub/user layers already built |
| No page metadata | `RDEPUBTextPageMetadata` on every page | Phase 7 | breakReason, blockKinds, semanticHints already available |
**Deprecated/outdated in CONTEXT.md:**
- D-01 (integrate layouter): Already done. `RDEPUBTextBookBuilder` line 196 calls `content.rd_paginatedFrames(size:)`.
- D-02 (expose metadata): Already done. `RDEPUBTextPage.metadata` is populated from `frame.metadata`.
- D-04 (CSS `` inline): Already done. `RDEPUBTextRendererSupport.inlineLinkedStyleSheets()` handles this.
- D-05 (5-layer cascade): Already done. `makeStyleSheetLayers()` produces the 5-layer package.
## Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | `NSKeyedArchiver` can serialize `NSAttributedString` with DTCoreText custom attributes | Cache serialization | Cache would fail silently; fallback to re-render every time |
| A2 | `CFAbsoluteTimeGetCurrent()` has sufficient precision for millisecond-level timing | Performance sampling | Sub-millisecond operations may show as 0ms |
| A3 | Cache key schema version bump is sufficient for invalidation on code changes | Cache invalidation | Stale cache could persist if version is forgotten |
| A4 | CTFrame line origins can detect orphan/widow conditions | Pagination quality | Orphan/widow control would need different approach |
## Open Questions (RESOLVED)
1. **Cache storage format: NSKeyedArchiver vs decomposed JSON?** ✅ RESOLVED
- Decision: Use `NSKeyedArchiver` first. Create private `NSCoding` wrapper classes for `RDEPUBTextBook`/`RDEPUBTextChapter`/`RDEPUBTextPage`/`RDEPUBTextPageMetadata`.
- Rationale: `NSAttributedString` and `NSRange` both support `NSCoding` natively. DTCoreText custom attributes are standard `NSAttributedString` attribute keys (string-typed) and survive archiver round-trip.
- Fallback: If runtime test reveals attribute loss, fall back to decomposed JSON (store HTML + render parameters, re-render on cache load).
2. **Should cache store full attributed content or just page ranges?** ✅ RESOLVED
- Decision: Cache full `RDEPUBTextBook` including `NSAttributedString` per page (matches D-01 decision in CONTEXT.md).
- Rationale: Re-slicing on load would negate the cache performance benefit. Memory pressure is manageable for typical EPUB books (< 500 pages).
- Mitigation: Cache eviction on memory warning (`didReceiveMemoryWarning` notification).
3. **Orphan/widow control: how aggressive?** ✅ RESOLVED
- Decision: Add `RDEPUBTextLayoutConfig` struct with configurable thresholds (`avoidOrphans: Bool = true`, `avoidWidows: Bool = true`), defaulting to enabled.
- Rationale: Follows the same pattern as `RDEPUBTextRenderStyle` (public struct + Equatable + explicit init). Hard-coding would prevent future tuning.
- WXRead reference: `WRCoreTextLayoutConfig` uses the same configurable approach.
## Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Xcode / iOS SDK | Build & run | ✓ | (system) | -- |
| DTCoreText | HTML rendering | ✓ | (existing dependency) | -- |
| CoreText | Pagination | ✓ | (system framework) | -- |
| CryptoKit | SHA256 for cache key | ✓ | iOS 13+ | CommonCrypto |
| FileManager | Cache directory | ✓ | (system framework) | -- |
**Missing dependencies with no fallback:** None
**Missing dependencies with fallback:** None
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | None detected -- no test target in project |
| Config file | none |
| Quick run command | N/A |
| Full suite command | N/A |
### Phase Requirements -> Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| QUAL-01 | Cache hit skips rebuild | manual/runtime | Print cache HIT/MISS in console | N/A |
| QUAL-02 | avoidPageBreakInside enforced | manual/runtime | Inspect page diagnostics for semanticBoundary breaks | N/A |
| QUAL-03 | Image sizing rules applied | manual/runtime | Verify image dimensions in diagnostic output | N/A |
| QUAL-04 | Performance samples output | manual/runtime | Check console for [PERF] lines | N/A |
### Sampling Rate
- **Per task commit:** Build succeeds (`build_sim`)
- **Per wave merge:** Runtime log includes [PERF] summary and cache HIT/MISS
- **Phase gate:** Full book builds with cache, second open shows HIT; complex chapter pagination quality visibly improved
### Wave 0 Gaps
- [ ] No test framework exists. All verification is manual/runtime via console output and simulator build.
- [ ] If automated tests desired, would need to create test target first (out of scope for Phase 8).
## Security Domain
No new security concerns introduced. Cache files are stored in the app's caches directory (sandboxed). No network requests, no user data exposure.
### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | no | N/A |
| V3 Session Management | no | N/A |
| V4 Access Control | no | N/A |
| V5 Input Validation | no | N/A |
| V6 Cryptography | no | SHA256 is one-way hash for cache key, not security-sensitive |
## Sources
### Primary (HIGH confidence)
- Project source code: `Sources/RDReaderView/EPUBTextRendering/*.swift` -- direct code inspection
- `Doc/WXRead/decompiled/WRChapterPageCount.h/.m` -- cache key pattern reference
- `Doc/WXRead/decompiled/WRCoreTextLayouter.h` -- layout config reference
- `Doc/WXRead/decompiled/WRCoreTextLayoutFrame.h/.m` -- avoidPageBreakInside reference
- `Doc/WXRead/resources/css/replace.css` -- CSS rule reference
### Secondary (MEDIUM confidence)
- `Doc/架构对比分析_WXRead_vs_ReadViewSDK.md` -- gap analysis (some gaps already closed)
### Tertiary (LOW confidence)
- A1 (NSKeyedArchiver + DTCoreText attributes): Needs runtime verification
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH -- no new dependencies, all system/existing frameworks
- Architecture: HIGH -- integration points clearly visible in code
- Pitfalls: HIGH -- serialization issues are well-understood Cocoa patterns
- Cache design: MEDIUM -- NSKeyedArchiver with DTCoreText custom attributes needs runtime test
**Research date:** 2026-05-23
**Valid until:** 2026-06-23 (30 days -- stable codebase, incremental changes)