md2link

Decrypt Parity Matrix — TapListenSDK ↔ Speechify SSDK

DraftMay 7, 2026

Decrypt Parity Matrix — TapListenSDK ↔ Speechify SSDK

Purpose. Single living document tracking every Speechify SSDK component (from recon/) against TapListenSDK's implementation status. Drives wave planning. Updated whenever a wave lands or a new gap is discovered.

Sources.

  • Speechify recon: /Users/kohuyn/StudioProjects/DecryptTTS/recon/ (class dumps, strings, pipeline notes)
  • TapListenSDK code: /Users/kohuyn/StudioProjects/TapListenSDK/
  • Plan reference: /Users/kohuyn/StudioProjects/DecryptTTS/plans/260429-1908-bookcast-tts-reader/

Status legend

Status Meaning
DONE Implemented, build clean, behavior verified or 1:1 mapping
PARTIAL Implemented with documented deficits — see Notes
SCAFFOLDED Protocol/types exist but throw notConfigured (cloud features)
MISSING Not implemented, no scaffold
INTENTIONAL_SKIP Anti-goal per plan.md (Tier 3+, CarPlay, AI, multi-device sync, etc.)

Severity legend

Severity Meaning
Critical User-visible bug or App Store blocker
High Feature parity loss, affects UX or correctness
Medium Polish, nice-to-have visible diff
Low Edge case or rarely-hit
N/A Severity not applicable (e.g. INTENTIONAL_SKIP)

Domain coverage

Domain Scope decision Status
D1 — Content Pipeline (import/extract/bundle) In scope Pilot done
D2 — Reading Bundle (runtime) In scope Audited
D3 — Reader UI + Block types In scope Audited
D4 — Highlight & Polygon In scope Audited
D5 — TTS Engine + Voice In scope Audited
D6 — Playback / Audio session / CarPlay In scope (no CarPlay) Audited
D7 — Persistence & Library In scope Audited
D8 — User Highlights & Notes Tier 2 TBD (cross-linked from D4.6 + D7.6)
D9 — In-document Search Tier 2 TBD (cross-linked from D3.8.5 + D4.7 + D7.8)
D10 — Skip detection (heuristic + ML) Heuristic only; ML INTENTIONAL_SKIP Folded into D2.4
D11 — Sync / Cloud accounts INTENTIONAL_SKIP (offline-first) Skipped
D12 — Auth & Account INTENTIONAL_SKIP Skipped
D13 — Onboarding & Paywall INTENTIONAL_SKIP Skipped
D14 — AI features (summarize/quiz/podcast) INTENTIONAL_SKIP Skipped
D15 — SDK platform (DI, factories, plugins) In scope Audited

D1 — Content Pipeline

Speechify role: Ingest files from various sources → unified content representation. Entry point SSDKContentImporter, output SSDKContentBundle (sealed).

Key recon files: recon/ssdk-full-pipeline.md §D1, recon/speechify-import-classes.txt, recon/speechify-strings.txt:138315-138969.

D1.1 — Importer state machine

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D1.1.1 SSDKContentImporter strings:138957 Core/Services/ImportService.swift DONE Same role: state-machine-driven import orchestrator
D1.1.2 SSDKContentImporterState (sealed) strings:138960 ImportState enum in ImportService.swift PARTIAL Medium We have 5 cases (idle/starting/importing/finished/failed/duplicate); SSDK has 6 (NotImported/Starting/Importing/Imported/ImportedToLibrary/Failed). Missing: distinct "ImportedToLibrary" stage after Imported (we collapse them)
D1.1.3 SSDKContentImporterStateInfo strings:138959 (none) MISSING Low SSDK wraps state with metadata struct; we use enum associated values inline. Functionally equivalent for now
D1.1.4 SSDKContentImporterStateFailed (with error payload) strings:138961 .failed(message: String) case PARTIAL Medium We carry only message string; SSDK carries typed Error. Can recover later if we move to typed ImportError in state
D1.1.5 Per-state class hierarchy (Kotlin sealed → ObjC binding) strings:138962-138969 Swift enum (more idiomatic) DONE Idiomatic Swift; no parity issue

D1.2 — Content providers (input sources)

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D1.2.1 SSDKFileContentProvider (local file) ssdk-full-pipeline §D1 ImportService.importFile(at:) + DocumentFormat.detect(from:) DONE Function-based, not protocol-based; equivalent capability
D1.2.2 SSDKScanContentProvider (camera scan) ssdk-full-pipeline §D1 (none) INTENTIONAL_SKIP N/A Camera/scan multi-page OCR is anti-goal per plan.md "Anti-Goals (Defer Past Tier 2)"
D1.2.3 SSDKSpeechifyBookContentProvider (book store) ssdk-full-pipeline §D1 (none) INTENTIONAL_SKIP N/A Speechify-internal book catalog; not applicable
D1.2.4 URL/web content provider speechify-import-classes:22 (URLImportService) ImportService.importWebURL + WebPageExtractor DONE Web URL import flow exists
D1.2.5 WebFileImportService (multi-format web import dispatcher) speechify-import-classes:38-49 (none — single WebPageExtractor) PARTIAL Medium SSDK has dedicated dispatcher routing web URL to xmlFileImportModule / pdfImportModule / epubImportModule based on detected MIME. We route only HTML; downloading a PDF URL won't trigger PDFExtractor

D1.3 — Per-format extractors / import modules

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D1.3.1 PDFImportModule speechify-import-classes:15 Core/Services/Extractors/PDFExtractor.swift DONE PDFKit-based, with OCR fallback
D1.3.2 XMLFileImportModule (uses ConvertToPDFServiceInterface) speechify-import-classes:31 DOCXExtractor.swift (different path: native zip+XML walk) PARTIAL Medium SSDK relies on cloud-side PDF conversion for XML/DOC formats; we walk DOCX zip locally. Better for offline; risks coverage gaps on weird docx variants
D1.3.3 LocalFileImportModule speechify-import-classes:53 PlainTextExtractor.swift + format dispatch in ImportService DONE Equivalent role
D1.3.4 EPUB import module (implied via _epubImportModule lazy storage in WebFileImportService:45) Core/Services/Extractors/EPUBExtractor.swift DONE Custom zip+OPF walker, no EPUBKit dep
D1.3.5 Markdown parser (apple/swift-markdown + cmark-gfm) strings-all:50050-50100 + 30+ AST node files MarkdownExtractor.swift (AST walker via swift-markdown SPM 0.7.3 + cmark-gfm 0.7.1) DONE Wave 4 — same library Speechify uses. Closes ~21 gaps: ordered lists, GFM tables, frontmatter strip, intra-word _, escaped chars, autolinks, HTML entities, task list markers, etc.
D1.3.6 RTF (implicit; not separately enumerated in recon) RTFExtractor.swift DONE NSAttributedString native
D1.3.7 Plain text (implicit) PlainTextExtractor.swift DONE
D1.3.8 Image import module (_imageImportModule) speechify-import-classes:46 (none — OCR handled inside PDFExtractor only) PARTIAL Low Speechify has dedicated image-import → OCR path. We only do OCR inside PDF flow. Standalone JPG/PNG import → text would need a new path
D1.3.9 Locale file import module (_localeFileImportModule) speechify-import-classes:47 (none) MISSING Low Unclear purpose from recon alone — possibly localization-aware fallback. Defer until use case clear

D1.4 — Cloud conversion service

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D1.4.1 ConvertToPDFServiceInterface speechify-import-classes:33,40 Core/Services/Cloud/CloudConvertService.swift SCAFFOLDED High Protocol exists with CloudConvertStub throwing providerNotConfigured. Real impl needs CloudConvert API key. Used by DOCExtractor for legacy .doc
D1.4.2 convertToPdfService global injection speechify-strings (used in XMLFileImportModule, WebFileImportService) (no DI yet — direct instantiation in DOCExtractor) PARTIAL Medium Should inject via DependencyContainer not hard-code stub. Wave 9 candidate (bundler refactor)

D1.5 — Content bundle (output type)

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D1.5.1 SSDKContentBundle (sealed parent) strings:138881 RawDocument + CleanDocument structs PARTIAL Medium We split intermediate (raw) vs cleaned, but no sealed hierarchy. Functionally fine for Tier 1
D1.5.2 SSDKContentBundleBookBundle (PDF) strings:138882 RawDocument from PDFExtractor DONE Same data captured (blocks + metadata + TOC)
D1.5.3 SSDKContentBundleEpubBundle strings:138883 RawDocument from EPUBExtractor DONE
D1.5.4 SSDKContentBundleMarkdownBundle strings:138884 RawDocument from MarkdownExtractor (AST-backed) DONE Wave 4 — see D1.3.5
D1.5.5 SSDKContentBundlePlainTextBundle strings:138885 RawDocument from PlainTextExtractor DONE
D1.5.6 SSDKContentBundleStandardBundle (rich web) strings:138886 RawDocument from WebPageExtractor PARTIAL Medium Speechify's StandardBundle is the post-bundler "ready-to-listen" form (see D1.6). We don't have a bundler stage at all
D1.5.7 SSDKContentBundleStandardBundleCompanion strings:138887 (none) MISSING Medium "Companion" carries display markup alongside TTS text. Speechify ships TWO outputs from bundler: text-for-TTS + markup-for-rendering. We conflate both into TextBlock.text
D1.5.8 SSDKContentBundleWebPageBundle (raw HTML) strings:138888 (none — we go straight to cleaned web) MISSING Low SSDK keeps raw HTML alongside cleaned bundle. We don't preserve raw input. Could matter for re-parsing

D1.6 — Bundler stage (extract → bundle conversion)

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D1.6.1 SSDKContentBundler strings:138889 (no separate stage — ImportService.runImport chains extract+clean+save inline) MISSING High Architecturally significant. Speechify isolates bundling (text-for-TTS vs display markup separation) as a discrete stage. We don't. Affects Markdown rendering and future cloud TTS chunking. Wave 9 refactor candidate
D1.6.2 SSDKContentBundlerConfig strings:138890 SkipConfig struct PARTIAL Medium SkipConfig covers heuristic flags but not parsing strategy / OCR fallback config
D1.6.3 SSDKContentBundlerOptions (11 skip flags + parsing modes) ssdk-full-pipeline §D2 + strings:138891 SkipConfig (5 flags) PARTIAL High Missing: shouldSkipFootnotes, shouldSkipBraces, shouldSkipCitations, shouldSkipParentheses, shouldSkipBrackets, shouldSkipUrls, shouldSkipCaptions, shouldSkipTables, shouldSkipFormulas. We have only headers/footers/page-numbers/hyphens/wrapped-lines
D1.6.4 SSDKBundlerFactory + Config + Plugins strings:138871-138875 (no factory, direct instantiation) MISSING Low Plugin-extensible bundler factory. Over-engineered for Tier 1. Defer until Wave 9
D1.6.5 mlParsingMode (on-device ML parsing) ssdk-full-pipeline §D2 (none — heuristic only) INTENTIONAL_SKIP N/A Plan says "ML là Tier 3"
D1.6.6 sdkHeuristicParsingMode ssdk-full-pipeline §D2 TextCleaner DONE Heuristic noise removal exists
D1.6.7 ocrFallbackStrategy ssdk-full-pipeline §D2 PDFExtractor.enableOCRFallback + OCRService.shouldRunOCR PARTIAL Medium Boolean toggle + page-text-density heuristic. SSDK has typed strategy enum (likely .never/.fallback/.always). Our binary toggle is sufficient for Tier 1

