{"path":"NexusReader.Infrastructure/Services/EpubReaderService.cs","purpose":"Service that locates an EPUB file for an ebook in the DB, reads and parses a chapter into content blocks (text segments and AI-trigger blocks) for the reader UI.","classification":{"role":"service","layer":"infrastructure","confidence":0.9,"evidence":["Service naming pattern","Application/service path heuristic","namespace NexusReader.Infrastructure.Services","implements IEpubReader and reads from AppDbContext","uses EPUB parsing and builds ReaderPageViewModel"]},"className":"EpubReaderService","methods":[{"name":"EpubReaderService","line":21,"endLine":27,"signature":"(dbContextFactory: IDbContextFactory, logger: ILogger) -> EpubReaderService","purpose":"Constructor that injects the DB context factory and logger.","calls":[],"actions":[{"id":"dependency-injection_25","kind":"mapping","label":"stores dependencies","line":25,"detail":"_dbContextFactory and _logger assigned","visibility":"detail-only","confidence":0.7}]},{"name":"GetEpubContentAsync","line":30,"endLine":118,"signature":"(ebookId: Guid, chapterIndex: int, userId: string?) -> Task>","purpose":"Main runtime method: resolves the ebook file via DB, parses an EPUB, extracts chapter text into content blocks, inserts AI triggers based on word threshold, and returns a ReaderPageViewModel or a failure result.","calls":[{"targetFile":"self","targetMethod":"ResolvePath","callLine":54,"paramSummary":"ebook.FilePath (web-relative path)"},{"targetFile":"self","targetMethod":"FindTitleInNavigation","callLine":77,"paramSummary":"navigation, chapterRef.FilePath"},{"targetFile":"self","targetMethod":"ExtractParagraphs","callLine":88,"paramSummary":"chapterContent (HTML string)"},{"targetFile":"self","targetMethod":"SanitizeParagraph","callLine":91,"paramSummary":"paragraph HTML"},{"targetFile":"self","targetMethod":"CountWords","callLine":96,"paramSummary":"sanitized paragraph text"},{"targetFile":"self","targetMethod":"CreateAiTrigger","callLine":101,"paramSummary":"generated trigger id"},{"targetFile":"self","targetMethod":"CreateAiTrigger","callLine":108,"paramSummary":"generated trigger id"}],"actions":[{"id":"getepubcontentasync_try_36_0","kind":"try","label":"Begins protected execution","line":36,"detail":"try","visibility":"primary-visible","confidence":0.84},{"id":"try-catch_36","kind":"mapping","label":"overall error handling","line":36,"detail":"wraps the full operation and on exception logs and returns Result.Fail","visibility":"detail-only","confidence":0.7},{"id":"persistence-read_39","kind":"mapping","label":"resolve ebook row","line":39,"detail":"CreateDbContextAsync and query context.Ebooks.FirstOrDefaultAsync to find ebook by id and optional userId","visibility":"detail-only","confidence":0.7},{"id":"getepubcontentasync_await_39_1","kind":"await","label":"Waits for async work","line":39,"detail":"using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);","visibility":"secondary-visible","confidence":0.81},{"id":"getepubcontentasync_repository-read_41_2","kind":"repository-read","label":"Reads repository or persistence state","line":41,"detail":"var ebook = await context.Ebooks","visibility":"secondary-visible","confidence":0.86},{"id":"getepubcontentasync_await_41_3","kind":"await","label":"Waits for async work","line":41,"detail":"var ebook = await context.Ebooks","visibility":"secondary-visible","confidence":0.81},{"id":"guard-clause_47","kind":"guard-clause","label":"ebook not found","line":47,"detail":"returns Result.Fail with message","conditionSummary":"ebook == null","outcomeLabels":["fail-return"],"visibility":"detail-only","confidence":0.7},{"id":"getepubcontentasync_branch_47_4","kind":"branch","label":"Evaluates branch condition","line":47,"detail":"if (ebook == null)","conditionSummary":"ebook == null","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"getepubcontentasync_return_49_5","kind":"return","label":"Returns result","line":49,"detail":"return Result.Fail($\"Ebook '{ebookId}' not found for user '{userId}'.\");","visibility":"detail-only","confidence":0.7},{"id":"external-file-check_54","kind":"mapping","label":"resolve filesystem path and verify file exists","line":54,"detail":"_logger.LogError and Result.Fail when file missing","conditionSummary":"ResolvePath returned null or file missing","outcomeLabels":["log-error","fail-return"],"visibility":"detail-only","confidence":0.7},{"id":"getepubcontentasync_branch_55_6","kind":"branch","label":"Evaluates branch condition","line":55,"detail":"if (fullPath == null || !File.Exists(fullPath))","conditionSummary":"fullPath == null || !File.Exists(fullPath)","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"getepubcontentasync_log_57_7","kind":"log","label":"Logs runtime state","line":57,"detail":"_logger.LogError(\"EPUB file for ebook {EbookId} not found at path '{FilePath}'.\", ebookId, ebook.FilePath);","visibility":"secondary-visible","confidence":0.92},{"id":"getepubcontentasync_return_58_8","kind":"return","label":"Returns result","line":58,"detail":"return Result.Fail($\"The EPUB file for this book could not be found on the server.\");","visibility":"detail-only","confidence":0.7},{"id":"external-call_62","kind":"external-call","label":"open EPUB","line":62,"detail":"Calls VersOne.Epub.EpubReader.OpenBookAsync to open the EPUB (external library)","visibility":"detail-only","confidence":0.7},{"id":"getepubcontentasync_await_62_9","kind":"await","label":"Waits for async work","line":62,"detail":"using var bookRef = await EpubReader.OpenBookAsync(fullPath);","visibility":"secondary-visible","confidence":0.81},{"id":"getepubcontentasync_branch_65_10","kind":"branch","label":"Evaluates branch condition","line":65,"detail":"if (readingOrder == null || !readingOrder.Any())","conditionSummary":"readingOrder == null || !readingOrder.Any()","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"guard-clause_65","kind":"guard-clause","label":"no reading order","line":65,"detail":"returns Result.Fail if EPUB has no readable content","conditionSummary":"readingOrder == null || !readingOrder.Any()","outcomeLabels":["fail-return"],"visibility":"detail-only","confidence":0.7},{"id":"getepubcontentasync_return_67_11","kind":"return","label":"Returns result","line":67,"detail":"return Result.Fail(\"The EPUB has no readable content files in ReadingOrder.\");","visibility":"detail-only","confidence":0.7},{"id":"getepubcontentasync_branch_70_12","kind":"branch","label":"Evaluates branch condition","line":70,"detail":"if (chapterIndex < 0 || chapterIndex >= readingOrder.Count)","conditionSummary":"chapterIndex < 0 || chapterIndex >= readingOrder.Count","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"branch_70","kind":"branch","label":"normalize chapterIndex","line":70,"detail":"sets chapterIndex = 0 when invalid","conditionSummary":"chapterIndex out of bounds","outcomeLabels":["reset-to-0","continue"],"visibility":"detail-only","confidence":0.7},{"id":"getepubcontentasync_repository-read_77_13","kind":"repository-read","label":"Reads repository or persistence state","line":77,"detail":"var chapterTitle = FindTitleInNavigation(navigation, chapterRef.FilePath)","visibility":"secondary-visible","confidence":0.86},{"id":"external-call_81","kind":"external-call","label":"read chapter text","line":81,"detail":"chapterRef.ReadContentAsTextAsync() (external EPUB object)","visibility":"detail-only","confidence":0.7},{"id":"getepubcontentasync_await_81_14","kind":"await","label":"Waits for async work","line":81,"detail":"var chapterContent = await chapterRef.ReadContentAsTextAsync();","visibility":"secondary-visible","confidence":0.81},{"id":"loop_89","kind":"loop","label":"iterate paragraphs","line":89,"detail":"foreach paragraphs: sanitize, skip empties, add TextSegmentBlock, count words, insert AI trigger when threshold reached","visibility":"detail-only","confidence":0.7},{"id":"getepubcontentasync_loop_89_15","kind":"loop","label":"Repeats work over a collection or condition","line":89,"detail":"foreach (var p in paragraphs)","conditionSummary":"var p in paragraphs","loopTargetLine":89,"loopExitSummary":"Leaves the loop when the condition no longer holds.","visibility":"primary-visible","confidence":0.86},{"id":"getepubcontentasync_guard-clause_92_16","kind":"guard-clause","label":"Guards early exit or rejection path","line":92,"detail":"if (string.IsNullOrWhiteSpace(sanitizedContent)) continue;","conditionSummary":"string.IsNullOrWhiteSpace(sanitizedContent)","outcomeLabels":["exit","continue"],"visibility":"primary-visible","confidence":0.9},{"id":"getepubcontentasync_branch_99_17","kind":"branch","label":"Evaluates branch condition","line":99,"detail":"if (totalWordCount >= WordThreshold)","conditionSummary":"totalWordCount >= WordThreshold","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"threshold_99","kind":"mapping","label":"insert AI trigger after WordThreshold words","line":99,"detail":"totalWordCount compared to WordThreshold and resets after inserting trigger block","visibility":"detail-only","confidence":0.7},{"id":"post-condition_106","kind":"mapping","label":"ensure trailing AI trigger","line":106,"detail":"adds trigger if last block is not an AiActionTriggerBlock","visibility":"detail-only","confidence":0.7},{"id":"getepubcontentasync_branch_106_18","kind":"branch","label":"Evaluates branch condition","line":106,"detail":"if (blocks.Any() && blocks.Last() is not AiActionTriggerBlock)","conditionSummary":"blocks.Any() && blocks.Last() is not AiActionTriggerBlock","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"getepubcontentasync_return_111_19","kind":"return","label":"Returns result","line":111,"detail":"return Result.Ok(new ReaderPageViewModel(blocks, chapterIndex, readingOrder.Count, chapterTitle, ebook.Id));","visibility":"detail-only","confidence":0.7},{"id":"return_111","kind":"return","label":"successful result","line":111,"detail":"returns Result.Ok with ReaderPageViewModel","visibility":"detail-only","confidence":0.7},{"id":"getepubcontentasync_catch_113_20","kind":"catch","label":"Handles exception path","line":113,"detail":"catch (Exception ex)","conditionSummary":"Exception ex","outcomeLabels":["handled exception"],"visibility":"primary-visible","confidence":0.86},{"id":"log_115","kind":"log","label":"log error on exception","line":115,"detail":"_logger.LogError in catch, returns failure containing exception","visibility":"detail-only","confidence":0.7},{"id":"getepubcontentasync_log_115_21","kind":"log","label":"Logs runtime state","line":115,"detail":"_logger.LogError(ex, \"Failed to process EPUB for ebook {EbookId}.\", ebookId);","visibility":"secondary-visible","confidence":0.92},{"id":"getepubcontentasync_return_116_22","kind":"return","label":"Returns result","line":116,"detail":"return Result.Fail(new Error($\"Failed to process EPUB: {ex.Message}\").CausedBy(ex));","visibility":"detail-only","confidence":0.7}]},{"name":"ResolvePath","line":124,"endLine":143,"signature":"(relativePath: string) -> string?","purpose":"Resolves a web-relative storage path to an absolute filesystem path by searching up the app base directory for wwwroot (and a development src path).","calls":[],"actions":[{"id":"normalization_127","kind":"mapping","label":"normalize path separators","line":127,"detail":"converts '/' to OS directory separator","visibility":"detail-only","confidence":0.7},{"id":"resolvepath_loop_130_0","kind":"loop","label":"Repeats work over a collection or condition","line":130,"detail":"while (currentDir != null)","conditionSummary":"currentDir != null","loopTargetLine":130,"loopExitSummary":"Leaves the loop when the condition no longer holds.","visibility":"primary-visible","confidence":0.86},{"id":"loop_130","kind":"loop","label":"walk parent directories","line":130,"detail":"iteratively checks currentDir and its parents for candidate and devCandidate locations","visibility":"detail-only","confidence":0.7},{"id":"file-exists-check_132","kind":"mapping","label":"check candidate paths","line":132,"detail":"returns candidate or devCandidate when File.Exists","visibility":"detail-only","confidence":0.7},{"id":"resolvepath_guard-clause_133_1","kind":"guard-clause","label":"Guards early exit or rejection path","line":133,"detail":"if (File.Exists(candidate)) return candidate;","conditionSummary":"File.Exists(candidate)","outcomeLabels":["exit","continue"],"visibility":"primary-visible","confidence":0.9},{"id":"resolvepath_guard-clause_137_2","kind":"guard-clause","label":"Guards early exit or rejection path","line":137,"detail":"if (File.Exists(devCandidate)) return devCandidate;","conditionSummary":"File.Exists(devCandidate)","outcomeLabels":["exit","continue"],"visibility":"primary-visible","confidence":0.9},{"id":"return_142","kind":"return","label":"not found","line":142,"detail":"returns null if no candidate found","visibility":"detail-only","confidence":0.7},{"id":"resolvepath_return_142_3","kind":"return","label":"Returns result","line":142,"detail":"return null;","visibility":"detail-only","confidence":0.7}]},{"name":"ExtractParagraphs","line":145,"endLine":164,"signature":"(html: string) -> List","purpose":"Extracts paragraph-like blocks from HTML by selecting

, headings, lists, blockquote, pre, hr or splitting on breaks when none found.","calls":[],"actions":[{"id":"regex-parse_147","kind":"mapping","label":"extract body content","line":147,"detail":"Regex.Match to capture contents or fallback to full html","visibility":"detail-only","confidence":0.7},{"id":"regex-parse_151","kind":"mapping","label":"find paragraph-like tags","line":151,"detail":"Regex.Matches selects p, h1-6, ul, ol, blockquote, pre, hr","visibility":"detail-only","confidence":0.7},{"id":"extractparagraphs_loop_153_0","kind":"loop","label":"Repeats work over a collection or condition","line":153,"detail":"foreach (Match match in matches)","conditionSummary":"Match match in matches","loopTargetLine":153,"loopExitSummary":"Leaves the loop when the condition no longer holds.","visibility":"primary-visible","confidence":0.86},{"id":"extractparagraphs_branch_158_1","kind":"branch","label":"Evaluates branch condition","line":158,"detail":"if (paragraphs.Count == 0)","conditionSummary":"paragraphs.Count == 0","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"fallback-split_158","kind":"mapping","label":"split by breaks when no matches","line":158,"detail":"splits on
,
, double newlines","visibility":"detail-only","confidence":0.7},{"id":"extractparagraphs_return_163_2","kind":"return","label":"Returns result","line":163,"detail":"return paragraphs;","visibility":"detail-only","confidence":0.7}]},{"name":"SanitizeParagraph","line":166,"endLine":173,"signature":"(html: string) -> string","purpose":"Sanitizes HTML paragraph content by removing scripts/styles, stripping disallowed tags, normalizing allowed tags, HTML-decoding and trimming.","calls":[],"actions":[{"id":"regex-clean_168","kind":"mapping","label":"remove script/style blocks","line":168,"detail":"Regex.Replace to remove