D1.7 — Supporting types (cursors, boundaries, references)

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D1.7.1 SSDKContentDescriptor strings:138623 DocumentFormat enum DONE Type tag for content; enum is Swift-idiomatic
D1.7.2 SSDKContentBoundary (START/END markers) strings:138315-138318 (charOffset, charLength) on TextBlock/CleanBlock PARTIAL Low We use offsets directly. SSDK uses opaque boundary type (probably for cross-format polymorphism). Matters when adding cloud TTS chunking
D1.7.3 SSDKContentCursor (immutable position pointer) strings:138319 ReaderCursor struct DONE (blockIndex, charOffsetInBlock)
D1.7.4 SSDKContentCursorComparator strings:138323 (none — Equatable + manual compare) MISSING Low Comparator class for sorting cursors. We can derive from struct compare. Wire when needed for search/highlights ordering
D1.7.5 SSDKContentCursorCompanion (factory methods) strings:138320 ReaderCursor.zero + initializers DONE
D1.7.6 SSDKContentElementReference (deep link to element) strings:138327 (none) MISSING Medium Used for bookmark/note targeting and deep linking. Currently we only store block index — can't deep-link to mid-block element. Affects D8 (User Highlights) when implemented
D1.7.7 SSDKContentAccess (free/preview/paid permission) strings:139100 (none) INTENTIONAL_SKIP N/A Speechify monetization layer; we're free/offline-first

D1.8 — Import record schema (audit trail)

# Speechify field Recon evidence Our equivalent Status Severity Notes
D1.8.1 speechifyUri ssdk-full-pipeline §D1 method sig Document.id: UUID DONE Different format, same role
D1.8.2 primaryFileBlobStorageKey ssdk-full-pipeline §D1 Document.fileRelativePath DONE Wave 10 — relative path under Documents/Imports/ resolved at runtime. Sandbox restore safe
D1.8.3 scannedPages ssdk-full-pipeline §D1 (none) INTENTIONAL_SKIP N/A See D1.2.2
D1.8.4 sourceURL ssdk-full-pipeline §D1 Document.sourceURLString DONE
D1.8.5 importOptions ssdk-full-pipeline §D1 (none persisted) MISSING Low Skip-config used at import time isn't persisted with the document. If user changes config later, can't replay
D1.8.6 htmlContentLoadOptions ssdk-full-pipeline §D1 (none) MISSING Low Web-specific options
D1.8.7 importType ssdk-full-pipeline §D1 Document.format DONE
D1.8.8 attemptsPerformedCount ssdk-full-pipeline §D1 (none) MISSING Low Retry telemetry. Not user-visible
D1.8.9 owner ssdk-full-pipeline §D1 (none — single user) INTENTIONAL_SKIP N/A No multi-user support
D1.8.10 lastUpdatedAt ssdk-full-pipeline §D1 Document.lastReadDate (semantic mismatch) PARTIAL Low We track lastRead, not lastUpdated of the import record itself
D1.8.11 listeningProgress ssdk-full-pipeline §D1 Document.lastReadCharOffset + new lastReadBlockOrderIndex/lastReadCharOffsetInBlock DONE Wave 2 fix
D1.8.12 mimeType ssdk-full-pipeline §D1 (derived from Document.format) PARTIAL Low Not persisted as MIME string; derived
D1.8.13 importStatus ssdk-full-pipeline §D1 (none persisted — only in-memory ImportState) MISSING Low We don't write status to Document; on success, we just persist the doc. No "Failed" record kept. Acceptable for Tier 1

D1 summary

Severity Count Top items
Critical 1 D1.8.2 absolute file URL (App Store blocker, sandbox restore breaks)
High 4 D1.3.5 markdown regex, D1.5.4 markdown bundle, D1.6.1 bundler stage missing, D1.6.3 skip config gaps
Medium 8 importer state collapse, web file import dispatcher, content companion, cloud DI, ocr strategy enum, element reference, content boundary type, raw web bundle
Low 6 state info wrapper, locale module, image module, attempts count, html load options, mime persistence, import status persistence, content cursor comparator
Intentional skip 6 scan provider, speechify book, content access, scanned pages field, owner field, ML parsing

D1 wave assignment proposal:

  • Wave 4 (already proposed) — D1.3.5 + D1.5.4 (Markdown via swift-markdown SPM)
  • Wave 9 (architectural refactor) — D1.5.7 + D1.6.1 + D1.6.3 (Bundler stage with text-for-TTS vs display-markup split, expanded skip flags)
  • Wave 10 (P0 tech debt) — D1.8.2 (relative file URL)
  • Backlog — D1.2.5 (web dispatcher routing), D1.4.2 (DI for convert service), D1.7.6 (element reference for D8 prerequisite)

D2 — Reading Bundle

Speechify role: Convert an imported SSDKContentBundle into a ready-to-listen runtime object (SSDKReadingBundle subtype) via a discrete Bundler factory step. The bundle carries all block data, TOC, metadata, and playback config needed to drive both the Reader UI and the audio controller — separated from the content pipeline by a factory seam.

Key recon files: recon/ssdk-full-pipeline.md §D2 (lines 136–191), recon/speechify-strings.txt:138871–138956, recon/speechify-strings.txt:136398–136969, recon/ssdk-full-pipeline.md Pipeline B (lines 729–814).

D2.1 — ReadingBundle type hierarchy

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D2.1.1 SSDKReadingBundle (root interface) strings:138923 (no explicit type — Document + [TextBlock] loaded by ReaderViewModel.bind) MISSING Medium We have no ReadingBundle concept. ReaderViewModel does inline assembly from Document + sorted textBlocks. Functionally sufficient for Tier 1 but structurally no factory seam
D2.1.2 SSDKBasicReadingBundle strings:138904 (none) MISSING Low SSDK root abstract bundle; no direct equivalent. Gap is structural, not user-visible
D2.1.3 SSDKBasicReadingBundleWithContent strings:138905 (none) MISSING Low Second abstract level in hierarchy. Same note as D2.1.2
D2.1.4 SSDKReadingBundleWithContent strings:138924 (none) MISSING Low Third abstract level. Carries actual [Block]. Our ReaderViewModel.blocks: [TextBlock] fills the same slot but is not a bundle type
D2.1.5 SSDKClassicReadingBundle (flowing-text reader) strings:138933 Document + [TextBlock] assembled by ReaderViewModel.bind(_:modelContext:) PARTIAL High Key gap. SSDK's ClassicReadingBundle is the output of a discrete bundler stage that re-processes ContentBundle blocks at open time. We skip that stage; blocks are persisted directly from TextCleaner. No re-processing at open time. Affects skip-flag changes between imports and opens
D2.1.6 SSDKBookReadingBundle (fixed-layout PDF) strings:138928 (none) INTENTIONAL_SKIP N/A Anti-goal: PDF rendered as reflowed text only
D2.1.7 SSDKEpubReadingBundle strings:138956 (none — EPUB collapses into same Document/TextBlock path) INTENTIONAL_SKIP N/A EPUB folded into linear TextBlock list. Acceptable for Tier 1
D2.1.8 SSDKEmbeddedReadingBundle* strings:138951–138952 (none) INTENTIONAL_SKIP N/A Snippet/preview embed bundle for share-sheet/widgets. Not a Tier-1 goal
D2.1.9 SSDKDynamicStandardViewReadingBundle strings:138949 (none) INTENTIONAL_SKIP N/A Adaptive web reader mode. Anti-goal
D2.1.10 SSDKListeningBundle strings:138895 (none — audio payloads assembled inline in ReaderViewModel.buildPlayQueue()) MISSING Medium SSDK pre-stages audio chunks for the audio controller. We build TTS queue on-the-fly

D2.2 — Bundler factory (ContentBundle → ReadingBundle conversion)

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D2.2.1 SSDKReadingBundlerBase strings:138926 (none) MISSING Medium Abstract base for all bundlers. We have no bundler tier — ImportService.runImport chains extract→clean→persist inline
D2.2.2 SSDKReadingBundler strings:138925 (none) MISSING Medium Concrete bundler entry point. Closest equivalent is ImportService.runImport, but no typed bundle artifact
D2.2.3 SSDKClassicReadingBundler strings:138936 (none — split between TextCleaner.swift and ReaderViewModel.bind) MISSING High Architecturally significant. Runs layout analysis at open time against extracted content. We apply TextCleaner once at import; no re-bundle path if SkipConfig changes
D2.2.4 SSDKBookReadingBundler strings:138929 (none) INTENTIONAL_SKIP N/A Fixed-layout PDF
D2.2.5 SSDKUniversalSourcesReadingBundler strings:138927 (none) MISSING Low Auto-selects best bundler. We always use the same flat path — acceptable since we have one reader mode
D2.2.6 SSDKEmbeddedReadingBundler strings:138953 (none) INTENTIONAL_SKIP N/A Embedded snippet bundle
D2.2.7 SSDKListeningBundler strings:138896 (none) MISSING Medium Pre-stages audio chunks for SSDKAudioController. We produce TTSPlayBlock array inline at play time
D2.2.8 SSDKBundlerFactory + Config + Plugins strings:138871, 138873, 138874 (none) MISSING Low Plugin-extensible bundler factory. Overengineered for Tier 1
D2.2.9 createBundleForLibraryItem:bundleMetadata:callback: (async factory) strings:137040 ReaderViewModel.bind — synchronous, no callback PARTIAL Medium SSDK creates bundles asynchronously off main thread. Long documents may cause frame drop on open

D2.3 — Master bundler config (composition)

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D2.3.1 SSDKBundlerFactoryConfig (master config, 5 sub-configs) strings:138873; selector at strings:136919 (none — no bundler, no master config) MISSING Medium SSDK composes 5 typed sub-configs into one injectable object
D2.3.2 SSDKClassicReadingBundlerConfig/Options strings:138937–138938 SkipConfig in TextCleaner.swift PARTIAL High Covers 5 of 11 skip flags; see D2.4
D2.3.3 SSDKBookReadingBundlerConfig/Options strings:138930–138931 (none) INTENTIONAL_SKIP N/A Fixed-layout PDF config
D2.3.4 SSDKEmbeddedReadingBundlerConfig/Options strings:138954–138955 (none) INTENTIONAL_SKIP N/A Embedded snippet config
D2.3.5 SSDKListeningBundlerConfig/Options strings:138898–138899 (none) MISSING Medium Audio pre-staging config. Our audio settings scattered across ReaderViewModel.rate, voiceID — not composable
D2.3.6 SSDKContentBundlerConfig strings:138890 SkipConfig PARTIAL High Same gap as D2.3.2; additionally not persisted alongside import record
D2.3.7 SSDKBookReadingContentProcessingOptions strings:138932 (none) INTENTIONAL_SKIP N/A PDF-specific options

D2.4 — Content bundler options (skip flags + parsing modes)

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D2.4.1 shouldSkipHeaders strings:136949 SkipConfig.skipHeaders DONE Recurring-text heuristic
D2.4.2 shouldSkipFooters strings:136953 SkipConfig.skipFooters DONE Same heuristic
D2.4.3 shouldSkipPageNumbers (implicit) SkipConfig.skipPageNumbers + TextCleaner.isPageNumber DONE Regex (page\s*)?\d{1,4}(/\d{1,4})?
D2.4.4 shouldSkipFootnotes strings:136957 (none) MISSING High High-value for academic PDFs. Wave 4 candidate
D2.4.5 shouldSkipBraces strings:136398 (none) MISSING Medium Skips {...}
D2.4.6 shouldSkipCitations strings:136400 (none) MISSING Medium Skips [1], (Author, 2023). Academic value
D2.4.7 shouldSkipParentheses strings:136402 (none) MISSING Low Can break readability if too aggressive
D2.4.8 shouldSkipBrackets strings:136404 (none) MISSING Medium Editorial annotations
D2.4.9 shouldSkipUrls strings:136406 (none) MISSING Medium High value — reading raw URLs is jarring
D2.4.10 shouldSkipCaptions strings:136959 (none) MISSING Low Lower value for audio-only
D2.4.11 shouldSkipTables strings:136961 (none) MISSING Medium Tables read linearly are confusing
D2.4.12 shouldSkipFormulas strings:136963 (none) MISSING High LaTeX/MathML symbol strings unlistenable
D2.4.13 fixHyphens (Pipeline B:782) SkipConfig.fixHyphens + TextCleaner.fixHyphens DONE
D2.4.14 mergeWrappedLines (Pipeline B:781) SkipConfig.mergeWrappedLines + TextCleaner.mergeWrapped DONE
D2.4.15 sdkHeuristicParsingMode strings:136967 TextCleaner (all heuristics) DONE
D2.4.16 mlParsingMode strings:136965 (none) INTENTIONAL_SKIP N/A "ML là Tier 3"
D2.4.17 ocrFallbackStrategy (typed enum) strings:136969 PDFExtractor.enableOCRFallback: Bool + OCRService.shouldRunOCR PARTIAL Medium Boolean toggle vs typed enum. Sufficient for Tier 1

D2.5 — Bundle metadata and prepared blocks

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D2.5.1 SSDKBundleMetadata strings:138908 DocumentMetadata (import) + Document fields (runtime) PARTIAL Low SSDK adds analytics fields (contentSubType, importFlow, contentHostname, etc.). We carry only title/author/lang/pageCount/coverImage
D2.5.2 SSDKEmbeddedBundleMetadata strings:138914 (none) INTENTIONAL_SKIP N/A
D2.5.3 SSDKContentProperties/SSDKEventProperties strings:138909–138910 (none) MISSING Low Telemetry only
D2.5.4 SSDKPreparedBlock (text + audio + ref) strings:138943–138946 PreparedBlock struct (index + text + startCharOffset + blockIndex ref) in Core/Services/CloudTTS/PreparedBlock.swift DONE Wave 5 — same shape. Audio attached via PreparedAudio enum
D2.5.5 SSDKPreparedAudio* (URL/Data) strings:138939–138942 PreparedAudio enum: .data(AudioData) / .url(URL) / .fetching(Task<AudioData, Error>) DONE Wave 5 — sealed hierarchy matching SSDK. .fetching exposes in-flight prefetch task without blocking
D2.5.6 SSDKMutableObservableContentTitle strings:138922 Document.title (static) MISSING Low SSDK exposes title as observable Flow for live updates

D2.6 — ReaderViewModel as bundle consumer

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D2.6.1 SSDKAudioController.fromBundle(...) Pipeline B:769–791 ReaderViewModel.play() builds queue from blocks array PARTIAL Medium SSDK binds audio controller to bundle. We pass raw [TTSPlayBlock] at play time
D2.6.2 SSDKNavigationIntent (10 cases) §D13, Pipeline B:768 jumpTo(blockIndex:) + tapWord(...) PARTIAL Medium We have 2 navigation primitives. Missing GoToPage, GoToSearchMatch, GoToUserHighlight
D2.6.3 TOC backed by bundle Pipeline B:755 Document.tableOfContents from tocJSON DONE Same data; different transport
D2.6.4 Cursor restoration from bundle open intent Pipeline B ReaderViewModel.restoredCursor(for:) DONE Wave 2 stable cursor
D2.6.5 LazyContentProvider (sliding window block access) Pipeline B:799–800 ReaderViewModel.blocks: [TextBlock] eager load PARTIAL Low Memory pressure for large books

D2 summary

Severity Count Top items
Critical 0
High 5 D2.2.3 no ClassicReadingBundler stage; D2.3.2/D2.3.6 SkipConfig 5/11 flags; D2.4.4 footnote skip; D2.4.12 formula skip
Medium 13 D2.1.1 no ReadingBundle type; D2.1.10 no ListeningBundle; D2.2.1/D2.2.2 no bundler tier; D2.2.7 no audio pre-staging; D2.2.9 sync bundle creation; D2.3.1 no master config; D2.3.5 no ListeningBundlerConfig; D2.4.5/6/8/9/11 skip flags; D2.5.4/5 PreparedBlock/Audio; D2.6.1/2 audio binding + navigation
Low 7 abstract hierarchy levels; universal bundler; parens/captions skip; metadata analytics; mutable title; eager block load
Intentional skip 8 BookReadingBundle; EpubReadingBundle; EmbeddedReadingBundle; DynamicStandardView; book/embedded bundlers; ML parsing; embedded metadata

D2 wave assignment proposal:

  • Wave 4 (alongside Markdown) — D2.4.4 footnote skip + D2.4.9 URL skip + D2.4.6 citation skip (3 high-value flags, pure TextCleaner extension)
  • Wave 9 (architectural refactor with D1.6.1) — D2.2.3 ClassicReadingBundler stage, D2.3.1 master config, D2.3.5 ListeningBundlerConfig, D2.4.11 table skip, D2.4.12 formula skip, D2.5.4/D2.5.5 PreparedBlock/Audio pre-staging, D2.6.1 audio controller bundle binding
  • Backlog — D2.2.9 async bundle, D2.4.5/7/8 brace/paren/bracket, D2.6.5 lazy block loading, D2.5.6 observable title

D3 — Reader UI + Block Types

Speechify role: Render a reading bundle as interactive, speech-synchronized scrolling text. Five distinct reader surfaces exist in SSDK (Classic, FixedLayout/PDF, Epub, Kindle/Scan, AudioBrowser). TapListenSDK targets only the Classic-equivalent surface per plan.md. Block and element type hierarchies drive both rendering and TTS chunking fidelity.

Key recon files: recon/ssdk-full-pipeline.md§D3 (lines 194–254), recon/speechify-class-dump-readers.txt:13–95, recon/speechify-strings.txt:122098–124830, 139386–139495, 390611–390620.

D3.1 — Reader surface selection

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D3.1.1 ClassicReaderViewModel (UICollectionView+TextKit) class-dump-readers:13; strings:123815, 164993 ReaderViewModel + ReaderView + BlockTextView PARTIAL High Functional parity in SwiftUI+UITextView, not UICollectionView+TextKit. No virtualization/sliding-window paging — see D3.3
D3.1.2 FixedLayoutReaderViewModel (PDF page-curl) class-dump-readers:138; strings:123401, 124296 (none) INTENTIONAL_SKIP N/A Anti-goal per plan.md
D3.1.3 EpubReaderViewModel (vertical + horizontal pagination) strings:122024, 169619 (none) INTENTIONAL_SKIP N/A EPUB extracted and re-rendered in Classic surface
D3.1.4 KindleReader / ScanReader strings:122670, 373411 (none) INTENTIONAL_SKIP N/A DRM + camera-scan readers anti-goal
D3.1.5 AudioBrowserViewModel (browser overlay listener) strings:122899, 131403 (none) INTENTIONAL_SKIP N/A Tier 3+

D3.2 — ClassicBlock type hierarchy

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D3.2.1 SSDKClassicBlock (sealed parent) strings:139353; pipeline§D3 BlockKind enum (9 cases) PARTIAL High SSDK uses sealed class hierarchy; we use flat enum. Subtypes carry rich associated data; our enum carries only level: Int on heading
D3.2.2 SSDKClassicBlockHeading strings:139354 BlockKind.heading(level: Int) DONE
D3.2.3 SSDKClassicBlockParagraph strings:139368 BlockKind.paragraph DONE
D3.2.4 SSDKClassicBlockImageLocal/Remote strings:139355–139357 BlockKind.image PARTIAL Low No local/remote distinction; no URL on image blocks
D3.2.5 SSDKClassicBlockList + 6 subtypes strings:139358–139367 BlockKind.list (single flat case) PARTIAL Medium Nested list indentation and list-style markers lost
D3.2.6 SSDKClassicBlockTable + Row/Cell strings:139369–139371 (none — table text inlined) MISSING Medium No table case; cells flattened into paragraph text
D3.2.7 SSDKClassicBlockListStyle enum strings:139367 (none) MISSING Low No bullet/numbered/alpha discriminator

D3.3 — StandardBlock type hierarchy (web + rich)

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D3.3.1 SSDKStandardBlock (8 subtypes) strings:138426–138438 BlockKind partial overlap PARTIAL Medium Missing Formula, TextInsideImage, Caption
D3.3.2 SSDKStandardBlockCaption strings:138427 (none) MISSING Low Caption text folded into paragraphs
D3.3.3 SSDKStandardBlockFooter strings:138429 BlockKind.footer DONE
D3.3.4 SSDKStandardBlockFootnote strings:138430 (none) MISSING Low Footnotes become paragraphs
D3.3.5 SSDKStandardBlockFormula strings:138431 (none) MISSING Low LaTeX as raw text
D3.3.6 SSDKStandardBlockHeader strings:138432 BlockKind.header DONE
D3.3.7 SSDKStandardBlockHeading strings:138433 BlockKind.heading(level:) DONE
D3.3.8 SSDKStandardBlockList strings:138436 BlockKind.list PARTIAL Low Same gap as D3.2.5
D3.3.9 SSDKStandardBlockTable strings:138437 (none) MISSING Medium HTML/EPUB tables not preserved
D3.3.10 SSDKStandardBlockTextInsideImage strings:138438 (none) MISSING Low OCR'd text inside <img> not extracted

D3.4 — StandardElement granular inline types

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D3.4.1 SSDKStandardElement (21 subtypes) strings:138441–138515 markdownSource: String? on RawBlock/CleanBlock/TextBlock + NSAttributedString(markdown:) rendering PARTIAL Low Wave 6.1 — bold/italic/code/strike/link runs now preserved across all 6 extractors and rendered with merged font traits. Image inline rendering still missing (D3.4.5); sub-word element seek not exposed
D3.4.2 SSDKStandardElementAnchor (external + internal) strings:138442–138444 Markdown link runs preserved; Coordinator.handleTap opens .link URLs via UIApplication.shared.open DONE Wave 6.1 — external links work. Internal/intra-doc anchors not yet implemented (would need TOC anchor map)
D3.4.3 SSDKStandardElementBold/Italics/Underlined strings:138445, 138451, 138515 per-run UIFont symbolic traits merged with base font in BlockTextView.mergedFont DONE Wave 6.1 — bold + italic + bold-italic rendered for MD/DOCX/RTF/PDF/EPUB/Web. Underline (<u>) dropped (no MD equivalent)
D3.4.4 SSDKStandardElementCode strings:138446 inline ` runs → traitMonoSpace font in renderer DONE Wave 6.1 — both block-level and inline code spans rendered with monospace
D3.4.5 SSDKStandardElementImage (local + remote inline) strings:138448–138450 (none) MISSING Low Inline images not rendered
D3.4.6 SSDKStandardElementTable* strings:138509–138512 (none) MISSING Low Same as D3.3.9 element-level

D3.5 — Line-level decomposition

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D3.5.1 SSDKHeadingLines strings:139029 NSLayoutManager.usedRect derived PARTIAL Low We compute geometry on-the-fly; sufficient for word highlight
D3.5.2 SSDKParagraphLines strings:139033 (same — layout manager) PARTIAL Low
D3.5.3 SSDKParagraphParts (text-run decomposition) strings:139034 (none — block text is one string) MISSING Medium No run metadata. Affects inline formatting (D3.4.3) and seek accuracy
D3.5.4 SSDKParagraphPartsWithImplicitWhitespace strings:139036 (none) MISSING Low Whitespace preservation for TTS
D3.5.5 SSDKTopLevelItem (root container) strings:139028 RawDocument/CleanDocument struct PARTIAL Low Functionally equivalent
D3.5.6 SSDKPageParagraph (paragraph + page binding) strings:139484 TextBlock.pageIndex: Int? PARTIAL Low Sufficient for TOC navigation

D3.6 — ReaderViewModel architecture

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D3.6.1 ClassicReaderViewModel : NSObject <UIScrollViewDelegate> (Combine, 30+ subjects) class-dump-readers:13–95 @Observable @MainActor ReaderViewModel (Swift Observation) PARTIAL Medium Idiomatic SwiftUI deviation. No interactor decomposition
D3.6.2 ClassicReader.UserAction (event bus) class-dump-readers:25; strings:122376 tapWord(blockIndex:range:) direct method call PARTIAL Low No middleware composability
D3.6.3 ClassicReader.NavigationIntent (10-case rich type) class-dump-readers:31; strings:139470–139495 jumpTo(blockIndex:) only PARTIAL High Missing GoToPage, GoToSearchMatch, GoToUserHighlight, relative intents (Sentence/Word)
D3.6.4 ClassicReader.HighlightState (CurrentValueSubject) class-dump-readers:45, 62 highlightedBlockIndex + highlightedWordRange PARTIAL High No polygon-based cursor; multi-line bounding gap (see D4)
D3.6.5 ComponentActionBundle (compound gesture results) class-dump-readers:26 (none) MISSING Low
D3.6.6 TapActionHandler class-dump-readers:71 BlockTextView.Coordinator.handleTap PARTIAL Medium Inlined coordinator; not independently testable
D3.6.7 LongPressActionHandler class-dump-readers:72 (none) MISSING Medium Long-press selection + context menu absent. isSelectable = false disables system selection
D3.6.8 DragActionHandler class-dump-readers:73 (none) MISSING Low No drag-scrub seek
D3.6.9 SelectionToolbarPresentationManager class-dump-readers:80 (none) MISSING Medium Floating selection toolbar (copy/define/translate/highlight). Prerequisite for D8
D3.6.10 ContentProvider (3 variants: Classic/Lazy/Static) class-dump-readers:51 (none — VM holds blocks: [TextBlock] directly) MISSING High Memory gap. Static-equivalent only. Large docs (>500 blocks) cause memory pressure. LazyContentProvider needed for production
D3.6.11 ClassicReaderNavigator + InDocumentNavigationBundle class-dump-readers:69–70 inline scrollTo in view PARTIAL Low No navigation isolation
D3.6.12 PreferencesInteractor + TextCustomizations + CursorStyle class-dump-readers:74, 55–58 deps.preferences.readerTextSize only PARTIAL Medium Missing font picker, line-height, column width, cursor style
D3.6.13 SelectionInteractor/CopyTextInteractor/UserHighlightInteractor class-dump-readers:75–78 (none) MISSING Medium Prerequisite for D8
D3.6.14 TableLayoutInteractor class-dump-readers:75 (none) MISSING Low Depends on D3.2.6

D3.7 — Rendered view architecture

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D3.7.1 ClassicReaderRenderer (UICollectionView+compositional layout+TextKit 1) class-dump-readers:42, 60 LazyVStack + BlockTextView PARTIAL High Architectural deviation. No cell recycling; LazyVStack keeps each block's UITextView once visible
D3.7.2 visibleComponentSubject (viewport tracking) class-dump-readers:38, 61 onChange(of: cursor.blockIndex) + scrollTo PARTIAL Low No explicit visible-viewport tracking
D3.7.3 ClassicReaderConfiguration (typed config struct) class-dump-readers:83 scattered deps.preferences reads PARTIAL Low No single config object
D3.7.4 Word-tap → TTS seek class-dump-readers:71 Coordinator.handleTapWordBoundaryFindertapWord DONE Wave 1 fix
D3.7.5 Sentence + word dual-layer highlight class-dump-readers:99 TappableTextView.sentenceLayer + wordLayer DONE Wave 3
D3.7.6 Auto-scroll on cursor advance strings:137667 (enableAutoscroll) scrollTo(..., anchor: .center) DONE

D3.8 — Auxiliary panels

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D3.8.1 TOC panel strings:139471–139472 TableOfContentsView.swift DONE
D3.8.2 Bookmarks sheet strings:139471 BookmarksSheet.swift + Bookmark model DONE
D3.8.3 Voice / TTS settings (implicit) VoicePickerView.swift DONE
D3.8.4 MiniPlayer (Speechify full transport: skip-back, scrubber, AirPlay, sleep timer) MiniPlayer.swift (state-aware loading + progress bar + transport + speed) PARTIAL Medium Wave 6.A — added: state-aware status header (loading spinner / "Block N of M %" progress / error / finished), animated transitions, play button disabled while loading. Still missing: skip-back 30s, chapter prev/next, AirPlay target, sleep timer
D3.8.5 In-document search UI strings:139477; class-dump-readers:53 (none) MISSING Medium Tier 2
D3.8.6 User highlight overlay strings:139479; class-dump-readers:78 (none) MISSING Medium Tier 2
D3.8.7 Quick-back action class-dump-readers:81 (none) MISSING Low Return-to-prior-position button
D3.8.8 Full-screen toggle class-dump-readers:23 (none) MISSING Low
D3.8.9 Skip-to-main-content class-dump-readers:24; strings:139475 (none) MISSING Low
D3.8.10 ReaderEditText (OCR text correction) class-dump-readers:97–135 (none) INTENTIONAL_SKIP N/A We never render page images
D3.8.11 Translation overlay class-dump-readers:18 (none) INTENTIONAL_SKIP N/A Anti-goal

D3 summary

Severity Count Top items
Critical 0
High 6 D3.4.1 inline element tree absent; D3.6.3 navigation intent richness; D3.6.4 HighlightState scalar (no polygon); D3.6.10 no LazyContentProvider; D3.7.1 no cell virtualization; D3.2.1 ClassicBlock sealed vs flat
Medium 10 list/table block granularity; inline anchor/bold/italic; VM monolith; long-press; SelectionToolbar; preferences interactor; MiniPlayer gaps
Low 13 image local/remote, list-style, caption/footnote/formula/OCR-image blocks; inline code/image/table; paragraph parts; whitespace; ComponentActionBundle; DragActionHandler; navigator isolation; visible-component; reader config; quick-back; full-screen; skip-main
Intentional skip 6 FixedLayout, EpubNative, Kindle/Scan, AudioBrowser, ReaderEditText, Translation

D3 wave assignment proposal:

  • Wave 3 (current): D3.7.5 + D3.7.4 + D3.7.6 — DONE
  • Wave 5: D3.2.6 + D3.3.9 (table block + basic table rendering), D3.8.4 (skip-back + scrubber)
  • Wave 6: D3.4.1–D3.4.3 (inline element tree + bold/italic), D3.6.7 + D3.6.9 (long-press + SelectionToolbar — D8 prereq)
  • Wave 7: D3.6.10 (LazyContentProvider — required before production with real corpus)
  • Wave 8: D3.5.3 (paragraph parts / run-level model — architectural)
  • Tier 2: D3.6.3 rich NavigationIntent, D3.8.5 search, D3.8.6 highlights/notes UI

D4 — Highlight & Polygon

Speechify role: Three-tier polygon rendering (paragraph / sentence / word) driven by per-reader HighlightInteractor subclasses, wired to Combine publishers on ClassicReaderViewModel / FixedLayoutReaderViewModel. Color, blend-mode, and toggle state passed as typed parameters on the EPUB config. User-created highlights live in a parallel UserHighlightInteractor system.

Key recon files: recon/speechify-class-dump-readers.txt:37,45,59,62,78,99, recon/speechify-class-dump-render.txt:206-215, recon/speechify-strings.txt:123089,123173,123286-123288,123428,123566-123567,123868,124202,124290-124291,124799,131430,131692, 137667, 138710-138716, 162751, 385036,385357-385359,389040,391637,391712,393577-393578,393605,437958.

D4.1 — Polygon tier model

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D4.1.1 _wordPolygon (@Published CGPolygon?) strings:385357, 393578, 439387 wordLayer: CAShapeLayer in TappableTextView PARTIAL Medium Word polygon as CAShapeLayer rect-union path, not CGPolygon struct. Loses sub-glyph detail
D4.1.2 _sentencePolygon (@Published CGPolygon?) strings:385359, 391637 sentenceLayer: CAShapeLayer PARTIAL Medium Same approach as D4.1.1
D4.1.3 _paragraphPolygons (@Published [CGPolygon]) strings:391712, 437958 (none) MISSING Low Used in OCR-edit view. Not needed for TTS playback in Tier 1
D4.1.4 CGPolygon struct strings:124201,130098,135187 (none — [CGRect] array) MISSING Low Adequate for straight text; breaks on rotated PDF pages
D4.1.5 _sections (@Published [CGPolygon]) strings:391712 (none) MISSING Low Section/chapter visual separators

D4.2 — Word-polygon geometry refinements

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D4.2.1 wordPolygonVerticalEdges strings:389040, 393605 (none) MISSING Medium Edge-handling for word polygons (rounded vs flat caps at line-break boundaries). We apply static cornerRadius: 4 → unnecessary rounding at line-join edges
D4.2.2 roundedPath(rects:cornerRadius:) HighlightShapeBuilder.swift same PARTIAL Low Uniform radius vs SSDK's per-edge table
D4.2.3 wordRange/sentenceRange threading strings:385357-385359 highlightedWordRange + highlightedBlockIndex DONE Equivalent data flow

D4.3 — Blend mode & compositing

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D4.3.1 highlightMixBlendMode strings:137667 (none — alpha compositing only) MISSING Medium Visible defect on dark-mode PDFs. Fix: CAShapeLayer.compositingFilter = "multiplyBlendMode"
D4.3.2 sentenceHighlightHexColor strings:137667 hardcoded systemBlue.alpha(0.10) PARTIAL Medium No theme-aware token
D4.3.3 wordHighlightHexColor strings:137667 hardcoded systemYellow.alpha(0.55) PARTIAL Medium Harsh in dark theme
D4.3.4 hoveredSentenceHighlightHexColor strings:137667 (none) INTENTIONAL_SKIP N/A iPad pointer hover; phone-primary target

D4.4 — Reader-mode HighlightInteractor variants

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D4.4.1 HighlightInteractor (base) strings:123089 (no protocol — inline applyHighlights) MISSING Low No interactor abstraction
D4.4.2 EpubReaderVerticalHighlightInteractor strings:123173 (none) INTENTIONAL_SKIP N/A EPUB out of scope
D4.4.3 EpubReaderHorizontalHighlightInteractor strings:124202 (none) INTENTIONAL_SKIP N/A
D4.4.4 KindleHighlightInteractor strings:124799 (none) INTENTIONAL_SKIP N/A
D4.4.5 AudioBrowserHighlightInteractor strings:131430 (none) INTENTIONAL_SKIP N/A
D4.4.6 Classic reader highlight path class-dump-readers:37,45 applyHighlights + HighlightShapeBuilder PARTIAL Medium Refactor when Wave 7 PDF reader starts

D4.5 — Highlight state & Combine streaming

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D4.5.1 currentHighlightStatePublisher class-dump-readers:45; strings:384995 (none — direct @Observable mutation) MISSING High Wave 1 Bug D root cause. SSDK isolates highlight in dedicated publisher. We collapse with cursor → mid-seek onWord can clobber. lastEventSequence partial mitigation only
D4.5.2 currentHighlightStateSubject: CurrentValueSubject class-dump-readers:62 (none) MISSING High Backing store for D4.5.1
D4.5.3 HighlightPosition (PassthroughSubject value type) strings:123868; class-dump-readers:37 (none) MISSING Medium Drives auto-scroll position
D4.5.4 highlightPositionSubject class-dump-readers:37,161 (none) MISSING Medium Separate subject from state
D4.5.5 isSentenceHighlightEnabled toggle strings:385533, 386018; class-dump-readers:59 (none — always on) MISSING Medium User toggle missing
D4.5.6 isSentenceHighlightEnabledSubject class-dump-readers:59,183 (none) MISSING Medium Reactive backing

D4.6 — UserHighlight system (D8 territory)

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D4.6.1 UserHighlightInteractor strings:123286 (none) MISSING Low D8 prereq
D4.6.2 UserHighlightInteractorIntegration strings:123287 (none) MISSING Low D8 prereq
D4.6.3 UserHighlightInteractorError strings:123288 (none) MISSING Low
D4.6.4 MutableUserHighlight + SDKBackedMutableUserHighlight strings:123566-123567 (none) MISSING Low
D4.6.5 UserHighlightStyle (5-color palette) strings:138710-138716 (none) MISSING Low
D4.6.6 UserHighlightStyleState strings:124290 (none) MISSING Low
D4.6.7 _userHighlightPolygons strings:391638 (none) MISSING Low
D4.6.8 userHighlightColorTokens (EPUB only) strings:137667 (none) MISSING Low EPUB context

D4.7 — Selection & search highlight tracks

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D4.7.1 selectionBGHexColor strings:137667 isSelectable = false INTENTIONAL_SKIP N/A Selection disabled
D4.7.2 searchHighlightConfig strings:137667 (none) MISSING High Two-track architecture — blocks D9
D4.7.3 _focusedSearchMatchPolygon + _searchMatchPolygons strings:391638 (none) MISSING Low D9 prereq
D4.7.4 _regionsOfInterestPolygons strings (none) MISSING Low PDF polish

D4.8 — SpeechTextRenderer & cursor style

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D4.8.1 SpeechTextRenderer strings:131692 (folded into ReaderViewModel.wireTTS) MISSING Low
D4.8.2 cursorStyleSubject: CurrentValueSubject<CursorStyle> strings:385374; class-dump-readers:58 (none — fixed style) MISSING Medium Underline/fill/karaoke style picker missing
D4.8.3 _isSentenceHighlightEnabled (component iVar) strings:385376; class-dump-readers:59 (none) MISSING Medium Component shadow of D4.5.5

D4 summary

Severity Count Top items
Critical 0
High 3 D4.5.1/D4.5.2 currentHighlightStatePublisher (Wave 1 Bug D); D4.7.2 searchHighlightConfig (blocks D9)
Medium 9 wordPolygonVerticalEdges; highlightMixBlendMode; hardcoded colors; no interactor abstraction; HighlightPosition; isSentenceHighlightEnabled; CursorStyle
Low 11 paragraph polygon tier; CGPolygon struct; sections; uniform corner radius; UserHighlight system (D8); search polygon iVars; regions-of-interest; SpeechTextRenderer
Intentional skip 5 hover; Epub vertical/horizontal; Kindle; AudioBrowser; selection BG

D4 wave assignment proposal:

  • Wave 1 Bug D follow-up — D4.5.1/D4.5.2: dedicated CurrentValueSubject<HighlightState> decoupled from cursor
  • Wave 3 polish — D4.3.2/D4.3.3 theme-aware tokens; D4.3.1 blend mode
  • Wave 5 UI — D4.5.5/D4.5.6 sentence toggle; D4.8.2 CursorStyle picker
  • Wave 6 geometry — D4.2.1 line-boundary flat caps
  • Wave 7 PDF — D4.4.1/D4.4.6 HighlightInteractor protocol
  • Wave 9 / D9 — D4.7.2/D4.7.3 two-track search renderer
  • D8 — D4.6.1–D4.6.7 UserHighlight system

D5 — TTS Engine + Voice

Speechify role: Drive text-to-speech for any reading bundle — on-device via SSDKLocalSpeechSynthesisAdapter or cloud via SSDKLocalStreamingMediaPlayer — with word-level alignment (SSDKSpeechMarks), typed voice catalog (SSDKVoiceSpec sealed hierarchy), and an assembly layer (SSDKListeningBundler / SSDKListeningExperience) that wires TTS+playback+alignment.

Key recon files: recon/ssdk-full-pipeline.md §D5 – §D7, recon/speechify-strings.txt:138095–138277,138896–138903,139039–139082,139468–139469, recon/speechify-recon-and-comparison.md §Pipeline C.

D5.1 — TTS abstraction protocol

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D5.1.1 SSDKLocalSpeechSynthesisAdapter strings:138095 TTSService protocol DONE Single protocol covers local + cloud
D5.1.2 SSDKLocalSpeechSynthesisPlayer strings:138108 AVSpeechTTSService.swift DONE AVSpeechSynthesizer + delegate; Wave 1 Bug D race fixed
D5.1.3 SSDKLocalStreamingMediaPlayer (cloud streaming, 3 read modes) strings:138132-138133 AudioStreamingMode enum + CloudTTSService mode parameter PARTIAL High Wave 5+ — enum scaffolded with 3 cases matching SSDK read-modes (.fullFetch / .chunked / .range). Only .fullFetch implemented. .chunked blocked on backend (probe confirmed api.taplisten.com returns JSON with full Content-Length, no Transfer-Encoding: chunked). .range defers to Wave 7+ (audiobook content). precondition(mode.isImplemented) early-fails on unsupported modes
D5.1.4 SSDKOnDeviceMediaSynthesisAdapter (neural TTS) strings:138153-138154 (none) INTENTIONAL_SKIP N/A Tier 3
D5.1.5 SSDKSynthesisLocation (.device/.server/.auto) strings:138277 (none — hardcoded per impl class) MISSING Medium No auto-routing to cloud when backend configured
D5.1.6 SSDKLocalSpeechSynthesisEvent (5 cases) strings:138097–138107 5 closure callbacks PARTIAL Low Closures vs sealed event type
D5.1.7 SSDKOnDeviceSynthesisSupport strings:138217–138223 (none) MISSING Low No device capability check

D5.2 — Speech marks / alignment data model

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D5.2.1 SSDKSpeechMarks + Impl strings:138258, 138264 SpeechMarksAdapter.swift (static mapping) PARTIAL High Stateless adapter vs persistent object. Alignment re-derived each chunk
D5.2.2 SSDKSpeechMarksChunk strings:138261 [TTSAlignmentItem] keyed per TTSChunk PARTIAL Medium Plain array vs typed chunk; discarded after play
D5.2.3 SSDKSentenceAndWordLocation (sentenceIdx + wordIdx + range) strings:138354 TTSWordEvent(sequence, blockIndex, nsRangeInBlock, word) PARTIAL High No sentence-level granularity. Critical for sentence highlight (D4) and sentence-level skip
D5.2.4 SSDKWordAndSentenceOverlay + OverlayHelper (tick-driven) strings:139040, 139042 ReaderViewModel.wireTTS() inline closures + BlockTextView PARTIAL Medium No dedicated overlay type; tightly coupled
D5.2.5 SSDKHighlightedWordPosition (.InFocus/.Left/.Right) strings:139039, 139627–139631 (none) MISSING Medium No viewport-relative position type for auto-scroll
D5.2.6 SSDKHighlightMixBlendMode config recon-comparison §6 (none — solid fill) MISSING Low See D4.3.1
D5.2.7 Sentence + paragraph alignment marks (SSDKSentenceAndWordLocation) Word-level only PARTIAL High Same as D5.2.3
D5.2.8 50ms tick rate Pipeline C 50ms Timer in CloudTTSService DONE Matches

D5.3 — Voice catalog and voice spec

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D5.3.1 SSDKVoiceSpec (sealed: 5 types — Local/Available, Local/SynthesisBacked, Static, VMS/FromAudioServer, VMS/Persisted) strings:138292–138311 VoiceInfo (local) + CloudVoice (cloud) — flat structs PARTIAL High Type information erased; play() accepts voiceID: String? only
D5.3.2 SSDKVoiceGender enum strings:138286 CloudVoice.gender: String? PARTIAL Low Untyped string
D5.3.3 SSDKVoiceMetadata (rich) strings:138287–138291 VoiceInfo.quality: String PARTIAL Medium Missing accent, style, sample-rate, engine ID
D5.3.4 SSDKVoiceRef (opaque handle) strings:138118 String PARTIAL Low Plain string IDs
D5.3.5 SSDKVoicePreferences (defaultPremium/Free/Offline) strings:138903 PreferencesService.ttsVoiceID + ttsRate PARTIAL High Single voice ID; no premium/offline split, no per-language
D5.3.6 SSDKVoicePreferenceWithStaticValue (fallback chain) strings:138900–138901 (none) MISSING Medium If voice unavailable, silently falls back without notification
D5.3.7 SSDKLocalSpeechSynthesisVoicesProvider strings:138096 AVSpeechTTSService.availableVoices(languageCode:) DONE
D5.3.8 Voice preview URL / sample playback strings:415710 CloudVoice.previewURL (stored, not surfaced) PARTIAL Medium VoicePickerView does not expose play-preview button
D5.3.9 availableVoices cloud catalog strings:388079 BackendAPIClient.listVoices() (stub throws) SCAFFOLDED High Real VMS integration absent. Hardcoded "Backend not configured" status
D5.3.10 SSDKLocalSynthesisOptions (pitch, rate, delays) strings:138112 AVSpeechTTSService.mappedRate + hardcoded pitch/delay PARTIAL Medium Pitch and delays not configurable

D5.4 — Voice rate calibration

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D5.4.1 Per-voice WPM calibration Pipeline C: speedInWordsPerMinute; strings:418709, 418244, 387951 mappedRate linear PARTIAL Medium Perceived speed varies by voice
D5.4.2 currentSpeedFactor + currentVoice observable strings:387951-387952 PreferencesService.ttsRate + ttsVoiceID DONE Same role; SSDK additionally stores WPM

D5.5 — Chunk planning and audio chunking

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D5.5.1 Text chunk boundary algorithm Pipeline C; strings:136981–136990 ChunkPlanner (1500-char ceiling) PARTIAL Medium Fixed ceiling; no sentence-boundary awareness, no network-adaptive
D5.5.2 SSDKAudioStreamingStrategy strings:137918–137926 AudioStreamingStrategy enum (.always / .auto(lookahead:) / .never) — wired into ListeningBundler DONE Wave 5 — enum drives prefetch state machine. Default .auto(lookahead: 2) keeps queue saturated during playback
D5.5.3 Audio cache (per-voice/rate/text-hash) Pipeline §4.4 AudioCache.swift SHA-256-keyed disk cache DONE Solid implementation. SSDK additionally has in-memory layer
D5.5.4 Gapless / pre-buffered next-chunk Pipeline C ListeningBundle.audio(at:) + GaplessAudioPlayer.preloadNext(data:) DONE Wave 5 — pipeline: bundle.audio(at:) awaits prefetch (lookahead=2 ahead) → player.preloadNext schedules next file in AVAudioPlayerNode queue → handleSegmentFinished promotes nextFile when current ends → 0ms transition. Wave 6 client-side prewarm cuts cold-start ~500-900ms

D5.6 — ListeningBundler / ListeningExperience assembly

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D5.6.1 SSDKListeningBundler (assembles AudioConfig + voices + prefs + options) strings:138896 ListeningBundler class + ListeningBundle runtime in Core/Services/CloudTTS/ListeningBundler.swift DONE Wave 5 — bundler.bundle(blocks:voice:) returns ListeningBundle owning [Int: Task]. Strategy-driven prefetch state machine. CloudTTSService delegates fully — no longer inline in ReaderViewModel
D5.6.2 SSDKListeningBundlerConfig/Options strings:138898–138899 AudioStreamingStrategy + AudioStreamingMode parameters on ListeningBundler.init and CloudTTSService.init PARTIAL Low Wave 5 — strategy + mode are explicit init params (typed, default-friendly). Not wrapped in single config struct; each consumer picks defaults. Acceptable for Tier 1
D5.6.3 SSDKListeningExperience (transferable session) strings:139468–139469 (none — TTS lives in ReaderViewModel) MISSING Medium No transferable session object
D5.6.4 SSDKListeningBundle (output of bundler) strings:138893 (none) MISSING Low
D5.6.5 listeningBundlerConfig as sub-config strings:136917-136919 (none) MISSING Low Composition layer absent

D5.7 — Playback control events and state

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D5.7.1 SSDKAudioControllerEvent (10 cases) strings:138191–138211 PlaybackState (6 cases) PARTIAL Medium Missing Seeking, ChangedVoice, ChangedVolume, Buffering detail, Destroyed
D5.7.2 pause/resume/seekTo(cursor:) symmetry strings:136288-136366 pause + resume present; no seekTo — replaced by stop+replay PARTIAL High Word tap restarts TTS. Long-form gap
D5.7.3 SSDKWordsListenedHelper (analytics) strings:139058–139064 (none) INTENTIONAL_SKIP N/A Analytics anti-goal
D5.7.4 SSDKPlaybackControls + SSDKPlayPauseButton (6 sub-states) strings:139076–139078 PlaybackStateMiniPlayer DONE We collapse to fewer states
D5.7.5 SSDKPlaybackTime (NotReady/Ready) strings:139079–139082 .loading for both fetch and decode MISSING Low No timing-readiness signal
D5.7.6 SSDKNavigableSpeechProvider strings:138357 (none) MISSING Medium No navigation while preserving stream

D5.8 — Multi-provider routing and plugin

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D5.8.1 SSDKVoiceSpecVMSVoiceSpec (cloud provider abstraction) strings:138305, 138310–138312 BackendAPIClient protocol + Stub + HTTP impls PARTIAL Medium Right shape but only one invented contract; no voice-selection routing
D5.8.2 voiceFactory injection strings:136923, 443879 (none) MISSING Medium No factory pattern
D5.8.3 SSDKDocumentVoiceInfo (per-doc voice override) strings:138893 (none — global) MISSING Medium One global voice for all docs
D5.8.4 Cloud synthesize endpoint contract Pipeline C HTTPBackendAPIClient.synthesize (invented contract) PARTIAL High Fabricated contract — incompatible with ElevenLabs/Google/Azure/Speechify. No URL-based audio streaming

D5 summary

Severity Count Top items
Critical 0
High 6 D5.1.3 chunked streaming (backend-gated); D5.2.3/D5.2.7 no sentence alignment; D5.3.1 voice spec erasure; D5.3.5 flat preferences; D5.3.9 cloud catalog scaffolded; D5.7.2 no seekTo; D5.8.4 invented contract — Wave 5 closed: D5.5.2, D5.5.4, D5.6.1
Medium 13 synthesis-location auto-routing; overlay helper; viewport position type; voice metadata; preference fallback chain; preview button; rate WPM calibration; chunk ceiling; ListeningBundlerConfig; transferable experience; missing events; NavigableSpeechProvider; multi-provider routing; voice factory; per-doc voice
Low 5 closures vs sealed event; device capability check; blend mode; typed VoiceRef; PlaybackTime.NotReady
Intentional skip 2 on-device neural TTS; words-listened analytics

D5 wave assignment proposal:

  • Wave 5 (TTS quality) — DONE: D5.5.2 (AudioStreamingStrategy), D5.5.4 (gapless prefetch via ListeningBundler), D5.6.1 (ListeningBundler), D2.5.4 (PreparedBlock), D2.5.5 (PreparedAudio); REMAINING: D5.7.2 (in-chunk seekTo for cloud)
  • Wave 6 (voice catalog) — D5.3.8 (preview playback), D5.3.5 (structured prefs with fallback), D5.3.9 (real backend), D5.8.4 (real contract)
  • Wave 7 (alignment) — D5.2.3 + D5.2.7 (sentence-level), D5.2.5 (viewport position type)
  • Wave 9 (architecture) — D5.6.1 + D5.6.2 + D5.6.3 (extract ListeningBundler), D5.1.5 (auto-routing)
  • Backlog — D5.8.3 (per-doc voice), D5.4.1 (WPM calibration), D5.3.1 (typed VoiceSpec)

D6 — Playback / Audio session / CarPlay

Speechify role: Master audio orchestration — audio session lifecycle, OS-level integrations (lock screen, Control Center, headphone remote), interruption handling, Now Playing metadata, platform extensions (CarPlay). Entry: SSDKAudioController; supporting: audioSessionManager, nowPlayingPublisher, nowPlayingCenter, routeChangeObserver, interruptionStream, sleepTimerService.

Key recon files: recon/speechify-strings.txt:384751,412750,415078,440518,450501-505,457353-358,412372-373,418202-204,441477,385250, recon/ssdk-full-pipeline.md §D4, recon/strings-all.txt:63677,67175.

D6.1 — Audio session category & activation

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D6.1.1 audioSessionManager (dedicated class) strings:384751,440519 AudioSessionManager.swift DONE .playback + .spokenAudio + .duckOthers; activated/deactivated correctly
D6.1.2 Category/mode/options strings:133122-133126 matches DONE
D6.1.3 UIBackgroundModes: audio Info.plist key (App Store requirement) Info.plist at project root with UIBackgroundModes [audio] DONE Wave 10 — verified via plutil -p on built bundle
D6.1.4 Session deactivation with .notifyOthersOnDeactivation (inferred) correct DONE
D6.1.5 audioSessionSetupFailed error event strings:412708,412930 print(...) only PARTIAL Medium Errors swallowed; should call onError

D6.2 — Audio engine (gapless vs gapped)

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D6.2.1 AVAudioEngine + AVAudioPlayerNode (on-device gapless) strings:241690-691; scheduleBuffer at 453654 GaplessAudioPlayer in Core/Services/CloudTTS/GaplessAudioPlayer.swift DONE Wave 5 — full chain: engine.attach(playerNode) → timePitch → mainMixerNode. scheduleFile(file, at: nil, completionCallbackType: .dataPlayedBack) for both current + preloaded next. callbackGeneration / preloadGeneration counters guard stale completions. AVAudioUnitTimePitch.rate for variable-speed (avoids AVAudioPlayer enableRate MP3 hardware-decoder glitches). Wave 6 added prepareEngine() for app-launch pre-warm. Still used only by CloudTTSService — local AVSpeechTTSService keeps direct AVSpeechSynthesizer
D6.2.2 SSDKLocalStreamingMediaPlayer (cloud chunked HLS/MP3) Pipeline C CloudTTSService + ListeningBundler lookahead prefetch PARTIAL Medium Wave 5 — chunk boundary gap eliminated via prefetch lookahead=2. True HTTP chunked streaming (AudioStreamingMode.chunked) blocked on backend; see D5.1.3
D6.2.3 SSDKAudioStreamingStrategy (Always/Auto/Never) §D4 AudioStreamingStrategy enum on ListeningBundler.init DONE Wave 5 — see D5.5.2. Same enum drives both cloud TTS prefetch (D5.5.2) and audio session prefetch behavior (D6.2.3)

D6.3 — Interruption handling

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D6.3.1 interruptionStream / interruptionContinuation strings:415160-162,448045-049 PlaybackCoordinator.handleInterruption in Core/Services/PlaybackCoordinator.swift DONE Wave 10 — observes AVAudioSession.interruptionNotification, pauses on .began, auto-resumes on .ended only when shouldResume flag set
D6.3.2 interruptionResumeEnabled (honour shouldResume hint) strings:418282,448047 PlaybackCoordinator.handleInterruption checks AVAudioSession.InterruptionOptions.shouldResume DONE Wave 10
D6.3.3 audioSessionInterrupted: legacy ObjC selector strings:440518 (none) MISSING Low Pre-iOS-15 path

D6.4 — Route change / headphone disconnect

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D6.4.1 routeChangeObserver (AVAudioSession.routeChangeNotification) strings:412750-852,415161-163 PlaybackCoordinator.handleRouteChange DONE Wave 10 — pauses on oldDeviceUnavailable (headphone unplug, AirPods removal)
D6.4.2 NAVAudioSessionRouteChangeReason typed enum strings:132092 (none) MISSING Medium Minor — raw constants work
D6.4.3 AirPlay / Bluetooth routing (implicit via category) implicit (untested without background mode plist) PARTIAL Low Document in integration guide

D6.5 — Now Playing Info (lock screen / Control Center)

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D6.5.1 nowPlayingCenter / MPNowPlayingInfoCenter.default() strings:387873,450501-505 PlaybackCoordinator.updateNowPlayingInfo(...) DONE Wave 10import MediaPlayer, populates MPNowPlayingInfoCenter.default().nowPlayingInfo
D6.5.2 MPMediaItemPropertyTitle strings:450503 wired from document.title DONE Wave 10
D6.5.3 MPMediaItemPropertyArtist (inferred) wired from document.author DONE Wave 10
D6.5.4 MPMediaItemPropertyArtwork (inferred) Document.coverImageDataUIImageMPMediaItemArtwork(boundsSize:requestHandler:) DONE Wave 10
D6.5.5 MPNowPlayingInfoPropertyElapsedPlaybackTime strings:450503 computed from absoluteCharOffsetForCursor() / charsPerSecond DONE Wave 10 — char-offset estimate; coarse but functional
D6.5.6 MPNowPlayingInfoPropertyPlaybackRate strings:456027 set to viewModel.rate when playing, 0.0 when paused DONE Wave 10
D6.5.7 Periodic update of nowPlaying (inferred) ReaderViewModel.refreshNowPlaying() called on every word event + state change DONE Wave 10 — sub-second granularity (every word)

D6.6 — Remote Command Center (headphone / lock / AirPods)

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D6.6.1 playCommand / pauseCommand / togglePlayPauseCommand strings:451507,451314,418202-204 PlaybackCoordinator.registerRemoteCommands DONE Wave 10 — all 3 wired via MPRemoteCommandCenter.shared() to delegate?.coordinatorDidRequestPlay/Pause/Toggle
D6.6.2 skipForwardCommand (preferred 30s) strings:412372-373,457353-357 wired with preferredIntervals = [30] DONE Wave 10 — converts to char-offset delta via 200 WPM × rate
D6.6.3 skipBackwardCommand (preferred 15s) strings:457354 wired with preferredIntervals = [15] DONE Wave 10
D6.6.4 nextTrackCommand / previousTrackCommand strings:450386,451915 both .isEnabled = false DONE Wave 10 — explicitly disabled rather than silently ignoring
D6.6.5 changePlaybackPositionCommand (lock-screen scrubber seek) strings:441477 wired to seekToTime(_:) DONE Wave 10 — char-offset estimate maps position to block + offset
D6.6.6 playCommandTask (async back-pressure) strings:420892,430976 (none) MISSING Low Implementation detail

D6.7 — Sleep timer

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D6.7.1 sleepTimerService / Interactor / Configurator strings:385786,457364,163213 (none) MISSING Low Wave 7+
D6.7.2 sleepTimerStateObserver / sleepTimerRemainingTime strings:388047,438825 (none) MISSING Low Depends on D6.7.1

D6.8 — Mini-player UI parity

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D6.8.1 Play / Pause / Stop (inferred) MiniPlayer.swift DONE
D6.8.2 Speed slider (rate control) ChangedSpeedFactor event continuous 0.3–1.0 slider PARTIAL Low Speechify uses discrete WPM steps (1x/1.5x/2x/3x/4.5x); we cap at 1.0x
D6.8.3 Voice picker button ChangedVoice event voice button → VoicePickerView DONE
D6.8.4 Buffering indicator Buffering event hourglass.circle.fill during .loading DONE
D6.8.5 Progress bar / scrubber (inferred from full mini-player) (none) MISSING Medium Add ProgressView(value:total:) tied to progressFraction
D6.8.6 Chapter / block auto-advance (sequential streaming) speakNext() / playNextChunk() DONE
D6.8.7 Seek-by-tap-on-word NavigationIntent tapWord → playFromCursor DONE Wave 1

D6.9 — CarPlay

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D6.9.1 CPListItem, CPListTemplate, CPNowPlayingTemplate strings-all:63677,67175 (none) INTENTIONAL_SKIP N/A Anti-goal. Document for host apps
D6.9.2 CPApplicationDelegate / CPInterfaceController (inferred) (none) INTENTIONAL_SKIP N/A

D6 summary (after Wave 5/6/10)

Severity Count Top items
Critical 0 All 4 closed in Wave 10 P0
High 2 shouldResume flag (still missing); changePlaybackPositionCommand (still missing) — Wave 5 closed: D6.2.1 gapless engine, D6.2.2 cloud prefetch
Medium 4 session error swallowed; route-change reason enum; artwork; mini-player skip-back/scrubber polish
Low 6 session option; legacy ObjC interruption; AirPlay/BT untested; playback rate; nextTrack/previousTrack disabled; sleep timer
Intentional skip 2 CarPlay templates

D6 wave assignment proposal:

  • Wave 10 P0 — DONE ✓ — D6.1.3 (UIBackgroundModes), D6.3.1+D6.3.2 (interruption), D6.4.1 (route-change), D6.6.1+D6.6.2+D6.6.3 (RemoteCommandCenter), D6.5.1-D6.5.7 (Now Playing wiring)
  • Wave 5 — DONE ✓ — D6.2.1 (AVAudioEngine + AVAudioPlayerNode via GaplessAudioPlayer), D6.2.2 (cloud prefetch via ListeningBundler), D6.2.3 (AudioStreamingStrategy)
  • Wave 6 — DONE ✓ — first-byte client-side optimization (voice catalog disk cache + engine pre-warm + warmUp())
  • Wave 6.A — DONE ✓ — MiniPlayer state-aware loading + progress UI; ReaderViewModel state transition fix
  • Backlog — D6.6.5 changePlaybackPositionCommand (lock-screen scrubber seek), sleep timer, AirPlay surface, mini-player skip-back 30s

D7 — Persistence & Library

Speechify role: Local persistence via KMP-shared SQLite (GRDB adapter, SSDKDatabaseManager), cloud sync via Firebase Firestore (10 syncers per entity), library browsing (SSDKLibraryItem sealed), full-text search, archive management.

Key recon files: recon/speechify-strings.txt:122808,123247,135373,136725-136728,138638-138705,139048,139114-139247,139312-139545, recon/ssdk-full-pipeline.md §D10, §D11, Pipeline A/G.

D7.1 — Storage technology

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D7.1.1 SSDKDatabase + SSDKDatabaseManager (KMP SQLite via GRDB) strings:139114,139201; class-dump:123247 SwiftData modelContainer(for:) PARTIAL Low iOS 17+; same guarantees for our single-platform target
D7.1.2 SSDKDatabaseQuery DSL (9 Where variants) strings:139203-139228 FetchDescriptor + #Predicate PARTIAL Low Missing compound-key cursor queries
D7.1.3 SSDKDatabaseTransaction + WriteIntent strings:139247,139231-139234 modelContext.save() DONE
D7.1.4 SSDKDatabaseTableIndexDefinitionFullTextSearch strings:139244-139246 (none) MISSING Medium No FTS; library search must load all docs into memory
D7.1.5 Schema migration (DatabaseMigrator) strings:135562,137337 (none — SwiftData v1, no VersionedSchema) MISSING High Tech-debt H5. Any field rename triggers destructive migration

D7.2 — Library item model

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D7.2.1 SSDKLibraryItem (sealed: Content/Folder) strings:138672-138674 flat [Document] PARTIAL Medium No folder concept
D7.2.2 SSDKLibraryItemFolder strings:138674 (none) MISSING Medium Tier 2
D7.2.3 SSDKFolderQuery + SSDKFolderReference strings:138659-138668 (none) MISSING Low Folder DSL
D7.2.4 SSDKContentType strings:138646-138647 DocumentFormat DONE
D7.2.5 SSDKListeningProgress strings:139048-139049 lastReadCharOffset + lastReadBlockOrderIndex + lastReadCharOffsetInBlock DONE Wave 2 stable cursor

D7.3 — Filter and sort

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D7.3.1 SSDKFilterAndSortOptions strings:138649 (none — fixed sort) MISSING Medium Hardcoded lastReadDate DESC, importDate DESC
D7.3.2 SSDKFilterTypeANY/RECORDS/FOLDERS/ARCHIVED strings:138652-138658 (none) MISSING Medium No filter UI
D7.3.3 7 sort variants (RECENTLY_LISTENED, DATE_ADDED, ALPHABETICAL, LISTENING_PROGRESS, DATE_ARCHIVED) strings:138693-138703 2 sort keys hardcoded PARTIAL Low No sort picker
D7.3.4 lastListenedAt vs lastUpdatedAt distinction strings:136725 lastReadDate (collapsed) PARTIAL Low

D7.4 — Archived state

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D7.4.1 isArchived strings:136727 (none — delete only) MISSING Medium Hard-delete on swipe; no recovery
D7.4.2 Bulk archive ops strings:136727-136728,136686-136687 (none) MISSING Low Depends on D7.4.1
D7.4.3 SortByDATE_ARCHIVED strings:138699 (none) MISSING Low

D7.5 — Bookmarks

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D7.5.1 SSDKBookmark strings:138638 Bookmark @Model DONE id/createdAt/blockOrderIndex/charOffset/note?/snippet
D7.5.2 SSDKBookmarkHelper (CRUD service) strings:139439 inline modelContext.delete in BookmarksSheet PARTIAL Low Refactor into BookmarkService in Wave 8
D7.5.3 SSDKBookmarksList + Item strings:139440-139441 document.bookmarks sorted in view DONE
D7.5.4 BookmarkActionState (HasInViewport/NoneInViewport) strings:139435-139438 (none — always Add) MISSING Low Users may create duplicates at same block

D7.6 — User highlights and notes (D8 territory)

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D7.6.1 SSDKUserHighlight + Coordinates + Style strings:138708-138716,139622-139623 (none) MISSING High Range-based annotations; D8 scope. Cross-link D4.6
D7.6.2 SSDKUserHighlightsHelper + observer strings:139542-139545 (none) MISSING High No CRUD service
D7.6.3 Note text on highlight (D9 inferred) Bookmark.note: String? PARTIAL Medium Notes only on point bookmarks, not ranges

D7.7 — Import status persistence

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D7.7.1 SSDKImportStatus (Finished/Uploading) strings:138669-138671 (in-memory ImportState only) MISSING Low Acceptable for offline-first. Cross-link D1.8.13
# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D7.8.1 SSDKSearchHelper + Query + State strings:139500-139507 (none) MISSING Medium Tier 2. Cross-link D7.1.4 + D9

D7.9 — Cover image strategy

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D7.9.1 SSDKBookThumbnailsHelper (CDN URLs) strings:139640-139643 Document.coverImageData: Data? (JPEG embedded in SwiftData) PARTIAL Low Works offline; bloats store. Move to file-backed in Wave 8

D7.10 — Per-document settings

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D7.10.1 SSDKVoicePreference (per-doc) strings:138900-138903 global PreferencesService.ttsVoiceID MISSING Low All docs share one voice
D7.10.2 Per-doc speed/text-size (inferred) global only MISSING Low Same gap

D7.11 — File hash dedup

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D7.11.1 (no explicit symbol found) Document.fileHash SHA-256 + #Predicate check DONE Positive differentiator

D7.12 — iCloud / CloudKit

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D7.12.1 SSDKSyncProvider (10 syncers) + Firestore strings:139313-139331; pipeline G (none — local SwiftData only) INTENTIONAL_SKIP N/A Multi-device sync explicit anti-goal

D7.13 — Backup and file-URL integrity

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D7.13.1 Server-canonical blob key (sandbox-agnostic) Pipeline A Document.fileRelativePath resolved at runtime DONE Wave 10 — see D1.8.2
D7.13.2 OpenConnectionIsBlockingDatabaseMigration event strings:139198 (none) MISSING High Downstream of D7.1.5

D7 summary

Severity Count Top items
Critical 1 D7.13.1 absolute file URL (Bug B5, cross-links D1.8.2)
High 3 D7.1.5 no migration plan (H5); D7.6.1/D7.6.2 user highlights absent (D8); D7.13.2 no migration-blocking UI
Medium 5 folder hierarchy; filter/sort UI; archive (soft-delete); range notes; library search bar
Low 8 compound-key cursor; sort variants; lastListenedAt split; bulk archive; BookmarkHelper refactor; viewport bookmark state; import status persistence; cover image bloat; per-doc voice/speed
Intentional skip 1 CloudKit/multi-device sync
Done / positive 5 transactions; content type; listening progress cursor; bookmark model; SHA-256 dedup

D7 wave assignment proposal:

  • Wave 8 (annotations + library polish) — D7.4.1 archive, D7.5.2 BookmarkService, D7.9.1 cover → file-backed
  • Wave 8 prereq — D7.1.5 VersionedSchema + SchemaMigrationPlan
  • Wave 9 (search + filter UI) — D7.1.4 FTS, D7.8.1 search bar, D7.3.1/D7.3.2 filter+sort picker
  • P0 tech debt (Bug B5) — D7.13.1 relative file URL (paired with D1.8.2)
  • Tier 2 — D7.2.2 folders, D7.6.1 user highlights (D8), D7.10.1 per-doc voice

D15 — SDK Platform (DI, factories, plugins)

Speechify role: Platform scaffolding wiring SSDK services together — FactoryKit DI, config-driven bundler factory, plugin extension point, multi-service root client (SSDKSpeechifyClient), environment/telemetry/analytics infrastructure. Entry: SSDKSpeechifyClientFactorySSDKSpeechifyClient.

Key recon files: recon/speechify-strings.txt:127345-127360,137903-137948,138871-138875, recon/ssdk-full-pipeline.md §1.1, recon/ssdk-entry-points.txt.

D15.1 — Dependency injection architecture

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D15.1.1 FactoryKit (Container, SharedContainer, scopes) strings:127345-127352, 424522-424528 DependencyContainer singleton (lazy) PARTIAL Low No scope enforcement, no override-for-testing
D15.1.2 LazyInjected / WeakLazyInjected property wrappers strings:127359-127360, class-dump-render:201 (none — direct construction) MISSING Low Blocks constructor-injection unit testing
D15.1.3 Multi-level DI hierarchy (App/MainShell/Service/Listening/SDK) strings:123443,124244,124305,124362,123506; DI/SDK/SDKDIContainer.swift at 169596 Single flat singleton MISSING Low Adding per-screen lifetimes later requires rewrite
D15.1.4 Constructor injection vs service-locator mixing (FactoryKit Resolving) Mix: DependencyContainer.shared + @Environment(\.deps) PARTIAL Low One injection mechanism + one service-locator pull
D15.1.5 DI override seam for tests FactoryKit register mechanism (none — private init() singleton) MISSING Medium Tests cannot substitute mocks. Wave 9 candidate

D15.2 — Root client and environment

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D15.2.1 SSDKSpeechifyClient (root service object) strings:137945, §1.1 DependencyContainer (1 service) PARTIAL Low SSDK owns 7+ named services; we own 1. Will grow
D15.2.2 SSDKSpeechifyClientFactory strings:137948 (none — initialized unconditionally in App) MISSING Low No construction phase
D15.2.3 SSDKAppEnvironment (dev/staging/prod) + config strings:137909, 137948; selector at 136081 (none — single env) MISSING Low Add when staging/prod split needed
D15.2.4 SSDKSpeechifyClientServices (typed accessor bag) strings:137946 flat properties on container MISSING Low Equivalent at current scale
D15.2.5 SSDKAdaptersProvider (Firebase shim layer) strings:137903-137904 (none — no Firebase) INTENTIONAL_SKIP N/A Not applicable

D15.3 — Factory and plugin system

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D15.3.1 SSDKBundlerFactory strings:138871 (none — inline in ImportService) MISSING Low See D1.6.1
D15.3.2 SSDKBundlerFactoryConfig (composable) strings:138873 SkipConfig (5 flags only) PARTIAL Low Documented in D1.6.3
D15.3.3 SSDKBundlerPlugins + Companion strings:138874-138875 (none) MISSING Low Plugin layer for third-party hooks. Single-tenant — not needed for Tier 1

D15.4 — Service decomposition

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D15.4.1 SSDKDatabase* (KMP cross-platform mini-ORM) strings:139114-139225 SwiftData (iOS-native) PARTIAL Low No cross-platform portability — intentional
D15.4.2 SSDKLoggingService (structured logger) strings:138732 print(...) (1 call) MISSING Low Wave 9: replace with os.Logger
D15.4.3 SSDKTelemetryOptions / DiagnosticReporter strings:138743 (none) INTENTIONAL_SKIP N/A OSLog-only per plan
D15.4.4 20+ *Analytics protocols strings:123102, 122698, 127959 (none) INTENTIONAL_SKIP N/A Anti-goal
D15.4.5 Firebase Crashlytics strings:122885 (none) MISSING Low Revisit before broad rollout
D15.4.6 SSDKFirebaseAuthService strings:138011-138016 (none) INTENTIONAL_SKIP N/A D12 anti-goal
D15.4.7 SSDKFirebaseFirestoreService + Storage strings:138018-138030 (none) INTENTIONAL_SKIP N/A D11 anti-goal

D15.5 — Feature flags / A-B

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D15.5.1 SSDKNextGenConfig* (16 classes — Firebase Remote Config) §4.3 (none) INTENTIONAL_SKIP N/A No A/B requirement

D15.6 — Cloud provider registry (our addition)

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D15.6.1 (Speechify uses URLImportService + backend processing) CloudProviderRegistry.shared (Dropbox + GDrive registered) DONE Idiomatic; OneDrive etc. slot in without touching call sites

D15.7 — App wiring: URL scheme + App Groups

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D15.7.1 URL scheme registered in Info.plist (Speechify speechify://) Info.plist CFBundleURLTypes registered for taplisten scheme DONE Wave 10 — switched project to manual INFOPLIST_FILE; verified via plutil -p
D15.7.2 Deep link parser DeepLinkHandler.parse(_:) DONE Logic complete once D15.7.1 registered
D15.7.3 App Group entitlement (Speechify uses Firebase dynamic links) TapListenSDK.entitlements at project root with com.apple.security.application-groups = group.com.tap.listen.sdk.shared DONE Wave 10 — entitlement wired via CODE_SIGN_ENTITLEMENTS build setting. Note: Apple Developer Portal registration needed for paid-team device builds
D15.7.4 Environment propagation in App scene @Environment(coordinator) + @Environment(\.deps) + modelContainer DONE Clean SwiftUI scene setup

D15.8 — Module boundaries

# Speechify symbol Recon evidence Our equivalent Status Severity Notes
D15.8.1 Multi-module (SSDK / SpeechifyMobile / SpeechifyCommon / FactoryKit / SubscriptionsService) strings:169596,169751; class-dump-render:201 Single module TapListenSDK MISSING Low No API boundary enforcement. Add SPM local packages when team grows

D15 summary

Severity Count Top items
Critical 2 D15.7.1 URL scheme not in Info.plist (deep link dead); D15.7.3 App Group entitlement missing (SharedURLStore broken)
High 0
Medium 1 D15.1.5 no DI override seam (testability)
Low 8 FactoryKit vs singleton; LazyInjected; container hierarchy; mixed DI; root client/factory/env config gaps; bundler factory + plugins; structured logger; crash reporting; single-module
Intentional skip 7 adapter provider; telemetry; analytics; Crashlytics; Firebase Auth; Firestore/Storage; NextGenConfig

D15 wave assignment proposal:

  • Immediate / pre-ship (P0) — D15.7.1 (register taplisten://), D15.7.3 (create .entitlements with com.apple.security.application-groups)
  • Wave 9 (architectural refactor) — D15.1.5 constructor injection seam, D15.4.2 os.Logger, D15.3.1 BundlerFactory if D1.6.1 lands
  • Backlog — FactoryKit / scoped DI (only worthwhile if service count > 10), local SPM packages (team-growth concern)

Cross-domain aggregate

Aggregated severity across all 8 audited domains (D1 + D2 + D3 + D4 + D5 + D6 + D7 + D15).

Updated after Wave 5/6/6.A (2026-04-29).

Severity Open Done so far % done
Critical 0 8 100%
High ~17 ~14 ~45%
Medium ~52 ~13 ~20%
Low ~72 ~8 ~10%
Intentional skip (37 rows, n/a)

Wave 5/6/6.A closures (~9 High rows + ~4 Medium):

  • D2.5.4 PreparedBlock — Wave 5
  • D2.5.5 PreparedAudio — Wave 5
  • D5.5.2 AudioStreamingStrategy — Wave 5
  • D5.5.4 Gapless prefetch — Wave 5
  • D5.6.1 ListeningBundler — Wave 5
  • D6.2.1 AVAudioEngine + AVAudioPlayerNode — Wave 5
  • D6.2.2 Cloud chunk prefetch — Wave 5 (lookahead-based)
  • D6.2.3 AudioStreamingStrategy — Wave 5
  • D5.6.2 ListeningBundlerConfig — Wave 5 (partial — strategy + mode params)
  • D5.1.3 SSDKLocalStreamingMediaPlayer — Wave 5+ (AudioStreamingMode enum scaffold; .fullFetch DONE; .chunked backend-gated)
  • D3.8.4 MiniPlayer status header + progress bar — Wave 6.A
  • Wave 6 client-side prewarm — voice catalog disk cache + engine pre-warm + warmUp() — first-play ~500-900 ms saved (no specific Speechify symbol; client-only optimization)

Critical-path Wave 10 — ALL CLOSED ✓

Row Item Status
D6.1.3 UIBackgroundModes: audio ✓ DONE
D6.3.1 AVAudioSession.interruptionNotification handler ✓ DONE
D6.3.2 shouldResume flag handling ✓ DONE
D6.4.1 routeChangeNotification handler ✓ DONE
D6.5.1-7 MPNowPlayingInfoCenter (title, author, artwork, elapsed, rate, periodic) ✓ DONE (all 7 sub-items)
D6.6.1 playCommand / pauseCommand / togglePlayPauseCommand ✓ DONE
D6.6.2-3 skipForwardCommand / skipBackwardCommand ✓ DONE
D6.6.4 nextTrackCommand / previousTrackCommand (disabled cleanly) ✓ DONE
D6.6.5 changePlaybackPositionCommand ✓ DONE
D15.7.1 taplisten:// URL scheme registration ✓ DONE
D15.7.3 App Group entitlement ✓ DONE
D1.8.2 / D7.13.1 Relative file URL in Document ✓ DONE

Highest-priority remaining work

Wave Open Critical/High items
Wave 4 D1.3.5 + D1.5.4 + D2.4.4/6/9 (markdown SPM swap + skip flags)
Wave 5 D5.5.4 (gapless prefetch) + D5.7.2 (true seek) + D4.5.5 (sentence toggle) + D3.2.6 (table block)
Wave 6 D5.3.5 + D5.3.9 + D5.8.4 (real backend wiring) + D3.4.1-3 (inline element tree)
Wave 7 D5.2.3 (sentence alignment) + D3.6.10 (LazyContentProvider)
Wave 8 D7.1.5 (SchemaMigrationPlan — bomb hẹn giờ) + UserHighlight system
Wave 9 D1.6.1 + D2.2.3 (Bundler stage) + D5.6.1-3 (ListeningBundler) + D7.1.4 (FTS)

Tech-debt outside the matrix (still open)

  • B4 Share Extension target chưa tạo (high)
  • H2 Regex HTML parser (high) — Wave 2 partial fix only
  • H3 DOCX <w:t>-only extraction (high)
  • H4 EPUB no NCX TOC parsing (high)
  • H7 AudioCache no LRU/size cap (medium)
  • 0 unit tests, 0 UI tests, no CI (high — testing infrastructure)
  • Localization: hardcoded English UI strings (medium)
  • Apple Developer Portal: paid team + App Group registration + provisioning profiles (high)
  • Accessibility: VoiceOver labels, Dynamic Type respect (high)

Wave proposals (consolidated across domains)

Wave Focus Domain rows
Wave 4 Markdown via swift-markdown SPM + footnote/citation/URL skip D1.3.5, D1.5.4, D2.4.4, D2.4.6, D2.4.9
Wave 5 ✓ Cloud TTS strict Speechify pattern (ListeningBundler + GaplessAudioPlayer + PreparedBlock/Audio + AudioStreamingStrategy) D2.5.4, D2.5.5, D5.5.2, D5.5.4, D5.6.1, D5.6.2, D6.2.1, D6.2.2, D6.2.3
Wave 6 ✓ First-byte latency client-side optimization (voice catalog disk cache + engine pre-warm + warmUp + AudioStreamingMode enum) D5.1.3 (partial), Wave 6 client-side gains
Wave 6.A ✓ MiniPlayer state-aware progress UI + state transition fix D3.8.4 (improved)
Wave 5 remaining Sentence highlight toggle, cursor style, tables, true in-chunk seekTo D5.7.2, D4.5.5, D4.8.2, D3.2.6, D3.3.9, D6.6.5, D6.8.5
Wave 6 Voice catalog (preview + structured prefs + real backend) + inline element tree + long-press + audio quality D5.3.5, D5.3.8, D5.3.9, D5.8.4, D3.4.1-3, D3.6.7, D3.6.9, D6.2.x, D4.2.1
Wave 7 Sentence alignment + LazyContentProvider + PDF reader (HighlightInteractor protocol) D5.2.3, D5.2.7, D5.2.5, D3.6.10, D4.4.1, D4.4.6
Wave 8 Annotations + library polish + schema migration D7.4.1, D7.5.2, D7.9.1, D7.1.5, D8 (UserHighlight system from D4.6)
Wave 9 Architectural refactor (ContentBundler stage + DI seam + structured logger) + library search + filter UI D1.6.1, D2.2.3, D2.3.1, D15.1.5, D15.4.2, D7.1.4, D7.8.1, D7.3.1, D4.7.2 (D5.6.1 already done in Wave 5)
Wave 10 (P0) App Store unblock — Critical row 1-7 above (see Critical list)
Backlog Per-doc voice, WPM calibration, sleep timer, full-screen toggle, etc. many Low rows
Tier 2 D8 search match navigation, in-doc search UI, EPUB native reader (if rev'd) D3.6.3, D3.8.5, D3.8.6

Update protocol

When a wave lands or a gap is discovered:

  1. Update the relevant row's Status column.
  2. If status flipped to DONE, add the wave/commit reference in Notes.
  3. Re-run severity counts in domain summary table.
  4. Cross-link from plans/260429-1908-bookcast-tts-reader/plan.md Implementation Status section.

When adding a new row:

  1. Always cite recon evidence (file:line or section).
  2. Always specify our equivalent path (or explicit (none)).
  3. Severity is judged from a Vietnamese-user-shipping-Tier-1 perspective, not Speechify-feature-completeness perspective.