Skip to content

16 subtitle conversion library#17

Open
sjiamnocna wants to merge 15 commits into
masterfrom
16-subtitle-conversion-lib
Open

16 subtitle conversion library#17
sjiamnocna wants to merge 15 commits into
masterfrom
16-subtitle-conversion-lib

Conversation

@sjiamnocna

@sjiamnocna sjiamnocna commented Jul 22, 2025

Copy link
Copy Markdown
Collaborator

#16

Streaming Subtitle Conversion Feature

Overview

This Merge Request introduces support for streaming subtitle conversion. The new functionality enables the system to accept a stream of subtitle items and begin generating valid subtitle output (SRT, VTT, plain text, etc.) in real time, as the video is being transcribed. This allows users to receive subtitle data incrementally, improving responsiveness and user experience for long or live videos.

Why Incremental Subtitles?

While most users typically want the complete, finalized subtitle file (e.g., for download, archiving, or sharing), there are important scenarios where incremental or partial subtitle output is valuable:

  • Live or long-form content: For live streams, webinars, or lengthy videos, users may want to download subtitles as the content progresses, rather than waiting until the end. This is especially helpful for accessibility or for viewers joining a live event in real time.
  • Real-time feedback: Developers, editors, or QA testers might want to see how subtitles are being generated as the video is processed, to catch errors or make adjustments on the fly.
  • Progressive download/display: Some platforms allow users to start watching (and reading subtitles) before the entire video or subtitle file is ready—think of live captions or instant streaming features.
  • Partial data recovery: If a stream is interrupted, having the partial subtitles up to that point can be better than losing everything.

For most users, the default remains to wait for the whole subtitle file. The streaming feature is optional and designed to enhance flexibility for advanced or real-time use cases.

Motivation

  • Faster feedback: Users can see subtitles as soon as they are available, without waiting for the entire transcription to finish.
  • Improved UX: Enables real-time subtitle display and download for live or long-form content.
  • API flexibility: Supports both batch and streaming workflows for subtitle consumers.

Key Features

  • Stream-based processing: Accepts a stream (generator/iterator) of strongly typed subtitle items.
  • Incremental output: Generates valid output in the requested format (SRT, VTT, plain text, etc.) as new subtitle items arrive.
  • Type-safe and Effect-based: All streaming logic is implemented using EffectTS, ensuring type safety and robust error handling.
  • Environment agnostic: No environment-specific APIs are used; works in browser, Node.js, Bun, Cloudflare Workers, and AWS Lambda.
  • Format support:
    • SRT (SubRip)
    • VTT (WebVTT)
    • Plain text
    • JSON (for completeness)
  • Error handling:
    • Typed errors for malformed input, unsupported formats, or missing fields
    • Errors are catchable and integrate with existing error handling patterns

Usage Example

// Example: Streaming SRT output as subtitles are transcribed
const subtitleStream = getSubtitleStream(); // Generator or async iterator of SubtitleItem
for await (const srtChunk of streamSubtitleConversion(subtitleStream, 'srt')) {
  // srtChunk is a valid SRT fragment for the current subtitle item
  sendToClient(srtChunk); // e.g., push to websocket or HTTP stream
}
  • The same pattern applies for 'vtt', 'plain-text', or 'json' formats.
  • The output is always a valid fragment that can be concatenated to form a complete file.

API Changes

  • New streaming conversion functions:
    • runSubtitleProcessingStream and runSubtitleConversionStream now accept streams/generators and output incremental results.
  • Backward compatible:
    • Existing batch conversion APIs are unchanged.
    • Streaming is opt-in and does not break current consumers.

Testing

  • Unit and integration tests for streaming conversion in all supported formats
  • Tests for error handling with malformed or incomplete input
  • Tests for environment agnosticism (no Node.js/browser-specific APIs in core logic)
  • Tests for correct output structure and incremental validity

Documentation

  • JSDoc and code comments for all new streaming functions
  • Usage examples for both batch and streaming workflows
  • Error types and handling examples

Checklist

  • Accepts stream/generator of strongly typed subtitle items
  • Outputs valid subtitle fragments in requested format as data arrives
  • Type-safe and Effect-based implementation
  • Environment agnostic (no platform-specific APIs)
  • Typed error handling for all failure cases
  • Comprehensive tests for streaming and error scenarios
  • Documentation and usage examples updated

This feature enables real-time subtitle delivery and is ready for review and integration. Please provide feedback or request additional scenarios if needed.

@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@sjiamnocna sjiamnocna self-assigned this Jul 22, 2025
@sjiamnocna sjiamnocna linked an issue Jul 22, 2025 that may be closed by this pull request
37 tasks

This comment was marked as outdated.

Comment thread package.json Outdated
Comment thread src/domain/media/subtitle-formats/index.ts Outdated
Comment thread src/domain/media/subtitle-formats/subtitle-converter.test.ts Outdated
Comment thread src/domain/media/subtitle-formats/subtitle-formats.schema.ts
Comment thread src/domain/media/subtitle-formats/subtitle-formats.schema.ts
Comment thread src/domain/media/subtitle-formats/subtitle-filters.ts Outdated
Comment thread src/domain/media/subtitle-formats/subtitle-filters.ts Outdated
Comment thread src/domain/media/subtitle-formats/subtitle-filters.ts Outdated
Comment thread src/domain/media/subtitle-formats/subtitle-filters.ts Outdated
Comment thread src/domain/media/subtitle-formats/subtitle-filters.ts Outdated
sjiamnocna and others added 5 commits July 23, 2025 20:09
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a comprehensive streaming subtitle conversion library that enables real-time subtitle processing and format conversion. The implementation uses Effect-TS for type-safe, streaming-based subtitle processing across multiple output formats.

  • Streaming subtitle processing with Effect-TS for type safety and error handling
  • Support for multiple subtitle formats (SRT, VTT, JSON, plain text) with incremental output
  • Comprehensive filter system for subtitle transformation and processing

Reviewed Changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
subtitle-formats.schema.ts Defines core data structures and schemas for subtitle items, formats, and conversion options
subtitle-formats.errors.ts Error types for subtitle validation, conversion, and processing failures
subtitle-filters.ts Generic filter functions for subtitle transformation and processing pipelines
subtitle-converter.ts Core conversion logic with streaming support and format-specific converters
subtitle-converter.test.ts Comprehensive test suite covering all conversion formats and streaming functionality
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

src/domain/media/subtitle-formats/subtitle-converter.test.ts:1322

  • Using an in-memory mock for file system operations in tests is good practice, but ensure real file system integration is tested elsewhere
        const memoryFS: Record<string, string> = {};

Comment thread src/domain/media/subtitle-formats/subtitle-filters.ts Outdated
* @returns Function that takes a subtitle item, logs it, and returns it unchanged
*/
export const debugSubtitle = (label?: string) => (subtitle: SubtitleItem): SubtitleItem => {
console.log(subtitle)

Copilot AI Jul 27, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug function should include the optional label parameter in the console output for better debugging experience

Suggested change
console.log(subtitle)
console.log(label ? `[${label}]` : '', subtitle)

Copilot uses AI. Check for mistakes.
Comment thread src/domain/media/subtitle-formats/subtitle-converter.ts
Comment thread src/domain/media/subtitle-formats/subtitle-converter.ts Outdated
Comment thread src/domain/media/subtitle-formats/subtitle-converter.test.ts Outdated
Comment thread src/domain/media/subtitle-formats/subtitle-converter.test.ts Outdated
Comment thread .vscode/settings.json Outdated
Comment thread src/domain/media/subtitle-formats/subtitle-converter.ts
@coderabbitai

coderabbitai Bot commented Aug 17, 2025

Copy link
Copy Markdown
Contributor

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Summary by CodeRabbit

  • New Features

    • End-to-end subtitle processing with multi-format output (SRT, VTT, JSON, Plain Text).
    • New API endpoints: health, supported formats, single-format process, and enhanced multi-format process.
    • Options for timing offset, speaker inclusion, text cleaning, and merging adjacent subtitles.
    • Robust validation and clear error responses (400, 422, 500).
  • Documentation

    • New guide covering features, API usage, examples, and troubleshooting.
  • Tests

    • Extensive unit and integration tests for processing, streaming, and endpoints.
  • Chores

    • Added subtitle-specific test scripts and a helper shell script for running checks.

Walkthrough

Adds a complete subtitle processing subsystem: runtime schemas, typed errors, conversion and streaming utilities, composable filters and a simple pipeline, HTTP endpoints and handlers, extensive unit/integration tests, documentation, npm test scripts and a test helper script for JSON/SRT/VTT/plain-text multi-format processing.

Changes

Cohort / File(s) Summary of changes
Documentation
src/domain/media/subtitle-formats/README.md
New README documenting features, API, errors, testing and examples.
Schemas & Types
src/domain/media/subtitle-formats/subtitle-formats.schema.ts
Adds runtime schemas and exported types for SubtitleItem/SubtitleJson, SubtitleFormat, ConversionOptions, conversion result types, enhanced request/response and multi-format response.
Error Types
src/domain/media/subtitle-formats/subtitle-formats.errors.ts
Adds tagged API-bound and domain error types (e.g., SubtitleDataInvalid, SubtitleFormatUnsupported, InvalidTimingError, ConversionError, ProcessingError).
Core Converter
src/domain/media/subtitle-formats/subtitle-converter.ts
New converter implementing validation, transforms (timing offset, cleaning, speaker), merge logic, streaming pipelines and format conversions; exposes SubtitleConverterLive and utility exports.
Filters & Streaming
src/domain/media/subtitle-formats/subtitle-filters.ts
New composable filters, transformers, stream/generator utilities, stream sinks and helpers for formatting/saving.
Simple Pipeline
src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts
New stage-based pipeline with chainable builder, optional parallelism, collectors and formatters plus convenience process helpers.
HTTP Endpoints
src/domain/media/subtitle-formats/endpoints.ts
Adds ProcessSubtitles and enhancedProcessSubtitles endpoints, getSupportedFormats, healthCheck, schema definitions and grouping under /subtitles.
Handlers
src/domain/media/subtitle-formats/subtitle-processor-enhanced.handler.ts
Implements enhanced multi-format and legacy handlers with validation, format parsing, parallel conversion, error mapping and health/formats handlers.
Tests — Converter & Filters
src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts, src/domain/media/subtitle-formats/subtitle-streaming.test.ts
Extensive tests for validation, processing, conversions, streaming, filters, edge cases and effect pipelines.
Tests — Pipeline
src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
Tests for pipeline creation/execution, filters, formatters, error cases and performance.
Tests — Endpoints (unit & integration)
src/domain/media/subtitle-formats/endpoints.test.ts, src/domain/media/subtitle-formats/test-enhanced-endpoints.test.ts
Unit tests for schemas and wiring; integration tests exercising health, formats, legacy and enhanced endpoints and error flows against a local server.
Test Orchestration Script
src/domain/media/subtitle-formats/run-tests.sh
Adds helper shell script to check server health and run subtitle tests with guidance.
Package Scripts
package.json
Adds npm scripts: test:subtitles and test:subtitles:watch.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant API as HttpApi (/subtitles)
  participant Handler as enhancedProcessSubtitlesHandler
  participant Converter as SubtitleConverterLive
  participant Formatter as Formatters
  participant Errors as ErrorMapping

  Client->>API: POST /process-enhanced {title, outputFormat, subtitleData, options}
  API->>Handler: Validate payload (schema)
  Handler->>Handler: parseAndValidateFormats()
  Handler->>Handler: validateSubtitleData()
  loop per format (parallel)
    Handler->>Converter: convert(subtitleData, format, options)
    Converter->>Converter: process (validate → transform → merge)
    Converter->>Formatter: convertTo(format)
    Formatter-->>Converter: content
    Converter-->>Handler: content
  end
  Handler-->>API: MultiFormatResponse
  API-->>Client: 200 OK

  Handler-->>Errors: catchTags(...)
  Errors-->>API: map to 400/422/500
  API-->>Client: Error response
Loading
sequenceDiagram
  autonumber
  actor Client
  participant API as HttpApi (/subtitles)
  participant Handler as processSubtitlesHandler
  participant Converter as SubtitleConverterLive

  Client->>API: POST /process {title, outputFormat, subtitleData, options}
  API->>Handler: Validate payload
  Handler->>Converter: convert(subtitleData, format, options)
  Converter-->>Handler: content
  Handler-->>API: ProcessSubtitlesResponse
  API-->>Client: 200 OK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • Subtitle Conversion Function Implementation #16 — Covers an EffectTS-based, environment-agnostic subtitle conversion pipeline with multi-format conversion and type-safe errors; the PR appears to implement the same conversion and error-handling scope and could be linked.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 16-subtitle-conversion-lib

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 29

♻️ Duplicate comments (9)
.vscode/settings.json (1)

2-9: style(vscode): remove opinionated colour overrides from workspace settings

Workspace-level colour customisations override individual developer preferences and don’t belong in the repo config. Please drop this block.

Apply this diff:

-  "workbench.colorCustomizations": {
-    "editor.selectionBackground": "#135ba2",
-    "editor.selectionHighlightBackground": "#264f7844",
-    "editor.findMatchBackground": "#515c6a",
-    "editor.findMatchHighlightBackground": "#515c6a40",
-    "editor.findMatchBorder": "#515c6a",
-    "editor.findMatchHighlightBorder": "#515c6a40"
-  },
src/domain/media/subtitle-formats/subtitle-filters.ts (4)

190-195: Debug function implementation can be improved

The current implementation matches the past review suggestion but could be more flexible.


56-59: Consider using Option instead of conditional return

This aligns with the Effect-TS patterns used elsewhere in the codebase. The conditional ternary could be simplified.

-export const filterBySpeaker =
-  (speakerId: number) =>
-  (subtitle: SubtitleItem): Option.Option<SubtitleItem> =>
-    subtitle.speaker === speakerId ? Option.some(subtitle) : Option.none()
+export const filterBySpeaker =
+  (speakerId: number) =>
+  (subtitle: SubtitleItem): Option.Option<SubtitleItem> =>
+    Option.fromNullable(subtitle.speaker === speakerId ? subtitle : null)

67-73: Consider using Option pattern more idiomatically

The current implementation could benefit from using Option's built-in methods for cleaner code.

 export const filterBySpeakers =
   (speakerIds: number[]) =>
   (subtitle: SubtitleItem): Option.Option<SubtitleItem> =>
-    typeof subtitle.speaker === 'number' &&
-    speakerIds.includes(subtitle.speaker)
-      ? Option.some(subtitle)
-      : Option.none()
+    Option.fromNullable(subtitle.speaker).pipe(
+      Option.filter((speaker) => speakerIds.includes(speaker)),
+      Option.map(() => subtitle)
+    )

201-216: Consider using Option combinators for validation

The validation logic could be more composable using Option's built-in methods.

 export const validateSubtitle = (
   subtitle: SubtitleItem,
 ): Option.Option<SubtitleItem> => {
-  // Basic validation rules
-  if (subtitle.start < 0) {
-    return Option.none()
-  }
-  if (subtitle.end <= subtitle.start) {
-    return Option.none()
-  }
-  if (subtitle.text.trim().length === 0) {
-    return Option.none()
-  }
-  return Option.some(subtitle)
+  return Option.some(subtitle).pipe(
+    Option.filter((s) => s.start >= 0),
+    Option.filter((s) => s.end > s.start),
+    Option.filter((s) => s.text.trim().length > 0)
+  )
 }
src/domain/media/subtitle-formats/subtitle-converter.ts (4)

42-48: Consider using Effect Schema's NonEmptyArray validation

As suggested in past reviews, Effect Schema provides utilities for NonEmptyArray validation that would be more idiomatic.

+import { NonEmptyArray } from 'effect/Array'
+
     // Check if subtitles array exists and is not empty
-    if (!Array.isArray(actualSubtitles) || actualSubtitles.length === 0) {
+    const nonEmptyResult = NonEmptyArray.fromArray(actualSubtitles)
+    if (Option.isNone(nonEmptyResult)) {
       return yield* E.fail(
         new InvalidSubtitleDataError({
           cause: new Error('Subtitle data must be a non-empty array'),
         }),
       )
     }

99-110: Consider using Schema's NonNegativeInt validation

The manual validation of speaker field could use Effect Schema's NonNegativeInt type for consistency.

The speaker field validation is already defined as Schema.optional(Schema.NonNegativeInt) in the schema. Consider using Schema validation directly instead of manual checks.


325-326: Inefficient array creation for single item validation

Creating a new array with a single item for validation is inefficient, as noted in past reviews.

-    Stream.mapEffect((item) =>
-      validateSubtitleData([item], allowEmptyText).pipe(E.map((arr) => arr[0])),
-    ),
+    Stream.mapEffect((item) => {
+      // Validate single item directly without array wrapper
+      return E.gen(function* () {
+        if (typeof item.start !== 'number' || typeof item.end !== 'number' || typeof item.text !== 'string') {
+          return yield* E.fail(new InvalidSubtitleDataError({
+            cause: new Error('Invalid subtitle item structure')
+          }))
+        }
+        if (item.start < 0 || item.end < 0) {
+          return yield* E.fail(new InvalidTimingError({
+            cause: new Error('Negative timing values')
+          }))
+        }
+        if (item.start >= item.end) {
+          return yield* E.fail(new InvalidTimingError({
+            cause: new Error('Start time >= end time')
+          }))
+        }
+        if (!allowEmptyText && item.text.trim().length === 0) {
+          return yield* E.fail(new InvalidSubtitleDataError({
+            cause: new Error('Empty text content')
+          }))
+        }
+        return item
+      })
+    }),

657-659: Implement proper schema validation as noted in TODO

The TODO comment indicates incomplete implementation. Schema validation should be properly implemented for production use.

-      // For now, skip schema validation to avoid complex Either handling
-      // In production, you might want to add proper schema validation here
+      // Validate against schema
+      const validationResult = yield* E.try({
+        try: () => Schema.decodeUnknownSync(SubtitleJson)(subtitles),
+        catch: (error) => new InvalidSubtitleDataError({
+          cause: error instanceof Error ? error : new Error(String(error))
+        })
+      })
       return yield* convertSubtitleFormat(subtitles, format, options)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 53c1a26 and 2dacfce.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • .vscode/settings.json (1 hunks)
  • src/domain/media/subtitle-formats/README.md (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-converter.test.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-converter.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-filters.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-streaming.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
src/**/*.ts

📄 CodeRabbit Inference Engine (.rules/code-style.mdc)

src/**/*.ts: Always use relative paths for internal imports; never use src/ aliases for internal imports in TypeScript files.
Use package imports only for external dependencies in TypeScript files.
Avoid obvious comments; never add comments that just repeat what the code is doing. Only add comments for complex business logic that needs explanation.
Use JSDoc for public APIs, not inline comments for obvious operations.
Add comments only for complex business logic, non-obvious workarounds, edge cases, or important context that affects future changes.
Use Effect.Service class extension pattern for services, include dependencies in service definition, and provide makeTestService for testing.
Use E.gen(function* () {}) for complex flows; yield effects directly without helper functions; use yield* for effects that return values.
Use Data.Tagged for domain errors, create specific error types for different failure cases, and handle errors explicitly with E.catchTags.
Use Effect's logging utilities instead of console.log; include relevant context in log messages and use appropriate log levels.
Add spans for observability using E.withSpan, include relevant attributes, and use descriptive span names.
Use PascalCase for service classes (e.g., BlogStore, SearchStore).
Use camelCase for functions and variables (e.g., getBlogById, blogPost).
Use UPPERCASE for constants (e.g., TABLE_NAME, DEFAULT_LIMIT).
Use descriptive names for functions and methods that indicate purpose.
Prefix usecases with action (e.g., createBlogPostUsecase, getBlogByIdUsecase).
Prefix handlers with HTTP method context (e.g., _handler, handler).
Use Effect Schema for all data validation; define schemas with explicit types and use branded types for domain identifiers.
Use branded types for domain identifiers and create factory functions with validation.
Access configuration through the Effect system, not directly via process.env.
Use pipe syntax for effect composition;...

Files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
  • src/domain/media/subtitle-formats/subtitle-streaming.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts
  • src/domain/media/subtitle-formats/subtitle-converter.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
src/**/*

📄 CodeRabbit Inference Engine (.rules/code-style.mdc)

Use kebab-case for file names (e.g., blog.store.ts, get-blog-by-id.ts).

Files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
  • src/domain/media/subtitle-formats/README.md
  • src/domain/media/subtitle-formats/subtitle-streaming.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts
  • src/domain/media/subtitle-formats/subtitle-converter.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
src/**/*.test.ts

📄 CodeRabbit Inference Engine (.rules/code-style.mdc)

Always use @effect/vitest for Effect-based tests; use appropriate test variants (it.effect, it.scoped, it.live); mock services using makeTestService.

Files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
  • src/domain/media/subtitle-formats/subtitle-streaming.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.rules/effect-service-architecture.mdc)

Always use relative paths for internal imports; never use src/ aliases for internal imports. Use package imports only for external dependencies.

Files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
  • src/domain/media/subtitle-formats/subtitle-streaming.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts
  • src/domain/media/subtitle-formats/subtitle-converter.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
**/*.test.ts

📄 CodeRabbit Inference Engine (.rules/effect-testing-patterns.mdc)

**/*.test.ts: Always use @effect/vitest for tests involving Effect-based code; import test functions (describe, expect, it) from @effect/vitest, not regular vitest.
Always use relative paths for internal imports in test files; never use src/ aliases.
Use package imports for external dependencies in test files.
Test tagged errors with proper Effect patterns (using E.exit and getExitError).
Use it.effect for tests that need TestContext (TestClock, etc.) in Effect-TS test files.
Use it.scoped for tests involving resources that need proper cleanup in Effect-TS test files.
Use it.live for tests that need real system time and live services in Effect-TS test files.
Use FetchHttpClientTest from @t6c/lib-api for mocking HTTP responses in API client tests.
Use mock data from domain packages when available in tests.
Create reusable mock objects for complex data structures in tests.
Use proper branded type constructors for mock data in tests.
Declare variables outside generator functions when used in FetchHttpClientTest to prevent scoping issues in tests.
Use .pipe(E.exit) directly on service calls for cleaner error testing in tests; avoid nested E.exit(E.gen(...)) patterns.
Use Exit.isFailure(result) to check for failures and getExitError(result) to extract error details in tests.
Test all public methods of services, including both success and error scenarios, default parameters, edge cases, and validation.
Use appropriate HTTP status codes (200, 201, 404, 500) and match response structure to actual API responses in mock responses for tests.
Use descriptive test names that explain the scenario, following the pattern: 'should [action] [condition]'.
Group related tests in describe blocks by method.
Reuse mock data across tests when possible.
Create typed mock objects with proper constructors for tests.
Always provide required layers in correct order in tests (e.g., DefaultWithoutDependencies for services, MockConfigLayer for configurat...

Files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
  • src/domain/media/subtitle-formats/subtitle-streaming.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
🧠 Learnings (26)
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use pipe syntax for effect composition; chain operations logically and keep pipes readable with proper formatting.

Applied to files:

  • src/domain/media/subtitle-formats/README.md
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Test all public methods of services, including both success and error scenarios, default parameters, edge cases, and validation.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Write pure functions without side effects; use Effect for operations with side effects; keep business logic separate from infrastructure.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Use proper Effect error handling patterns in utility functions.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use `it.effect` for tests that need TestContext (TestClock, etc.) in Effect-TS test files.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Use Effect-TS patterns for async operations in utility functions.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Access configuration through the Effect system, not directly via `process.env`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Always use `effect/vitest` for tests involving Effect-based code; import test functions (`describe`, `expect`, `it`) from `effect/vitest`, not regular `vitest`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use `E.gen(function* () {})` for complex flows; yield effects directly without helper functions; use `yield*` for effects that return values.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Log errors within the Effect system using `E.logError` or `E.log`, not `console.log`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:46:54.925Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-store-patterns.mdc:0-0
Timestamp: 2025-08-10T21:46:54.925Z
Learning: Applies to **/*.store.ts : Store classes should integrate with external services (e.g., Algolia, DynamoDB, S3) using Effect-TS patterns

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Use `yield* new ErrorClass()` in Effect.gen instead of throwing errors.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:48:12.331Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/schema-validation.mdc:0-0
Timestamp: 2025-08-10T21:48:12.331Z
Learning: Applies to **/*.types.ts|**/schema/**/*.ts|**/schemas/**/*.ts|**/*-schema.ts|**/*-schemas.ts|**/branded.ts|**/domain/**/*.ts : Use `Data.TaggedError` for domain-specific error types and include relevant identifiers and context in error data

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use `Data.Tagged` for domain errors, create specific error types for different failure cases, and handle errors explicitly with `E.catchTags`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
📚 Learning: 2025-08-10T21:46:54.925Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-store-patterns.mdc:0-0
Timestamp: 2025-08-10T21:46:54.925Z
Learning: Applies to **/*.store.ts : Convert external errors (e.g., DynamoDB, S3) to domain errors using Effect-TS error handling patterns

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts
📚 Learning: 2025-08-10T21:46:54.925Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-store-patterns.mdc:0-0
Timestamp: 2025-08-10T21:46:54.925Z
Learning: Applies to **/stores/**/*.ts : Convert external errors (e.g., DynamoDB, S3) to domain errors using Effect-TS error handling patterns

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts
📚 Learning: 2025-08-10T21:46:54.925Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-store-patterns.mdc:0-0
Timestamp: 2025-08-10T21:46:54.925Z
Learning: Applies to **/*-store.ts : Convert external errors (e.g., DynamoDB, S3) to domain errors using Effect-TS error handling patterns

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use Effect Schema for all data validation; define schemas with explicit types and use branded types for domain identifiers.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:03:54.779Z
Learnt from: BleedingDev
PR: BleedingDev/coursition-effect-experiment#18
File: src/domain/workflow/workflow.errors.ts:3-6
Timestamp: 2025-08-10T21:03:54.779Z
Learning: In Effect-based applications, Schema.TaggedError is used for serializable errors that can bubble up to clients/external systems, while Data.TaggedError is used for internal errors that stay within the application boundaries. This is an intentional architectural pattern to differentiate between public API errors and internal implementation errors.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Never use `throw new Error()` in Effect-TS applications; always create and use `Data.TaggedError` for error handling.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
📚 Learning: 2025-08-10T21:46:38.101Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-service-architecture.mdc:0-0
Timestamp: 2025-08-10T21:46:38.101Z
Learning: Use tagged errors for type-safe error handling. Convert external errors to domain errors at store boundaries, add context and logging at the usecase level, and map to appropriate HTTP responses at the handler level.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts
📚 Learning: 2025-08-10T21:46:24.385Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-handler-patterns.mdc:0-0
Timestamp: 2025-08-10T21:46:24.385Z
Learning: Applies to **/lambdas/**/handler.ts,**/*-handler.ts : Handlers must map domain errors to HTTP responses using 'E.catchTags' with appropriate error-to-response mappings.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Test tagged errors with proper Effect patterns (using `E.exit` and `getExitError`).

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.errors.ts
📚 Learning: 2025-08-10T21:48:12.331Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/schema-validation.mdc:0-0
Timestamp: 2025-08-10T21:48:12.331Z
Learning: Applies to **/*.types.ts|**/schema/**/*.ts|**/schemas/**/*.ts|**/*-schema.ts|**/*-schemas.ts|**/branded.ts|**/domain/**/*.ts : Always use `Schema as S` for Effect Schema imports

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:46:10.977Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/cursor-rule-maintainer.mdc:0-0
Timestamp: 2025-08-10T21:46:10.977Z
Learning: Applies to .cursor/rules/schema-validation.mdc : Add new Effect Schema patterns, validation improvements, branded type patterns, and error handling for schemas to `schema-validation.mdc`

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:48:12.331Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/schema-validation.mdc:0-0
Timestamp: 2025-08-10T21:48:12.331Z
Learning: Applies to **/schema/**/*.ts|**/schemas/**/*.ts|**/*-schema.ts|**/*-schemas.ts : Test schema validation with valid and invalid inputs using effect/vitest and Effect-based test patterns

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
🧬 Code Graph Analysis (9)
src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts (3)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (2)
  • SubtitleItem (7-16)
  • SubtitleItem (64-64)
src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts (13)
  • createPipeline (266-268)
  • SubtitlePipeline (144-257)
  • createArrayPipeline (273-279)
  • PipelineConfig (43-47)
  • createCollector (121-128)
  • applyFilters (79-93)
  • formatToSrt (288-306)
  • formatToVtt (311-324)
  • formatToPlainText (329-331)
  • formatToJson (336-338)
  • processToSrt (373-382)
  • processToVtt (387-396)
  • processWithConfig (401-415)
src/domain/media/subtitle-formats/subtitle-filters.ts (9)
  • toUpperCase (149-152)
  • applyFilters (348-373)
  • removeEmptySubtitles (179-182)
  • addPrefix (81-86)
  • filterBySpeakers (67-73)
  • addTimingOffset (42-48)
  • transformText (137-142)
  • capitalize (169-172)
  • validateSubtitle (202-216)
src/domain/media/subtitle-formats/README.md (1)
src/stores/media/media.store.test.ts (2)
  • describe (17-49)
  • Effect (7-14)
src/domain/media/subtitle-formats/subtitle-streaming.test.ts (3)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (2)
  • SubtitleItem (7-16)
  • SubtitleItem (64-64)
src/domain/media/subtitle-formats/subtitle-filters.ts (14)
  • collectStream (395-401)
  • createSubtitleStream (315-316)
  • applyFilters (348-373)
  • toUpperCase (149-152)
  • removeEmptySubtitles (179-182)
  • addTimingOffset (42-48)
  • processAndCollect (410-415)
  • filterBySpeaker (56-59)
  • processSubtitlesParallel (425-430)
  • addPrefix (81-86)
  • streamSubtitlesGenerator (439-467)
  • validateSubtitle (202-216)
  • collectGenerator (475-483)
  • processSubtitlesWithGenerator (492-500)
src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts (1)
  • applyFilters (79-93)
src/domain/media/subtitle-formats/subtitle-converter.test.ts (5)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (2)
  • SubtitleItem (7-16)
  • SubtitleItem (64-64)
src/domain/media/subtitle-formats/subtitle-converter.ts (7)
  • SubtitleItem (712-712)
  • validateSubtitleData (25-122)
  • processSubtitles (252-309)
  • SubtitleConverterLive (631-709)
  • runSubtitleProcessingStream (351-359)
  • runSubtitleConversionStream (446-453)
  • addSpeakerInfo (159-170)
src/domain/media/subtitle-formats/subtitle-filters.ts (6)
  • applyFiltersToArray (225-264)
  • replaceText (16-34)
  • addTimingOffset (42-48)
  • filterBySpeaker (56-59)
  • addPrefix (81-86)
  • streamSubtitles (273-302)
src/domain/media/subtitle-formats/subtitle-formats.errors.ts (2)
  • UnsupportedFormatError (43-48)
  • InvalidTimingError (50-52)
src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts (1)
  • filter (169-172)
src/domain/media/subtitle-formats/subtitle-formats.errors.ts (2)
src/domain/media/media.errors.ts (6)
  • MediaParsingError (23-26)
  • MediaClientError (28-31)
  • MediaEmpty (4-7)
  • YtDlpValidationError (37-41)
  • MediaEmptyError (15-17)
  • YtDlpDownloadError (33-35)
src/domain/workflow/workflow.errors.ts (1)
  • WorkflowError (3-6)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (4)
src/domain/media/subtitle-formats/subtitle-converter.ts (2)
  • SubtitleItem (712-712)
  • SubtitleJson (712-712)
src/steps/media/persist-subtitles.step.ts (1)
  • subtitlesJson (3-14)
src/stores/media/media.store.test.ts (4)
  • describe (17-49)
  • it (18-48)
  • Effect (19-31)
  • Effect (7-14)
src/domain/media/media.errors.ts (3)
  • MediaEmpty (4-7)
  • MediaParsingError (23-26)
  • MediaClientError (28-31)
src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts (3)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (2)
  • SubtitleItem (7-16)
  • SubtitleItem (64-64)
src/domain/media/subtitle-formats/subtitle-filters.ts (1)
  • applyFilters (348-373)
src/steps/media/persist-subtitles.step.ts (1)
  • subtitlesJson (3-14)
src/domain/media/subtitle-formats/subtitle-converter.ts (2)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (11)
  • SubtitleJson (21-21)
  • SubtitleJson (65-65)
  • SubtitleItem (7-16)
  • SubtitleItem (64-64)
  • ConversionOptions (31-42)
  • ConversionOptions (67-67)
  • SubtitleFormat (26-26)
  • SubtitleFormat (66-66)
  • SubtitleConversionResultSchema (54-57)
  • MultipleFormatResult (59-61)
  • MultipleFormatResult (68-70)
src/domain/media/subtitle-formats/subtitle-formats.errors.ts (4)
  • InvalidSubtitleDataError (37-41)
  • InvalidTimingError (50-52)
  • UnsupportedFormatError (43-48)
  • ConversionError (54-57)
src/domain/media/subtitle-formats/subtitle-filters.ts (5)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (2)
  • SubtitleItem (7-16)
  • SubtitleItem (64-64)
src/domain/media/subtitle-formats/subtitle-converter.ts (3)
  • SubtitleItem (712-712)
  • formatTimeSrt (461-467)
  • formatTimeVtt (475-481)
src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts (3)
  • filter (169-172)
  • stream (161-164)
  • applyFilters (79-93)
src/steps/media/persist-subtitles.step.ts (1)
  • subtitlesJson (3-14)
src/usecases/media/transcribe-workflow.usecase.ts (2)
  • transcribeMediaStep (51-52)
  • persistSubtitlesStep (55-56)
🪛 LanguageTool
src/domain/media/subtitle-formats/README.md

[style] ~80-~80: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...peline. #### .parallelFilter(filter) Adds a parallel filter stage to the pipeline...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~83-~83: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...pipeline. #### .collector(collector) Adds a collector stage to the pipeline. ###...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~86-~86: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...pipeline. #### .formatter(formatter) Adds a formatter stage to the pipeline. ###...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~100-~100: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...ssWithConfig(items, filters?, config?)` Processes subtitles with custom configuration. #...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~246-~246: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ...e buffer sizes - Consider streaming for very large datasets - Clean up references after pr...

(EN_WEAK_ADJECTIVE)


[grammar] ~278-~278: The verb ‘Ensure’ is plural. Did you mean: “ensures”? Did you use a verb instead of a noun?
Context: ...e tests 3. Update this documentation 4. Ensure type safety 5. Consider performance imp...

(PLURAL_VERB_AFTER_THIS)

🪛 markdownlint-cli2 (0.17.2)
src/domain/media/subtitle-formats/README.md

66-66: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


69-69: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


74-74: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


77-77: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


80-80: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


83-83: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


86-86: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


89-89: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


94-94: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


97-97: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


100-100: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


105-105: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


106-106: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


114-114: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


115-115: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


119-119: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


120-120: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


123-123: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


124-124: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


127-127: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


128-128: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


132-132: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


133-133: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


133-133: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


143-143: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


144-144: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


144-144: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


154-154: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


155-155: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


166-166: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


167-167: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


167-167: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


174-174: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


175-175: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


183-183: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


184-184: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


199-199: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


200-200: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


218-218: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


219-219: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


239-239: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


240-240: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


244-244: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


245-245: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


249-249: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


250-250: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


279-279: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


279-279: Files should end with a single newline character

(MD047, single-trailing-newline)

🔇 Additional comments (3)
src/domain/media/subtitle-formats/subtitle-formats.errors.ts (1)

3-35: feat(errors): good split between API errors (Schema.TaggedError) and internal domain errors (Data.TaggedError)

This follows our error architecture and is consistent with media/workflow error patterns.

src/domain/media/subtitle-formats/subtitle-formats.schema.ts (1)

54-61: refactor(schema): keep schema constants and DTO schemas clearly named

Minor: you already have SubtitleConversionResult as a Data class and a corresponding schema. Consider naming the schema explicitly for clarity.

-export const SubtitleConversionResultSchema = Schema.Struct({
-  format: SubtitleFormat,
-  content: Schema.String,
-})
+export const SubtitleConversionResultSchema = S.Struct({
+  format: SubtitleFormat,
+  content: S.String,
+})
⛔ Skipped due to learnings
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/schema-validation.mdc:0-0
Timestamp: 2025-08-10T21:48:12.331Z
Learning: Applies to **/*.types.ts|**/schema/**/*.ts|**/schemas/**/*.ts|**/*-schema.ts|**/*-schemas.ts|**/branded.ts|**/domain/**/*.ts : Always use `Schema as S` for Effect Schema imports
src/domain/media/subtitle-formats/subtitle-streaming.test.ts (1)

1-21: test(types): validate type import correctness

I’ve verified that there are no remaining “only refers to a value, but is being used as a type” errors in your tests. The import type { SubtitleItem } from './subtitle-formats.schema' remains valid now that schema types use S.Type<…>.

If you decide to rename runtime schemas (for example, SubtitleItemSchema) to avoid value/type shadowing, please update the corresponding type imports here:

  • File: src/domain/media/subtitle-formats/subtitle-streaming.test.ts (lines 1–21)

Comment on lines +62 to +102
## API Reference

### Pipeline Creation

#### `createPipeline(config?)`
Creates a new pipeline with optional configuration.

#### `createArrayPipeline(items, config?)`
Creates a pipeline that processes an array of subtitle items.

### Pipeline Methods

#### `.stream(generator)`
Adds a streaming stage to the pipeline.

#### `.filter(filter)`
Adds a filter stage to the pipeline.

#### `.parallelFilter(filter)`
Adds a parallel filter stage to the pipeline.

#### `.collector(collector)`
Adds a collector stage to the pipeline.

#### `.formatter(formatter)`
Adds a formatter stage to the pipeline.

#### `.execute()`
Executes the pipeline and returns the result.

### Pre-built Functions

#### `processToSrt(items, filters?)`
Processes subtitles and converts to SRT format.

#### `processToVtt(items, filters?)`
Processes subtitles and converts to VTT format.

#### `processWithConfig(items, filters?, config?)`
Processes subtitles with custom configuration.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🛠️ Refactor suggestion

🧩 Analysis chain

docs(subtitles): document new streaming APIs runSubtitleProcessingStream/runSubtitleConversionStream

PR objectives mention new stream-based functions, but this README doesn’t show them. Please add a “Streaming APIs” section with examples for:

  • runSubtitleProcessingStream: consuming a generator/async iterator, applying filters, yielding items.
  • runSubtitleConversionStream: producing incremental SRT/VTT/text/JSON fragments.

This will align the documentation with the new public capability.

Would you like me to draft a README section with runnable examples for both functions?


🏁 Script executed:

#!/bin/bash
set -e

echo "🔍 Searching for runSubtitleProcessingStream in the codebase…"
rg -n "runSubtitleProcessingStream"

echo "🔍 Searching for runSubtitleConversionStream in the codebase…"
rg -n "runSubtitleConversionStream"

Length of output: 1382


docs(subtitles): add Streaming APIs section for new stream-based functions
The README currently documents pipeline methods and pre-built functions but omits the newly introduced runSubtitleProcessingStream and runSubtitleConversionStream. Please add a Streaming APIs section (e.g. immediately after “Pre-built Functions”) that:

  • Describes runSubtitleProcessingStream(items, filters?, config?)
    • Consumes a (async) generator, applies filters, and yields processed subtitle items.
  • Describes runSubtitleConversionStream(items, filters?, config?)
    • Produces incremental output (SRT/VTT/text/JSON fragments) as it processes the stream.

This aligns the docs with the implementations in

  • src/domain/media/subtitle-formats/subtitle-converter.ts: lines 351 (runSubtitleProcessingStream)
  • src/domain/media/subtitle-formats/subtitle-converter.ts: lines 446 (runSubtitleConversionStream)

Let me know if you’d like example snippets drafted for both functions.

🧰 Tools
🪛 LanguageTool

[style] ~80-~80: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...peline. #### .parallelFilter(filter) Adds a parallel filter stage to the pipeline...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~83-~83: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...pipeline. #### .collector(collector) Adds a collector stage to the pipeline. ###...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~86-~86: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...pipeline. #### .formatter(formatter) Adds a formatter stage to the pipeline. ###...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~100-~100: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...ssWithConfig(items, filters?, config?)` Processes subtitles with custom configuration. #...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 markdownlint-cli2 (0.17.2)

66-66: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


69-69: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


74-74: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


77-77: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


80-80: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


83-83: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


86-86: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


89-89: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


94-94: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


97-97: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


100-100: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/README.md around lines 62 to 102, the
README is missing documentation for the new stream-based APIs; add a "Streaming
APIs" section immediately after "Pre-built Functions" that documents
runSubtitleProcessingStream(items, filters?, config?) — note it consumes an
(async) generator, applies filters, and yields processed subtitle items — and
runSubtitleConversionStream(items, filters?, config?) — note it produces
incremental output (SRT/VTT/text/JSON fragments) as it processes the stream;
reference the implementations in
src/domain/media/subtitle-formats/subtitle-converter.ts at lines ~351 and ~446
and keep the descriptions concise and parallel to existing function docs.

Comment thread src/domain/media/subtitle-formats/README.md Outdated
Comment thread src/domain/media/subtitle-formats/README.md Outdated
Comment thread src/domain/media/subtitle-formats/subtitle-converter.test.ts Outdated
Comment thread src/domain/media/subtitle-formats/subtitle-converter.test.ts Outdated
Comment on lines +90 to +92
expect(result).toHaveLength(1) // Only speaker 1, no empty subtitles
expect(result[0]?.start).toBe(500) // Timing offset applied
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

test(expectations): update counts after fixing per-element filtering semantics

Filtering by speaker 1 should return both speaker-1 items (non-empty), not terminate after encountering a speaker-2 item.

-      expect(result).toHaveLength(1) // Only speaker 1, no empty subtitles
-      expect(result[0]?.start).toBe(500) // Timing offset applied
+      expect(result).toHaveLength(2) // Only speaker 1, no empty subtitles
+      expect(result[0]?.start).toBe(500) // First speaker 1, offset applied
+      expect(result[1]?.start).toBe(5500) // Second speaker 1, offset applied
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(result).toHaveLength(1) // Only speaker 1, no empty subtitles
expect(result[0]?.start).toBe(500) // Timing offset applied
})
expect(result).toHaveLength(2) // Only speaker 1, no empty subtitles
expect(result[0]?.start).toBe(500) // First speaker 1, offset applied
expect(result[1]?.start).toBe(5500) // Second speaker 1, offset applied
})
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-streaming.test.ts around lines 90
to 92, update the assertions to reflect that filtering by speaker 1 should
return both non-empty speaker-1 items (the filter is per-element and should not
stop at a speaker-2 item). Change the length expectation from 1 to 2 and adjust
the subsequent checks to assert both returned items exist and have the correct
start times (with the timing offset applied) instead of only checking result[0].

Comment on lines +104 to +105
expect(result).toHaveLength(0) // Only speaker 2, no empty subtitles
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

test(expectations): fix parallel pipeline expectation (should be 2 items for speaker 2)

Two items have speaker 2 and are non-empty; prefix doesn’t affect filtering.

-      expect(result).toHaveLength(0) // Only speaker 2, no empty subtitles
+      expect(result).toHaveLength(2) // Only speaker 2, no empty subtitles
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(result).toHaveLength(0) // Only speaker 2, no empty subtitles
})
expect(result).toHaveLength(2) // Only speaker 2, no empty subtitles
})
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-streaming.test.ts around lines 104
to 105, the test incorrectly expects zero results for speaker 2 but there are
two non-empty items with speaker 2; update the assertion to expect 2 items
(e.g., change to toHaveLength(2)) and adjust the inline comment to reflect that
two non-empty subtitles belong to speaker 2.

Comment on lines +149 to +151
expect(result).toHaveLength(1)
expect(result[0]?.speaker).toBe(1)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

test(expectations): fix filter-by-speaker expectation (should be 2)

There are two speaker-1 items.

-      expect(result).toHaveLength(1)
+      expect(result).toHaveLength(2)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(result).toHaveLength(1)
expect(result[0]?.speaker).toBe(1)
})
expect(result).toHaveLength(2)
expect(result[0]?.speaker).toBe(1)
})
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-streaming.test.ts around lines 149
to 151, the test assertions expect only one speaker-1 item but there are
actually two; update the expectations to reflect two results
(expect(result).toHaveLength(2)) and assert both entries are speaker 1 (e.g.,
check result[0].speaker and result[1].speaker are 1 or use a loop/array-every
assertion) so the test matches the actual filtered output.

Comment on lines +162 to +166
expect(result).toHaveLength(3) // One empty subtitle removed
expect(result.every((subtitle) => subtitle.text.trim().length > 0)).toBe(
true,
)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

test(expectations): fix remove-empty expectation (should be 4)

Only a single empty subtitle is removed; others remain.

-      expect(result).toHaveLength(3) // One empty subtitle removed
+      expect(result).toHaveLength(4) // One empty subtitle removed
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(result).toHaveLength(3) // One empty subtitle removed
expect(result.every((subtitle) => subtitle.text.trim().length > 0)).toBe(
true,
)
})
expect(result).toHaveLength(4) // One empty subtitle removed
expect(result.every((subtitle) => subtitle.text.trim().length > 0)).toBe(
true,
)
})
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-streaming.test.ts around lines 162
to 166, the test incorrectly expects 3 subtitles after removal even though only
one empty subtitle is removed; update the length expectation to 4 (change
expect(result).toHaveLength(3) to expect(result).toHaveLength(4)) and keep the
existing non-empty-text assertion as-is.

Comment on lines +248 to +251
expect(result).toHaveLength(1) // Only speaker 1, no empty subtitles
expect(result[0]?.start).toBe(2000) // Timing offset applied
expect(result[0]?.text).toBe('[PROCESSED] HELLO, WORLD!') // Text transformed
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

test(expectations): fix complex chain expectation (should be 2 for speaker 1)

Both speaker-1 items survive validation and removal; then offset and transforms apply.

-      expect(result).toHaveLength(1) // Only speaker 1, no empty subtitles
-      expect(result[0]?.start).toBe(2000) // Timing offset applied
-      expect(result[0]?.text).toBe('[PROCESSED] HELLO, WORLD!') // Text transformed
+      expect(result).toHaveLength(2) // Only speaker 1, no empty subtitles
+      expect(result[0]?.start).toBe(2000)
+      expect(result[0]?.text).toBe('[PROCESSED] HELLO, WORLD!')
+      expect(result[1]?.start).toBe(7000)
+      expect(result[1]?.text).toBe('[PROCESSED] THIS IS A TEST.')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(result).toHaveLength(1) // Only speaker 1, no empty subtitles
expect(result[0]?.start).toBe(2000) // Timing offset applied
expect(result[0]?.text).toBe('[PROCESSED] HELLO, WORLD!') // Text transformed
})
expect(result).toHaveLength(2) // Only speaker 1, no empty subtitles
expect(result[0]?.start).toBe(2000)
expect(result[0]?.text).toBe('[PROCESSED] HELLO, WORLD!')
expect(result[1]?.start).toBe(7000)
expect(result[1]?.text).toBe('[PROCESSED] THIS IS A TEST.')
})
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-streaming.test.ts around lines 248
to 251, the test incorrectly expects only one surviving speaker-1 subtitle;
update the assertions to expect two items (expect(result).toHaveLength(2)) and
add/adjust expectations for the second item (its start time after offset and its
transformed text) so both speaker-1 entries are asserted instead of only one.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

♻️ Duplicate comments (3)
src/domain/media/subtitle-formats/subtitle-filters.ts (2)

225-264: refactor: collapse double-pass array processing into a single pass

The current implementation iterates twice (filter + map) with duplicated logic. This is unnecessary and increases complexity.

Apply this diff to make it single-pass while preserving Option handling:

 export const applyFiltersToArray = (
   subtitles: SubtitleItem[],
   ...filters: Array<
     (subtitle: SubtitleItem) => SubtitleItem | Option.Option<SubtitleItem>
   >
 ): SubtitleItem[] => {
-  return subtitles
-    .filter((subtitle) => {
-      let current = subtitle
-      for (const filter of filters) {
-        const result = filter(current)
-        if (Option.isOption(result)) {
-          if (Option.isSome(result)) {
-            current = result.value
-          } else {
-            return false // Filtered out
-          }
-        } else {
-          current = result
-        }
-      }
-      return true
-    })
-    .map((subtitle) => {
-      let current = subtitle
-      for (const filter of filters) {
-        const result = filter(current)
-        if (Option.isOption(result)) {
-          if (Option.isSome(result)) {
-            current = result.value
-          } else {
-            return subtitle // Should not happen due to filter above
-          }
-        } else {
-          current = result
-        }
-      }
-      return current
-    })
+  const results: SubtitleItem[] = []
+  for (const subtitle of subtitles) {
+    let current = subtitle
+    let include = true
+    for (const filter of filters) {
+      const result = filter(current)
+      if (Option.isOption(result)) {
+        if (Option.isSome(result)) {
+          current = result.value
+        } else {
+          include = false
+          break
+        }
+      } else {
+        current = result
+      }
+    }
+    if (include) results.push(current)
+  }
+  return results
 }

541-541: fix: return domain error for unsupported formats in saveToFile

Use a tagged domain error instead of a generic Error to stay consistent with the error model.

Apply this diff:

-          default:
-            return E.fail(new Error(`Unsupported format: ${format}`))
+          default:
+            return E.fail(
+              new UnsupportedFormatError({
+                format,
+                supportedFormats: ['json', 'srt', 'vtt', 'plain-text'],
+              }),
+            )

Additionally, add the import at the top of this file:

import { UnsupportedFormatError } from './subtitle-formats.errors'
src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts (1)

1-1: test: import from @effect/vitest for Effect-aligned tests

Per repo guidelines, tests should import from @effect/vitest.

Apply this diff:

-import { describe, expect, it } from 'vitest'
+import { describe, expect, it } from '@effect/vitest'
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2dacfce and afbec2e.

📒 Files selected for processing (5)
  • src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-converter.test.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-converter.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-filters.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
src/**/*.ts

📄 CodeRabbit Inference Engine (.rules/code-style.mdc)

src/**/*.ts: Always use relative paths for internal imports; never use src/ aliases for internal imports in TypeScript files.
Use package imports only for external dependencies in TypeScript files.
Avoid obvious comments; never add comments that just repeat what the code is doing. Only add comments for complex business logic that needs explanation.
Use JSDoc for public APIs, not inline comments for obvious operations.
Add comments only for complex business logic, non-obvious workarounds, edge cases, or important context that affects future changes.
Use Effect.Service class extension pattern for services, include dependencies in service definition, and provide makeTestService for testing.
Use E.gen(function* () {}) for complex flows; yield effects directly without helper functions; use yield* for effects that return values.
Use Data.Tagged for domain errors, create specific error types for different failure cases, and handle errors explicitly with E.catchTags.
Use Effect's logging utilities instead of console.log; include relevant context in log messages and use appropriate log levels.
Add spans for observability using E.withSpan, include relevant attributes, and use descriptive span names.
Use PascalCase for service classes (e.g., BlogStore, SearchStore).
Use camelCase for functions and variables (e.g., getBlogById, blogPost).
Use UPPERCASE for constants (e.g., TABLE_NAME, DEFAULT_LIMIT).
Use descriptive names for functions and methods that indicate purpose.
Prefix usecases with action (e.g., createBlogPostUsecase, getBlogByIdUsecase).
Prefix handlers with HTTP method context (e.g., _handler, handler).
Use Effect Schema for all data validation; define schemas with explicit types and use branded types for domain identifiers.
Use branded types for domain identifiers and create factory functions with validation.
Access configuration through the Effect system, not directly via process.env.
Use pipe syntax for effect composition;...

Files:

  • src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
src/**/*

📄 CodeRabbit Inference Engine (.rules/code-style.mdc)

Use kebab-case for file names (e.g., blog.store.ts, get-blog-by-id.ts).

Files:

  • src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
src/**/*.test.ts

📄 CodeRabbit Inference Engine (.rules/code-style.mdc)

Always use @effect/vitest for Effect-based tests; use appropriate test variants (it.effect, it.scoped, it.live); mock services using makeTestService.

Files:

  • src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.rules/effect-service-architecture.mdc)

Always use relative paths for internal imports; never use src/ aliases for internal imports. Use package imports only for external dependencies.

Files:

  • src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
**/*.test.ts

📄 CodeRabbit Inference Engine (.rules/effect-testing-patterns.mdc)

**/*.test.ts: Always use @effect/vitest for tests involving Effect-based code; import test functions (describe, expect, it) from @effect/vitest, not regular vitest.
Always use relative paths for internal imports in test files; never use src/ aliases.
Use package imports for external dependencies in test files.
Test tagged errors with proper Effect patterns (using E.exit and getExitError).
Use it.effect for tests that need TestContext (TestClock, etc.) in Effect-TS test files.
Use it.scoped for tests involving resources that need proper cleanup in Effect-TS test files.
Use it.live for tests that need real system time and live services in Effect-TS test files.
Use FetchHttpClientTest from @t6c/lib-api for mocking HTTP responses in API client tests.
Use mock data from domain packages when available in tests.
Create reusable mock objects for complex data structures in tests.
Use proper branded type constructors for mock data in tests.
Declare variables outside generator functions when used in FetchHttpClientTest to prevent scoping issues in tests.
Use .pipe(E.exit) directly on service calls for cleaner error testing in tests; avoid nested E.exit(E.gen(...)) patterns.
Use Exit.isFailure(result) to check for failures and getExitError(result) to extract error details in tests.
Test all public methods of services, including both success and error scenarios, default parameters, edge cases, and validation.
Use appropriate HTTP status codes (200, 201, 404, 500) and match response structure to actual API responses in mock responses for tests.
Use descriptive test names that explain the scenario, following the pattern: 'should [action] [condition]'.
Group related tests in describe blocks by method.
Reuse mock data across tests when possible.
Create typed mock objects with proper constructors for tests.
Always provide required layers in correct order in tests (e.g., DefaultWithoutDependencies for services, MockConfigLayer for configurat...

Files:

  • src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
🧠 Learnings (24)
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use proper branded type constructors for mock data in tests.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Write pure functions without side effects; use Effect for operations with side effects; keep business logic separate from infrastructure.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Use proper Effect error handling patterns in utility functions.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use `it.effect` for tests that need TestContext (TestClock, etc.) in Effect-TS test files.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Use Effect-TS patterns for async operations in utility functions.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Access configuration through the Effect system, not directly via `process.env`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Always use `effect/vitest` for tests involving Effect-based code; import test functions (`describe`, `expect`, `it`) from `effect/vitest`, not regular `vitest`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use `E.gen(function* () {})` for complex flows; yield effects directly without helper functions; use `yield*` for effects that return values.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Log errors within the Effect system using `E.logError` or `E.log`, not `console.log`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:46:54.925Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-store-patterns.mdc:0-0
Timestamp: 2025-08-10T21:46:54.925Z
Learning: Applies to **/*.store.ts : Store classes should integrate with external services (e.g., Algolia, DynamoDB, S3) using Effect-TS patterns

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Use `yield* new ErrorClass()` in Effect.gen instead of throwing errors.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use Effect Schema for all data validation; define schemas with explicit types and use branded types for domain identifiers.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:48:12.331Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/schema-validation.mdc:0-0
Timestamp: 2025-08-10T21:48:12.331Z
Learning: Applies to **/*.types.ts|**/schema/**/*.ts|**/schemas/**/*.ts|**/*-schema.ts|**/*-schemas.ts|**/branded.ts|**/domain/**/*.ts : Always use `Schema as S` for Effect Schema imports

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:46:10.977Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/cursor-rule-maintainer.mdc:0-0
Timestamp: 2025-08-10T21:46:10.977Z
Learning: Applies to .cursor/rules/schema-validation.mdc : Add new Effect Schema patterns, validation improvements, branded type patterns, and error handling for schemas to `schema-validation.mdc`

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:48:12.331Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/schema-validation.mdc:0-0
Timestamp: 2025-08-10T21:48:12.331Z
Learning: Applies to **/schema/**/*.ts|**/schemas/**/*.ts|**/*-schema.ts|**/*-schemas.ts : Test schema validation with valid and invalid inputs using effect/vitest and Effect-based test patterns

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:47:41.747Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-usecase-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:41.747Z
Learning: Applies to **/*-usecase.ts : UPDATE/DELETE usecases must use E.orDie for unexpected errors and use only simple error logging with E.tapError(E.logError).

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-filters.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use `Data.Tagged` for domain errors, create specific error types for different failure cases, and handle errors explicitly with `E.catchTags`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-filters.ts
📚 Learning: 2025-08-10T21:45:41.119Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/api-client-testing.mdc:0-0
Timestamp: 2025-08-10T21:45:41.119Z
Learning: Applies to **/*api-client.test.ts : API client test files must import { describe, expect, it } from 'effect/vitest', { FetchHttpClientTest, getExitError } from 't6c/lib-api', { Effect as E, Exit } from 'effect', and { MockConfigLayer } from '../config.js'.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.test.ts : Always use `effect/vitest` for Effect-based tests; use appropriate test variants (`it.effect`, `it.scoped`, `it.live`); mock services using `makeTestService`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:45:41.119Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/api-client-testing.mdc:0-0
Timestamp: 2025-08-10T21:45:41.119Z
Learning: Applies to **/*api-client.test.ts : API client test files should follow the standard test structure: use 'it.effect' with an Effect generator, provide layers in the correct order, and assert results using 'expect'.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use `it.live` for tests that need real system time and live services in Effect-TS test files.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use `it.scoped` for tests involving resources that need proper cleanup in Effect-TS test files.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:46:38.101Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-service-architecture.mdc:0-0
Timestamp: 2025-08-10T21:46:38.101Z
Learning: Applies to **/stores/*-store.test.{ts,js} : Store testing should use Effect-TS testing utilities (e.g., `effect/vitest`), provide mock implementations for dependencies, and test store methods in isolation.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Test tagged errors with proper Effect patterns (using `E.exit` and `getExitError`).

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
🧬 Code Graph Analysis (5)
src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts (3)
src/domain/media/subtitle-formats/subtitle-converter.ts (6)
  • SubtitleItem (740-740)
  • validateSubtitleData (106-150)
  • processSubtitles (304-343)
  • SubtitleConverterLive (659-737)
  • runSubtitleProcessingStream (385-393)
  • runSubtitleConversionStream (480-487)
src/domain/media/subtitle-formats/subtitle-formats.errors.ts (2)
  • UnsupportedFormatError (43-48)
  • InvalidTimingError (50-52)
src/domain/media/subtitle-formats/subtitle-filters.ts (6)
  • replaceText (19-37)
  • addTimingOffset (45-51)
  • addPrefix (84-89)
  • filterBySpeaker (59-62)
  • applyFiltersToArray (225-264)
  • streamSubtitles (273-302)
src/domain/media/subtitle-formats/subtitle-converter.test.ts (4)
src/domain/media/subtitle-formats/subtitle-converter.ts (7)
  • SubtitleItem (740-740)
  • validateSubtitleData (106-150)
  • processSubtitles (304-343)
  • SubtitleConverterLive (659-737)
  • runSubtitleProcessingStream (385-393)
  • runSubtitleConversionStream (480-487)
  • addSpeakerInfo (187-198)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (2)
  • SubtitleItem (7-16)
  • SubtitleItem (64-64)
src/domain/media/subtitle-formats/subtitle-filters.ts (6)
  • applyFiltersToArray (225-264)
  • replaceText (19-37)
  • addTimingOffset (45-51)
  • filterBySpeaker (59-62)
  • addPrefix (84-89)
  • streamSubtitles (273-302)
src/domain/media/subtitle-formats/subtitle-formats.errors.ts (2)
  • UnsupportedFormatError (43-48)
  • InvalidTimingError (50-52)
src/domain/media/subtitle-formats/subtitle-converter.ts (2)
src/domain/media/subtitle-formats/subtitle-formats.errors.ts (4)
  • InvalidSubtitleDataError (37-41)
  • InvalidTimingError (50-52)
  • UnsupportedFormatError (43-48)
  • ConversionError (54-57)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (11)
  • SubtitleJson (21-21)
  • SubtitleJson (65-65)
  • SubtitleItem (7-16)
  • SubtitleItem (64-64)
  • ConversionOptions (31-42)
  • ConversionOptions (67-67)
  • SubtitleFormat (26-26)
  • SubtitleFormat (66-66)
  • SubtitleConversionResultSchema (54-57)
  • MultipleFormatResult (59-61)
  • MultipleFormatResult (68-70)
src/domain/media/subtitle-formats/subtitle-filters.ts (5)
src/domain/media/subtitle-formats/subtitle-converter.ts (3)
  • SubtitleItem (740-740)
  • formatTimeSrt (495-501)
  • formatTimeVtt (509-515)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (2)
  • SubtitleItem (7-16)
  • SubtitleItem (64-64)
src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts (3)
  • filter (169-172)
  • stream (161-164)
  • applyFilters (79-93)
src/steps/media/persist-subtitles.step.ts (1)
  • subtitlesJson (3-14)
src/usecases/media/transcribe-workflow.usecase.ts (2)
  • transcribeMediaStep (51-52)
  • persistSubtitlesStep (55-56)
src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts (3)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (2)
  • SubtitleItem (7-16)
  • SubtitleItem (64-64)
src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts (13)
  • createPipeline (266-268)
  • SubtitlePipeline (144-257)
  • createArrayPipeline (273-279)
  • PipelineConfig (43-47)
  • createCollector (121-128)
  • applyFilters (79-93)
  • formatToSrt (288-306)
  • formatToVtt (311-324)
  • formatToPlainText (329-331)
  • formatToJson (336-338)
  • processToSrt (373-382)
  • processToVtt (387-396)
  • processWithConfig (401-415)
src/domain/media/subtitle-formats/subtitle-filters.ts (7)
  • toUpperCase (152-155)
  • applyFilters (348-373)
  • filterBySpeakers (70-76)
  • addTimingOffset (45-51)
  • transformText (140-145)
  • capitalize (172-175)
  • validateSubtitle (202-216)
🔇 Additional comments (2)
src/domain/media/subtitle-formats/subtitle-filters.ts (1)

509-515: docs: reverseStream returns Effect, not a Stream transform

The JSDoc says “returns Stream transformation function” but the function returns Effect<Stream<...>>. Either adjust the docstring or refactor to return a Stream directly (e.g., Stream.fromIterableEffect(...)).

Would you prefer we change the signature to return a Stream (no Effect), or correct the JSDoc to reflect Effect?

src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts (1)

1-1127: test: solid, assertion-focused suite without noisy logs

This file aligns well with the guidelines (uses @effect/vitest, avoids console logs, asserts structure and content across formats and streams). Nicely done.

Comment on lines +616 to +617
`# File size: ${new Blob([content]).size} bytes`,
].join('\n')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

test: avoid Blob in Node test environments; use Buffer.byteLength

Blob may not be available or behaves differently across environments. Buffer.byteLength is stable in Node.

Apply this diff:

-            `# File size: ${new Blob([content]).size} bytes`,
+            `# File size: ${Buffer.byteLength(content, 'utf8')} bytes`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`# File size: ${new Blob([content]).size} bytes`,
].join('\n')
`# File size: ${Buffer.byteLength(content, 'utf8')} bytes`,
].join('\n')
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-converter.test.ts around lines
616-617, the test uses new Blob([content]).size to compute file size which is
flaky in Node test environments; replace that expression with
Buffer.byteLength(content, 'utf8') (or Buffer.from(content).length) so the test
uses Node-stable byte length; update the string interpolation to use
Buffer.byteLength(content, 'utf8') and run tests to verify.

Comment thread src/domain/media/subtitle-formats/subtitle-converter.test.ts Outdated
Comment on lines +1333 to +1341
SubtitleConverterLive.convert(subtitles as SubtitleItem[], 'json'),
),
E.catchAll((error) => E.succeed(error)),
)

// The first validation error will be InvalidTimingError for negative start time
expect(result).toBeInstanceOf(InvalidTimingError)
}),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

test: prefer Effect Exit pattern for error assertions

Using E.exit helps assert tagged errors without side effects or manual catchAll branches.

Example:

const exit = yield* SubtitleConverterLive.convert(sampleSubtitles, 'unsupported' as never).pipe(E.exit)
expect(Exit.isFailure(exit)).toBe(true)
expect(getExitError(exit)).toBeInstanceOf(UnsupportedFormatError)

Apply similarly for invalid timing scenarios to assert InvalidTimingError.

Also applies to: 1133-1141

🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-converter.test.ts around lines
1333–1341 (and similarly at 1133–1141), the tests use E.catchAll/E.succeed to
get errors instead of the Effect Exit pattern; update each test to call
.pipe(E.exit) (or yield* ... .pipe(E.exit)), assert Exit.isFailure(exit) is
true, and then use getExitError(exit) to assert the specific error class (e.g.,
InvalidTimingError or UnsupportedFormatError) so you assert tagged errors
without side effects or manual catchAll branches.

Comment thread src/domain/media/subtitle-formats/subtitle-converter.test.ts Outdated
Comment thread src/domain/media/subtitle-formats/subtitle-converter.test.ts Outdated
Comment on lines +111 to +118
export const filterByDuration =
(minDuration: number, maxDuration: number) =>
(subtitle: SubtitleItem): Option.Option<SubtitleItem> => {
const duration = subtitle.end - subtitle.start
return duration >= minDuration && duration <= maxDuration
? Option.some(subtitle)
: Option.none()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

feat: validate duration bounds in filterByDuration

Guard against min > max to avoid surprising “always none” behaviour.

Apply this diff:

 export const filterByDuration =
   (minDuration: number, maxDuration: number) =>
   (subtitle: SubtitleItem): Option.Option<SubtitleItem> => {
+    if (minDuration > maxDuration) {
+      const [min, max] = [maxDuration, minDuration]
+      const duration = subtitle.end - subtitle.start
+      return duration >= min && duration <= max ? Option.some(subtitle) : Option.none()
+    }
     const duration = subtitle.end - subtitle.start
     return duration >= minDuration && duration <= maxDuration
       ? Option.some(subtitle)
       : Option.none()
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const filterByDuration =
(minDuration: number, maxDuration: number) =>
(subtitle: SubtitleItem): Option.Option<SubtitleItem> => {
const duration = subtitle.end - subtitle.start
return duration >= minDuration && duration <= maxDuration
? Option.some(subtitle)
: Option.none()
}
export const filterByDuration =
(minDuration: number, maxDuration: number) =>
(subtitle: SubtitleItem): Option.Option<SubtitleItem> => {
if (minDuration > maxDuration) {
const [min, max] = [maxDuration, minDuration]
const duration = subtitle.end - subtitle.start
return duration >= min && duration <= max ? Option.some(subtitle) : Option.none()
}
const duration = subtitle.end - subtitle.start
return duration >= minDuration && duration <= maxDuration
? Option.some(subtitle)
: Option.none()
}
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-filters.ts around lines 111 to
118, the filterByDuration function doesn't validate the min/max bounds so
passing minDuration > maxDuration makes it always return none; add a guard at
the start that checks if minDuration > maxDuration and throw a clear Error (e.g.
"filterByDuration: minDuration must be <= maxDuration") to surface invalid
input, then proceed with the existing duration calculation and Option return.

Comment on lines +324 to +341
export const applyFilter =
<T extends SubtitleItem>(
filter: (subtitle: SubtitleItem) => T | Option.Option<T>,
) =>
(stream: Stream.Stream<SubtitleItem, never, never>) =>
stream.pipe(
Stream.mapEffect((subtitle) => {
const result = filter(subtitle)
if (Option.isOption(result)) {
return Option.isSome(result)
? E.succeed(result.value)
: E.fail('filtered')
}
return E.succeed(result)
}),
Stream.catchAll(() => Stream.empty),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

refactor: avoid string-sentinel errors in stream filters; use filterMap or a tagged error

Returning E.fail('filtered') then catching-all hides other errors and loses type-safety. Either:

  • Use Stream.filterMap to keep everything in the Option domain, or
  • Fail with a specific Tagged error (e.g., FilteredOutError) and catch only that tag.

Example using filterMap for applyFilter:

return stream.pipe(
  Stream.filterMap((subtitle) => {
    const result = filter(subtitle)
    return Option.isOption(result) ? result : Option.some(result)
  })
)

And for applyFilters, perform the same within the mapping function by building a final Option and filterMap it.

Also applies to: 348-374

Comment on lines +612 to +629
const formatTimeSrt = (ms: number): string => {
const hours = Math.floor(ms / 3600000)
const minutes = Math.floor((ms % 3600000) / 60000)
const seconds = Math.floor((ms % 60000) / 1000)
const milliseconds = ms % 1000
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')},${milliseconds.toString().padStart(3, '0')}`
}

/**
* Formats time in milliseconds to VTT format (HH:MM:SS.mmm)
*/
const formatTimeVtt = (ms: number): string => {
const hours = Math.floor(ms / 3600000)
const minutes = Math.floor((ms % 3600000) / 60000)
const seconds = Math.floor((ms % 60000) / 1000)
const milliseconds = ms % 1000
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

refactor: deduplicate time-formatting; import shared helpers

Time formatting logic is duplicated here and in subtitle-converter.ts. Prefer a single source of truth.

Apply this diff to remove the local implementations:

-/**
- * Formats time in milliseconds to SRT format (HH:MM:SS,mmm)
- */
-const formatTimeSrt = (ms: number): string => {
-  const hours = Math.floor(ms / 3600000)
-  const minutes = Math.floor((ms % 3600000) / 60000)
-  const seconds = Math.floor((ms % 60000) / 1000)
-  const milliseconds = ms % 1000
-  return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')},${milliseconds.toString().padStart(3, '0')}`
-}
-
-/**
- * Formats time in milliseconds to VTT format (HH:MM:SS.mmm)
- */
-const formatTimeVtt = (ms: number): string => {
-  const hours = Math.floor(ms / 3600000)
-  const minutes = Math.floor((ms % 3600000) / 60000)
-  const seconds = Math.floor((ms % 60000) / 1000)
-  const milliseconds = ms % 1000
-  return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`
-}

Then add this import near the top of the file:

import { formatTimeSrt, formatTimeVtt } from './subtitle-converter'
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-filters.ts around lines 612 to
629, the local implementations of formatTimeSrt and formatTimeVtt duplicate
logic found in subtitle-converter.ts; remove these two local functions and
replace them by importing the shared helpers. At the top of the file add an
import: import { formatTimeSrt, formatTimeVtt } from './subtitle-converter';
ensure any local references remain unchanged and that the file compiles after
removing the duplicated functions.

Comment thread src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
Comment on lines +332 to +357
describe('Performance Tests', () => {
it('should handle large datasets efficiently', () => {
const largeDataset: SubtitleItem[] = []
for (let i = 0; i < 100; i++) {
largeDataset.push({
start: i * 1000,
end: (i + 1) * 1000,
text: `Subtitle ${i + 1}`,
speaker: (i % 3) + 1,
})
}

const startTime = Date.now()
const result = createArrayPipeline(largeDataset)
.filter(
applyFilters(validateSubtitle, toUpperCase, addPrefix('[PROCESSED]')),
)
.collector(createCollector())
.formatter(formatToSrt)
.execute()
const endTime = Date.now()

expect(result).toBeInstanceOf(Array)
expect((result as string[]).length).toBeGreaterThan(0)
expect(endTime - startTime).toBeLessThan(1000) // Should complete within 1 second
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

test: avoid time-based performance assertions

Using Date.now in tests is brittle and environment-dependent. Prefer asserting on functional outcomes or using fake timers.

Consider removing the duration assertion or relaxing it behind a much higher threshold, or use a benchmark outside the unit test suite.

🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts around
lines 332 to 357, the test uses a real time measurement (Date.now) and asserts
the duration is under 1 second which is brittle; remove the timing assertion and
keep only functional assertions (result is an array and has length), or if you
must assert performance move this to a dedicated benchmark/integration test or
use fake timers and deterministic workload; update the test to either drop
expect(endTime - startTime).toBeLessThan(1000) or replace it with a
non-time-based check or a much higher, documented threshold and/or mark the test
as flaky/slow so it doesn’t run in regular unit test runs.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (12)
src/domain/media/subtitle-formats/subtitle-converter.test.ts (2)

611-617: test: avoid Blob in Node/Vitest; prefer Buffer.byteLength for size

Use Buffer.byteLength for stable sizing across Node test environments. This was previously raised; keeping the replacement consistent.

-            `# File size: ${new Blob([content]).size} bytes`,
+            `# File size: ${Buffer.byteLength(content, 'utf8')} bytes`,

1130-1138: test: prefer Exit pattern for unsupported format error

Assert UnsupportedFormatError using Exit + Cause, avoiding catchAll on the entire generator.

-    it.effect('should reject unsupported format', () =>
-      E.gen(function* () {
-        const result = yield* SubtitleConverterLive.convert(
-          sampleSubtitles,
-          'unsupported' as never,
-        )
-        expect(result).toBeInstanceOf(UnsupportedFormatError)
-      }).pipe(E.catchAll(E.succeed)),
-    )
+    it.effect('should reject unsupported format', () =>
+      E.gen(function* () {
+        const exit = yield* SubtitleConverterLive.convert(
+          sampleSubtitles,
+          'unsupported' as never,
+        ).pipe(E.exit)
+        expect(Exit.isFailure(exit)).toBe(true)
+        if (Exit.isFailure(exit)) {
+          const errorOpt = Cause.failureOption(exit.cause)
+          expect(Option.isSome(errorOpt)).toBe(true)
+          if (Option.isSome(errorOpt)) {
+            expect(errorOpt.value).toBeInstanceOf(UnsupportedFormatError)
+          }
+        }
+      }),
+    )
src/domain/media/subtitle-formats/subtitle-filters.ts (7)

70-76: Use Set for O(1) speaker lookup performance

When dealing with potentially large speaker ID arrays, using Set.has() provides O(1) lookup complexity compared to Array.includes() O(n) complexity.

 export const filterBySpeakers =
   (speakerIds: number[]) =>
-  (subtitle: SubtitleItem): Option.Option<SubtitleItem> =>
-    typeof subtitle.speaker === 'number' &&
-    speakerIds.includes(subtitle.speaker)
-      ? Option.some(subtitle)
-      : Option.none()
+  (subtitle: SubtitleItem): Option.Option<SubtitleItem> => {
+    const speakerSet = new Set(speakerIds)
+    return typeof subtitle.speaker === 'number' &&
+      speakerSet.has(subtitle.speaker)
+        ? Option.some(subtitle)
+        : Option.none()
+  }

84-102: Prevent double spaces in prefix/suffix operations

The current implementation may introduce double spaces if the prefix/suffix already contains trailing/leading whitespace.

 export const addPrefix =
   (prefix: string) =>
   (subtitle: SubtitleItem): SubtitleItem => ({
     ...subtitle,
-    text: `${prefix} ${subtitle.text}`,
+    text: prefix.endsWith(' ') ? `${prefix}${subtitle.text}` : `${prefix} ${subtitle.text}`,
   })

 export const addSuffix =
   (suffix: string) =>
   (subtitle: SubtitleItem): SubtitleItem => ({
     ...subtitle,
-    text: `${subtitle.text} ${suffix}`,
+    text: suffix.startsWith(' ') ? `${subtitle.text}${suffix}` : `${subtitle.text} ${suffix}`,
   })

111-118: Add validation for duration bounds

Consider validating that minDuration <= maxDuration to prevent unexpected filtering behaviour.

 export const filterByDuration =
   (minDuration: number, maxDuration: number) =>
   (subtitle: SubtitleItem): Option.Option<SubtitleItem> => {
+    if (minDuration > maxDuration) {
+      throw new Error('filterByDuration: minDuration must be <= maxDuration')
+    }
     const duration = subtitle.end - subtitle.start
     return duration >= minDuration && duration <= maxDuration
       ? Option.some(subtitle)
       : Option.none()
   }

225-237: Refactor to single-pass iteration for efficiency

The function currently iterates through the array twice with identical logic. This can be optimised to a single pass.

 export const applyFiltersToArray = (
   subtitles: SubtitleItem[],
   ...filters: Array<
     (subtitle: SubtitleItem) => SubtitleItem | Option.Option<SubtitleItem>
   >
 ): SubtitleItem[] => {
-  return subtitles
-    .filter((subtitle) => Option.isSome(applyFiltersToItem(subtitle, filters)))
-    .map((subtitle) => {
-      const result = applyFiltersToItem(subtitle, filters)
-      return Option.isSome(result) ? result.value : subtitle
-    })
+  const results: SubtitleItem[] = []
+  for (const subtitle of subtitles) {
+    const result = applyFiltersToItem(subtitle, filters)
+    if (Option.isSome(result)) {
+      results.push(result.value)
+    }
+  }
+  return results
 }

281-297: Use type-safe filtering with Stream.filterMap

Using string-sentinel errors ('filtered') loses type safety. Consider using Stream.filterMap to keep filtering within the Option domain.

 export const applyFilter =
   <T extends SubtitleItem>(
     filter: (subtitle: SubtitleItem) => T | Option.Option<T>,
   ) =>
   (stream: Stream.Stream<SubtitleItem, never, never>) =>
     stream.pipe(
-      Stream.mapEffect((subtitle) => {
+      Stream.filterMap((subtitle) => {
         const result = filter(subtitle)
-        if (Option.isOption(result)) {
-          return Option.isSome(result)
-            ? E.succeed(result.value)
-            : E.fail('filtered')
-        }
-        return E.succeed(result)
-      }),
-      Stream.catchAll(() => Stream.empty),
+        return Option.isOption(result) ? result : Option.some(result)
+      })
     )

Apply similar refactoring to applyFilters:

 export const applyFilters =
   (
     ...filters: Array<
       (subtitle: SubtitleItem) => SubtitleItem | Option.Option<SubtitleItem>
     >
   ) =>
   (stream: Stream.Stream<SubtitleItem, never, never>) =>
     stream.pipe(
-      Stream.mapEffect((subtitle) => {
-        const result = applyFiltersToItem(subtitle, filters)
-        return Option.isSome(result)
-          ? E.succeed(result.value)
-          : E.fail('filtered')
-      }),
-      Stream.catchAll(() => Stream.empty),
+      Stream.filterMap((subtitle) => applyFiltersToItem(subtitle, filters))
     )

Also applies to: 305-320


488-508: Use domain-specific error types for better error handling

Replace generic Error with the domain-specific UnsupportedFormatError for consistency with the codebase error handling patterns.

+import { UnsupportedFormatError } from './subtitle-formats.errors'

 export const saveToFile =
   (format: 'json' | 'srt' | 'vtt' | 'plain-text') =>
   (stream: Stream.Stream<SubtitleItem, never, never>) =>
     stream.pipe(
       Stream.runCollect,
       E.map((chunk) => Array.from(chunk)),
       E.flatMap((subtitles) => {
         switch (format) {
           case 'json':
             return E.succeed(JSON.stringify(subtitles, null, 2))
           case 'srt':
             return E.succeed(convertToSrtFormat(subtitles))
           case 'vtt':
             return E.succeed(convertToVttFormat(subtitles))
           case 'plain-text':
             return E.succeed(convertToPlainTextFormat(subtitles))
           default:
-            return E.fail(new Error(`Unsupported format: ${format}`))
+            return E.fail(new UnsupportedFormatError({
+              format: format as string,
+              supportedFormats: ['json', 'srt', 'vtt', 'plain-text']
+            }))
         }
       }),
     )

573-590: Remove duplicate time formatting functions

These time formatting functions are duplicated across multiple files. Import them from a shared module to maintain DRY principles.

+import { formatTimeSrt, formatTimeVtt } from './subtitle-converter'
+
 // ... rest of the file ...

-/**
- * Formats time in milliseconds to SRT format (HH:MM:SS,mmm)
- */
-const formatTimeSrt = (ms: number): string => {
-  const hours = Math.floor(ms / 3600000)
-  const minutes = Math.floor((ms % 3600000) / 60000)
-  const seconds = Math.floor((ms % 60000) / 1000)
-  const milliseconds = ms % 1000
-  return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')},${milliseconds.toString().padStart(3, '0')}`
-}
-
-/**
- * Formats time in milliseconds to VTT format (HH:MM:SS.mmm)
- */
-const formatTimeVtt = (ms: number): string => {
-  const hours = Math.floor(ms / 3600000)
-  const minutes = Math.floor((ms % 3600000) / 60000)
-  const seconds = Math.floor((ms % 60000) / 1000)
-  const milliseconds = ms % 1000
-  return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`
-}
src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts (3)

98-116: Avoid E.runSync in pure functions - return Effect instead

Using E.runSync can throw exceptions if the Effect fails. Return an Effect to allow proper error handling by the caller.

 export const processParallel =
   (filter: SubtitleFilter) =>
-  (items: SubtitleItem[]): SubtitleItem[] => {
+  (items: SubtitleItem[]): E.Effect<SubtitleItem[], never, never> => {
     const program = E.all(
       items.map((item) => {
         const result = applySingleFilter(filter)(item)
         if (Option.isSome(result)) {
           return E.succeed(result.value)
         }
         return E.succeed(null)
       }),
     ).pipe(
       E.map((results) =>
         results.filter((item): item is SubtitleItem => item !== null),
       ),
     )
 
-    return E.runSync(program)
+    return program
   }

Then update the execute method to handle the Effect properly:

     const handleFilter = (
       stage: FilterStage,
       items: SubtitleItem[],
     ): SubtitleItem[] => {
       if (this.config.parallelProcessing) {
-        return processParallel(stage.filter)(items)
+        return E.runSync(processParallel(stage.filter)(items))
       }
       // ... rest of the method
     }

287-290: Remove unused exhaustive check in default case

The default case should either throw an error or use TypeScript's satisfies operator for exhaustiveness checking.

         default: {
-          // This should never happen due to TypeScript's exhaustive checking
-          break
+          // TypeScript's exhaustive checking ensures this is never reached
+          stage satisfies never
+          break
         }

386-403: Extract duplicate time formatting to shared utilities

These time formatting functions duplicate logic from subtitle-converter.ts. Import them instead to maintain a single source of truth.

+import { formatTimeSrt, formatTimeVtt } from './subtitle-converter'
+
 // ... rest of the file ...

-/**
- * Formats time in milliseconds to SRT format (HH:MM:SS,mmm)
- */
-const formatTimeSrt = (ms: number): string => {
-  const hours = Math.floor(ms / 3600000)
-  const minutes = Math.floor((ms % 3600000) / 60000)
-  const seconds = Math.floor((ms % 60000) / 1000)
-  const milliseconds = ms % 1000
-  return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')},${milliseconds.toString().padStart(3, '0')}`
-}
-
-/**
- * Formats time in milliseconds to VTT format (HH:MM:SS.mmm)
- */
-const formatTimeVtt = (ms: number): string => {
-  const hours = Math.floor(ms / 3600000)
-  const minutes = Math.floor((ms % 3600000) / 60000)
-  const seconds = Math.floor((ms % 60000) / 1000)
-  const milliseconds = ms % 1000
-  return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`
-}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between afbec2e and 3c8e3c9.

📒 Files selected for processing (3)
  • src/domain/media/subtitle-formats/subtitle-converter.test.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-filters.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
src/**/*.ts

📄 CodeRabbit Inference Engine (.rules/code-style.mdc)

src/**/*.ts: Always use relative paths for internal imports; never use src/ aliases for internal imports in TypeScript files.
Use package imports only for external dependencies in TypeScript files.
Avoid obvious comments; never add comments that just repeat what the code is doing. Only add comments for complex business logic that needs explanation.
Use JSDoc for public APIs, not inline comments for obvious operations.
Add comments only for complex business logic, non-obvious workarounds, edge cases, or important context that affects future changes.
Use Effect.Service class extension pattern for services, include dependencies in service definition, and provide makeTestService for testing.
Use E.gen(function* () {}) for complex flows; yield effects directly without helper functions; use yield* for effects that return values.
Use Data.Tagged for domain errors, create specific error types for different failure cases, and handle errors explicitly with E.catchTags.
Use Effect's logging utilities instead of console.log; include relevant context in log messages and use appropriate log levels.
Add spans for observability using E.withSpan, include relevant attributes, and use descriptive span names.
Use PascalCase for service classes (e.g., BlogStore, SearchStore).
Use camelCase for functions and variables (e.g., getBlogById, blogPost).
Use UPPERCASE for constants (e.g., TABLE_NAME, DEFAULT_LIMIT).
Use descriptive names for functions and methods that indicate purpose.
Prefix usecases with action (e.g., createBlogPostUsecase, getBlogByIdUsecase).
Prefix handlers with HTTP method context (e.g., _handler, handler).
Use Effect Schema for all data validation; define schemas with explicit types and use branded types for domain identifiers.
Use branded types for domain identifiers and create factory functions with validation.
Access configuration through the Effect system, not directly via process.env.
Use pipe syntax for effect composition;...

Files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts
src/**/*

📄 CodeRabbit Inference Engine (.rules/code-style.mdc)

Use kebab-case for file names (e.g., blog.store.ts, get-blog-by-id.ts).

Files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts
src/**/*.test.ts

📄 CodeRabbit Inference Engine (.rules/code-style.mdc)

Always use @effect/vitest for Effect-based tests; use appropriate test variants (it.effect, it.scoped, it.live); mock services using makeTestService.

Files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.rules/effect-service-architecture.mdc)

Always use relative paths for internal imports; never use src/ aliases for internal imports. Use package imports only for external dependencies.

Files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts
**/*.test.ts

📄 CodeRabbit Inference Engine (.rules/effect-testing-patterns.mdc)

**/*.test.ts: Always use @effect/vitest for tests involving Effect-based code; import test functions (describe, expect, it) from @effect/vitest, not regular vitest.
Always use relative paths for internal imports in test files; never use src/ aliases.
Use package imports for external dependencies in test files.
Test tagged errors with proper Effect patterns (using E.exit and getExitError).
Use it.effect for tests that need TestContext (TestClock, etc.) in Effect-TS test files.
Use it.scoped for tests involving resources that need proper cleanup in Effect-TS test files.
Use it.live for tests that need real system time and live services in Effect-TS test files.
Use FetchHttpClientTest from @t6c/lib-api for mocking HTTP responses in API client tests.
Use mock data from domain packages when available in tests.
Create reusable mock objects for complex data structures in tests.
Use proper branded type constructors for mock data in tests.
Declare variables outside generator functions when used in FetchHttpClientTest to prevent scoping issues in tests.
Use .pipe(E.exit) directly on service calls for cleaner error testing in tests; avoid nested E.exit(E.gen(...)) patterns.
Use Exit.isFailure(result) to check for failures and getExitError(result) to extract error details in tests.
Test all public methods of services, including both success and error scenarios, default parameters, edge cases, and validation.
Use appropriate HTTP status codes (200, 201, 404, 500) and match response structure to actual API responses in mock responses for tests.
Use descriptive test names that explain the scenario, following the pattern: 'should [action] [condition]'.
Group related tests in describe blocks by method.
Reuse mock data across tests when possible.
Create typed mock objects with proper constructors for tests.
Always provide required layers in correct order in tests (e.g., DefaultWithoutDependencies for services, MockConfigLayer for configurat...

Files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
🧠 Learnings (28)
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Test all public methods of services, including both success and error scenarios, default parameters, edge cases, and validation.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use proper branded type constructors for mock data in tests.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Write pure functions without side effects; use Effect for operations with side effects; keep business logic separate from infrastructure.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Use proper Effect error handling patterns in utility functions.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use `it.effect` for tests that need TestContext (TestClock, etc.) in Effect-TS test files.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Use Effect-TS patterns for async operations in utility functions.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Access configuration through the Effect system, not directly via `process.env`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Always use `effect/vitest` for tests involving Effect-based code; import test functions (`describe`, `expect`, `it`) from `effect/vitest`, not regular `vitest`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use `E.gen(function* () {})` for complex flows; yield effects directly without helper functions; use `yield*` for effects that return values.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Log errors within the Effect system using `E.logError` or `E.log`, not `console.log`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:46:54.925Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-store-patterns.mdc:0-0
Timestamp: 2025-08-10T21:46:54.925Z
Learning: Applies to **/*.store.ts : Store classes should integrate with external services (e.g., Algolia, DynamoDB, S3) using Effect-TS patterns

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Use `yield* new ErrorClass()` in Effect.gen instead of throwing errors.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use `.pipe(E.exit)` directly on service calls for cleaner error testing in tests; avoid nested `E.exit(E.gen(...))` patterns.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:45:41.119Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/api-client-testing.mdc:0-0
Timestamp: 2025-08-10T21:45:41.119Z
Learning: Applies to **/*api-client.test.ts : Use the clean exit pattern for error testing in API client test files: apply '.pipe(E.exit)' directly to service calls, avoid nested 'E.exit(E.gen(...))', and keep layer provision at the test level.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:45:41.119Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/api-client-testing.mdc:0-0
Timestamp: 2025-08-10T21:45:41.119Z
Learning: Applies to **/*api-client.test.ts : Error tests in API client test files should use the preferred pattern: call '.pipe(E.exit)' on the service call, check 'Exit.isFailure(result)', extract the error with 'getExitError', and assert error details.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Test tagged errors with proper Effect patterns (using `E.exit` and `getExitError`).

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:45:41.119Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/api-client-testing.mdc:0-0
Timestamp: 2025-08-10T21:45:41.119Z
Learning: Applies to **/*api-client.test.ts : When asserting errors in API client test files, check for failure with 'Exit.isFailure(result)', extract error details with 'getExitError', and assert error type and message.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use `Exit.isFailure(result)` to check for failures and `getExitError(result)` to extract error details in tests.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:45:41.119Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/api-client-testing.mdc:0-0
Timestamp: 2025-08-10T21:45:41.119Z
Learning: Applies to **/*api-client.test.ts : API client test files must import { describe, expect, it } from 'effect/vitest', { FetchHttpClientTest, getExitError } from 't6c/lib-api', { Effect as E, Exit } from 'effect', and { MockConfigLayer } from '../config.js'.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:45:41.119Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/api-client-testing.mdc:0-0
Timestamp: 2025-08-10T21:45:41.119Z
Learning: Applies to **/*api-client.test.ts : API client test files should follow the standard test structure: use 'it.effect' with an Effect generator, provide layers in the correct order, and assert results using 'expect'.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:41.747Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-usecase-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:41.747Z
Learning: Applies to **/*-usecase.ts : Always use the required observability pattern in usecase files: .pipe(E.tapError(E.logError), E.orDie or let domain errors bubble up, E.withSpan with only primitive attributes).

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-filters.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use Effect's logging utilities instead of `console.log`; include relevant context in log messages and use appropriate log levels.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use pipe syntax for effect composition; chain operations logically and keep pipes readable with proper formatting.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use `it.scoped` for tests involving resources that need proper cleanup in Effect-TS test files.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:46:38.101Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-service-architecture.mdc:0-0
Timestamp: 2025-08-10T21:46:38.101Z
Learning: Applies to **/usecase.test.{ts,js} : Usecase testing should use Effect-TS testing utilities, provide mock store implementations, and verify business logic and logging.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.test.ts
📚 Learning: 2025-08-10T21:47:41.747Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-usecase-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:41.747Z
Learning: Applies to **/*-usecase.ts : UPDATE/DELETE usecases must use E.orDie for unexpected errors and use only simple error logging with E.tapError(E.logError).

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-filters.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use `Data.Tagged` for domain errors, create specific error types for different failure cases, and handle errors explicitly with `E.catchTags`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-filters.ts
📚 Learning: 2025-08-10T21:47:41.747Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-usecase-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:41.747Z
Learning: Applies to **/*-usecase.ts : Always use E.tapError(E.logError) for error logging in usecase files; never use custom error messages, JSON.stringify, or verbose logging.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-filters.ts
🧬 Code Graph Analysis (3)
src/domain/media/subtitle-formats/subtitle-converter.test.ts (4)
src/domain/media/subtitle-formats/subtitle-converter.ts (7)
  • SubtitleItem (740-740)
  • validateSubtitleData (106-150)
  • processSubtitles (304-343)
  • SubtitleConverterLive (659-737)
  • runSubtitleProcessingStream (385-393)
  • runSubtitleConversionStream (480-487)
  • addSpeakerInfo (187-198)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (2)
  • SubtitleItem (7-16)
  • SubtitleItem (64-64)
src/domain/media/subtitle-formats/subtitle-filters.ts (6)
  • applyFiltersToArray (225-237)
  • replaceText (19-37)
  • addTimingOffset (45-51)
  • filterBySpeaker (59-62)
  • addPrefix (84-89)
  • streamSubtitles (246-259)
src/domain/media/subtitle-formats/subtitle-formats.errors.ts (2)
  • UnsupportedFormatError (43-48)
  • InvalidTimingError (50-52)
src/domain/media/subtitle-formats/subtitle-filters.ts (4)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (2)
  • SubtitleItem (7-16)
  • SubtitleItem (64-64)
src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts (4)
  • filter (169-172)
  • stream (161-164)
  • applyFilters (79-93)
  • applySingleFilter (66-74)
src/steps/media/persist-subtitles.step.ts (1)
  • subtitlesJson (3-14)
src/usecases/media/transcribe-workflow.usecase.ts (2)
  • transcribeMediaStep (51-52)
  • persistSubtitlesStep (55-56)
src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts (3)
src/domain/media/subtitle-formats/subtitle-converter.ts (3)
  • SubtitleItem (740-740)
  • formatTimeSrt (495-501)
  • formatTimeVtt (509-515)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (2)
  • SubtitleItem (7-16)
  • SubtitleItem (64-64)
src/domain/media/subtitle-formats/subtitle-filters.ts (1)
  • applyFilters (305-320)
🔇 Additional comments (2)
src/domain/media/subtitle-formats/subtitle-converter.test.ts (2)

1-21: test: imports and relative paths — LGTM

  • Uses @effect/vitest correctly.
  • Internal imports are all relative.
  • External packages are used via package imports only.

1491-1513: test: add coverage for unsupported format via stream conversion

We assert timing errors in streaming, but not UnsupportedFormatError for runSubtitleConversionStream. Add a test to cover this error path to fully exercise typed errors.

You can append this test near the other stream conversion tests:

it.effect('should surface UnsupportedFormatError in stream conversion', () =>
  E.gen(function* () {
    const result = yield* runSubtitleConversionStream(
      sampleSubtitles,
      'unsupported' as never,
    )
    const hasError = typeof result === 'object' && result !== null && 'error' in result
    expect(hasError).toBe(true)
    if (hasError && 'error' in result) {
      expect(result.error).toBeInstanceOf(UnsupportedFormatError)
    }
  }),
)

If helpful, I can push a patch with this addition.

⛔ Skipped due to learnings
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Test tagged errors with proper Effect patterns (using `E.exit` and `getExitError`).

Comment on lines +67 to +75
it.effect('should reject invalid subtitle data', () =>
E.gen(function* () {
const result = yield* validateSubtitleData(
invalidSubtitles as SubtitleItem[],
)
expect('cause' in result).toBe(true)
}).pipe(E.catchAll(E.succeed)),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

test: prefer Exit pattern over catchAll for error assertions (invalid data)

Adopt the Effect Exit pattern for cleaner, side‑effect‑free error assertions (per testing guidelines). This removes the need to pipe the whole generator through catchAll.

Apply this diff:

- import { Effect as E } from 'effect'
+ import { Effect as E, Cause, Exit } from 'effect'
@@
-    it.effect('should reject invalid subtitle data', () =>
-      E.gen(function* () {
-        const result = yield* validateSubtitleData(
-          invalidSubtitles as SubtitleItem[],
-        )
-        expect('cause' in result).toBe(true)
-      }).pipe(E.catchAll(E.succeed)),
-    )
+    it.effect('should reject invalid subtitle data', () =>
+      E.gen(function* () {
+        const exit = yield* validateSubtitleData(
+          invalidSubtitles as SubtitleItem[],
+        ).pipe(E.exit)
+        expect(Exit.isFailure(exit)).toBe(true)
+      }),
+    )

Also applies to: 2-2

🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-converter.test.ts around lines 67
to 75, the test currently pipes the generator through E.catchAll to swallow
failures; replace this with the Effect Exit pattern: run or exit the Effect to
obtain its Exit value (without catchAll) and assert on that Exit being a failure
(e.g., check exit indicates a failure or that 'cause' exists). Remove the
.pipe(E.catchAll(E.succeed)) usage and update the expectation to inspect the
Exit directly.

Comment on lines +76 to +86
it.effect('should reject empty subtitle array', () =>
E.gen(function* () {
const result = yield* validateSubtitleData([])
expect('cause' in result).toBe(true)
if ('cause' in result && result.cause instanceof Error) {
expect(result.cause.message).toBe(
'Subtitle data must be a non-empty array',
)
}
}).pipe(E.catchAll(E.succeed)),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

test: assert error message via Exit + Cause instead of catchAll (empty array)

Keep the message assertion, but derive it from the Exit’s Cause to follow the preferred pattern.

-    it.effect('should reject empty subtitle array', () =>
-      E.gen(function* () {
-        const result = yield* validateSubtitleData([])
-        expect('cause' in result).toBe(true)
-        if ('cause' in result && result.cause instanceof Error) {
-          expect(result.cause.message).toBe(
-            'Subtitle data must be a non-empty array',
-          )
-        }
-      }).pipe(E.catchAll(E.succeed)),
-    )
+    it.effect('should reject empty subtitle array', () =>
+      E.gen(function* () {
+        const exit = yield* validateSubtitleData([]).pipe(E.exit)
+        expect(Exit.isFailure(exit)).toBe(true)
+        if (Exit.isFailure(exit)) {
+          const errorOpt = Cause.failureOption(exit.cause)
+          if (Option.isSome(errorOpt) && errorOpt.value instanceof Error) {
+            expect(errorOpt.value.message).toBe(
+              'Subtitle data must be a non-empty array',
+            )
+          }
+        }
+      }),
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it.effect('should reject empty subtitle array', () =>
E.gen(function* () {
const result = yield* validateSubtitleData([])
expect('cause' in result).toBe(true)
if ('cause' in result && result.cause instanceof Error) {
expect(result.cause.message).toBe(
'Subtitle data must be a non-empty array',
)
}
}).pipe(E.catchAll(E.succeed)),
)
it.effect('should reject empty subtitle array', () =>
E.gen(function* () {
const exit = yield* validateSubtitleData([]).pipe(E.exit)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) {
const errorOpt = Cause.failureOption(exit.cause)
if (Option.isSome(errorOpt) && errorOpt.value instanceof Error) {
expect(errorOpt.value.message).toBe(
'Subtitle data must be a non-empty array',
)
}
}
}),
)
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-converter.test.ts around lines
76-86, the test currently uses catchAll to unwrap the validation failure; change
it to obtain the Effect's Exit and assert the error via its Cause instead.
Replace the .pipe(E.catchAll(E.succeed)) approach with running the effect to get
its Exit (using the project's run/exit helper for Effects), assert the Exit
represents failure, extract the Cause from the Exit and assert its message
equals "Subtitle data must be a non-empty array", keeping the rest of the test
structure unchanged.

Comment on lines +88 to +93
it.effect('should reject null subtitle data', () =>
E.gen(function* () {
const result = yield* validateSubtitleData(null as never)
expect('cause' in result).toBe(true)
}).pipe(E.catchAll(E.succeed)),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

test: use Exit pattern for null input validation

Consistent with the above, assert failure via Exit.

-    it.effect('should reject null subtitle data', () =>
-      E.gen(function* () {
-        const result = yield* validateSubtitleData(null as never)
-        expect('cause' in result).toBe(true)
-      }).pipe(E.catchAll(E.succeed)),
-    )
+    it.effect('should reject null subtitle data', () =>
+      E.gen(function* () {
+        const exit = yield* validateSubtitleData(null as never).pipe(E.exit)
+        expect(Exit.isFailure(exit)).toBe(true)
+      }),
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it.effect('should reject null subtitle data', () =>
E.gen(function* () {
const result = yield* validateSubtitleData(null as never)
expect('cause' in result).toBe(true)
}).pipe(E.catchAll(E.succeed)),
)
it.effect('should reject null subtitle data', () =>
E.gen(function* () {
const exit = yield* validateSubtitleData(null as never).pipe(E.exit)
expect(Exit.isFailure(exit)).toBe(true)
}),
)
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-converter.test.ts around lines 88
to 93, the test currently asserts a failure by checking for a 'cause' property
on the result; update it to follow the Exit pattern used elsewhere by asserting
that the effect produced an Exit failure. Replace the ad-hoc 'cause' check with
the proper Exit-based assertion (e.g., use the library's Exit/Effect match or
isFailure helper to assert the result is an Exit failure) so the test uniformly
verifies null input validation via Exit.

Comment on lines +180 to +182
it.effect('should process subtitles and print valid SRT file', () =>
E.gen(function* () {
const complexSubtitles: SubtitleItem[] = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

test: rename “print” to “produce” in test descriptions

These tests don’t print; they produce and assert content. Rename for accuracy and to avoid confusion.

-    it.effect('should process subtitles and print valid SRT file', () =>
+    it.effect('should process subtitles and produce valid SRT content', () =>
@@
-    it.effect('should process subtitles and print valid JSON format', () =>
+    it.effect('should process subtitles and produce valid JSON content', () =>
@@
-    it.effect('should process subtitles and print valid VTT format', () =>
+    it.effect('should process subtitles and produce valid VTT content', () =>
@@
-    it.effect(
-      'should process subtitles and print valid plain text format',
+    it.effect(
+      'should process subtitles and produce valid plain text content',
       () =>
@@
-    it.effect('should process subtitles and print all formats for comparison', () =>
+    it.effect('should process subtitles and produce all formats for comparison', () =>

Also applies to: 263-265, 351-353, 420-423, 496-499

🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-converter.test.ts around lines
180-182 (and also update occurrences at 263-265, 351-353, 420-423, 496-499),
rename the test description text by replacing the word "print" with "produce"
(e.g., "should print valid SRT file" -> "should produce valid SRT file") so
descriptions accurately reflect that tests produce/assert content; update the
string literals in the it.effect/test declaration lines only, keeping the rest
of each test body unchanged.

Comment on lines +1352 to +1364
it.effect('should handle errors in pipes', () =>
E.gen(function* () {
const result = yield* E.succeed(invalidSubtitles).pipe(
E.flatMap((subtitles) =>
SubtitleConverterLive.convert(subtitles as SubtitleItem[], 'json'),
),
E.catchAll((error) => E.succeed(error)),
)

// The first validation error will be InvalidTimingError for negative start time
expect(result).toBeInstanceOf(InvalidTimingError)
}),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

test: avoid catching the entire pipe when asserting errors; use Exit

Catching the whole pipe with catchAll obscures failure signals. Prefer Exit to assert the specific tagged error with no side effects.

-    it.effect('should handle errors in pipes', () =>
-      E.gen(function* () {
-        const result = yield* E.succeed(invalidSubtitles).pipe(
-          E.flatMap((subtitles) =>
-            SubtitleConverterLive.convert(subtitles as SubtitleItem[], 'json'),
-          ),
-          E.catchAll((error) => E.succeed(error)),
-        )
-
-        // The first validation error will be InvalidTimingError for negative start time
-        expect(result).toBeInstanceOf(InvalidTimingError)
-      }),
-    )
+    it.effect('should handle errors in pipes', () =>
+      E.gen(function* () {
+        const exit = yield* E.succeed(invalidSubtitles).pipe(
+          E.flatMap((subtitles) =>
+            SubtitleConverterLive.convert(subtitles as SubtitleItem[], 'json'),
+          ),
+          E.exit,
+        )
+
+        expect(Exit.isFailure(exit)).toBe(true)
+        if (Exit.isFailure(exit)) {
+          const errorOpt = Cause.failureOption(exit.cause)
+          expect(Option.isSome(errorOpt)).toBe(true)
+          if (Option.isSome(errorOpt)) {
+            expect(errorOpt.value).toBeInstanceOf(InvalidTimingError)
+          }
+        }
+      }),
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it.effect('should handle errors in pipes', () =>
E.gen(function* () {
const result = yield* E.succeed(invalidSubtitles).pipe(
E.flatMap((subtitles) =>
SubtitleConverterLive.convert(subtitles as SubtitleItem[], 'json'),
),
E.catchAll((error) => E.succeed(error)),
)
// The first validation error will be InvalidTimingError for negative start time
expect(result).toBeInstanceOf(InvalidTimingError)
}),
)
it.effect('should handle errors in pipes', () =>
E.gen(function* () {
const exit = yield* E.succeed(invalidSubtitles).pipe(
E.flatMap((subtitles) =>
SubtitleConverterLive.convert(subtitles as SubtitleItem[], 'json'),
),
E.exit,
)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) {
const errorOpt = Cause.failureOption(exit.cause)
expect(Option.isSome(errorOpt)).toBe(true)
if (Option.isSome(errorOpt)) {
expect(errorOpt.value).toBeInstanceOf(InvalidTimingError)
}
}
}),
)
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-converter.test.ts around lines
1352 to 1364, the test is catching the entire pipe with E.catchAll which hides
the effect failure; instead run the effect to an Exit (use E.exit or equivalent
to get an Exit<E, A>) and assert the Exit is a failure containing the specific
InvalidTimingError (no side-effectful catch). Replace the current
E.catchAll(...) usage with an Exit-producing call, inspect Exit.failure to
verify the error is the expected InvalidTimingError (or use
Exit.match/Exit.failureOption to assert the tagged error) and remove the
catchAll-based succeed branch so the test fails correctly on unexpected
outcomes.

Comment on lines +1447 to +1467
describe('Middleware filter debug', () => {
it('should print subtitles before and after each filter', () => {
const originalSubtitles: SubtitleItem[] = [
{ start: 0, end: 2000, text: 'First line', speaker: 1 },
{ start: 2000, end: 4000, text: 'Second line', speaker: 2 },
]

// Apply addTimingOffset
const offsetSubtitles = originalSubtitles.map(addTimingOffset(1000))

// Apply replaceText
const replacedSubtitles = offsetSubtitles.map(replaceText('Replaced!'))

// Apply addPrefix
const prefixedSubtitles = replacedSubtitles.map(addPrefix('[PREFIX]'))

// Final assertion (just to keep the test green)
expect(prefixedSubtitles[0]?.text).toBe('[PREFIX] Replaced!')
expect(prefixedSubtitles[1]?.text).toBe('[PREFIX] Replaced!')
})
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

test: avoid “debug” wording; make intent explicit

The block no longer logs. Rename to reflect behaviour.

-  describe('Middleware filter debug', () => {
-    it('should print subtitles before and after each filter', () => {
+  describe('Composed middleware filters (no logs)', () => {
+    it('should transform subtitles with each filter applied', () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('Middleware filter debug', () => {
it('should print subtitles before and after each filter', () => {
const originalSubtitles: SubtitleItem[] = [
{ start: 0, end: 2000, text: 'First line', speaker: 1 },
{ start: 2000, end: 4000, text: 'Second line', speaker: 2 },
]
// Apply addTimingOffset
const offsetSubtitles = originalSubtitles.map(addTimingOffset(1000))
// Apply replaceText
const replacedSubtitles = offsetSubtitles.map(replaceText('Replaced!'))
// Apply addPrefix
const prefixedSubtitles = replacedSubtitles.map(addPrefix('[PREFIX]'))
// Final assertion (just to keep the test green)
expect(prefixedSubtitles[0]?.text).toBe('[PREFIX] Replaced!')
expect(prefixedSubtitles[1]?.text).toBe('[PREFIX] Replaced!')
})
})
describe('Composed middleware filters (no logs)', () => {
it('should transform subtitles with each filter applied', () => {
const originalSubtitles: SubtitleItem[] = [
{ start: 0, end: 2000, text: 'First line', speaker: 1 },
{ start: 2000, end: 4000, text: 'Second line', speaker: 2 },
]
// Apply addTimingOffset
const offsetSubtitles = originalSubtitles.map(addTimingOffset(1000))
// Apply replaceText
const replacedSubtitles = offsetSubtitles.map(replaceText('Replaced!'))
// Apply addPrefix
const prefixedSubtitles = replacedSubtitles.map(addPrefix('[PREFIX]'))
// Final assertion (just to keep the test green)
expect(prefixedSubtitles[0]?.text).toBe('[PREFIX] Replaced!')
expect(prefixedSubtitles[1]?.text).toBe('[PREFIX] Replaced!')
})
})
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-converter.test.ts around lines
1447 to 1467, the test and describe block use the word "debug" despite not
performing logging; rename them to explicitly state the intent (e.g., describe:
"Middleware filter transformation" or "Middleware filter pipeline", it: "should
apply timing offset, replaceText and addPrefix in sequence producing expected
texts") so the names reflect behavior rather than debugging; update only the
describe and it strings to the new explicit wording, leaving the test body and
assertions unchanged.

Comment on lines +1477 to +1488
// Type guard to check if result has error property
const hasError =
typeof result === 'object' && result !== null && 'error' in result
expect(hasError).toBe(false)

if (!hasError && Array.isArray(result)) {
expect(result).toHaveLength(3)
expect(result[0]?.start).toBe(1000)
expect(result[0]?.end).toBe(6000)
expect(result[2]?.text).toBe('[Speaker 1]: Subtitle processing')
}
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

test: factor repeated error type‑guard into helper

The “hasError” type guard is duplicated. Introduce a small helper to improve readability and reduce repetition.

Add once near the top of the Streaming Processing describe:

const isErrorResult = (x: unknown): x is { error: unknown } =>
  typeof x === 'object' && x !== null && 'error' in x

Then replace occurrences of the inline type guards with isErrorResult(result).

Also applies to: 1502-1511, 1523-1561, 1592-1600

🤖 Prompt for AI Agents
src/domain/media/subtitle-formats/subtitle-converter.test.ts lines 1477-1488
(and also apply to ranges 1502-1511, 1523-1561, 1592-1600): factor the repeated
inline type-guard into a single helper by adding a helper named isErrorResult
near the top of the "Streaming Processing" describe block that narrows unknown
to { error: unknown }, then replace every inline guard expression (typeof x ===
'object' && x !== null && 'error' in x) with isErrorResult(result) in the listed
ranges so tests use the helper consistently.

Comment on lines +1603 to +1606
it.effect('should save subtitle content to file using Bun FS', () =>
E.gen(function* () {
const complexSubtitles: SubtitleItem[] = [
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

test: clarify FS usage in title (no Bun FS calls here)

Title mentions Bun FS, but the test uses an in‑memory Effect‑style mock. Rename to avoid misleading future readers.

-    it.effect('should save subtitle content to file using Bun FS', () =>
+    it.effect('should save subtitle content to file using an in-memory FS (Effect Platform mock)', () =>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it.effect('should save subtitle content to file using Bun FS', () =>
E.gen(function* () {
const complexSubtitles: SubtitleItem[] = [
{
it.effect('should save subtitle content to file using an in-memory FS (Effect Platform mock)', () =>
E.gen(function* () {
const complexSubtitles: SubtitleItem[] = [
{
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-converter.test.ts around lines
1603 to 1606, the test title incorrectly mentions "Bun FS" while the test uses
an in-memory Effect-style mock; update the test description to accurately
reflect the implementation (e.g., "should save subtitle content to file using
in-memory FS mock" or "should save subtitle content to file using mocked FS") so
readers aren’t misled, leaving the test body unchanged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 27

♻️ Duplicate comments (10)
src/domain/media/subtitle-formats/README.md (2)

111-119: docs(tooling): switch README commands to pnpm to match workspace package manager

The repository uses pnpm. Updating the examples avoids confusion for contributors.

Apply this diff:

 #### **Option 2: NPM Scripts**
 ```bash
-# From project root
-npm run test:subtitles          # Run all subtitle tests
-npm run test:subtitles:watch    # Watch mode
-
-# From subtitle directory  
-npm test -- test-enhanced-endpoints.test.ts
+# From project root
+pnpm run test:subtitles          # Run all subtitle tests
+pnpm run test:subtitles:watch    # Watch mode
+
+# From subtitle directory (root script path still applies)
+pnpm test -- src/domain/media/subtitle-formats/test-enhanced-endpoints.test.ts

---

`1-18`: **docs(subtitles): add “Streaming APIs” section for runSubtitleProcessingStream/runSubtitleConversionStream**

Per PR objectives, the new streaming functions are public APIs but aren’t documented here. Add a concise “Streaming APIs” section (with short examples) right after “Usage Examples”, covering:
- runSubtitleProcessingStream(items, filters?, config?) — consumes (async) iterables and yields processed items.
- runSubtitleConversionStream(items, filters?, config?) — yields incremental SRT/VTT/Text/JSON fragments as items arrive.



I can draft the section with runnable snippets aligned to `subtitle-converter.ts` if helpful.


Also applies to: 190-237

</blockquote></details>
<details>
<summary>src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts (3)</summary><blockquote>

`1-1`: **Use `@effect/vitest` for Effect-based tests**

According to the coding guidelines, tests involving Effect-based code should import from `@effect/vitest`, not regular `vitest`.



```diff
-import { describe, expect, it } from 'vitest'
+import { describe, expect, it } from '@effect/vitest'

132-132: Fix expected SRT text assertion

The formatToSrt function preserves the original text including punctuation. Your test data contains 'Hello, world.' but the expectation is 'Hello world'.

-      expect(strings[2]).toBe('Hello world')
+      expect(strings[2]).toBe('Hello, world.')

344-356: Remove time-based performance assertions

Using Date.now() in tests is brittle and environment-dependent. Performance assertions should be in dedicated benchmark tests, not unit tests.

-      const startTime = Date.now()
       const result = createArrayPipeline(largeDataset)
         .filter(
           applyFilters(validateSubtitle, toUpperCase, addPrefix('[PROCESSED]')),
         )
         .collector(createCollector())
         .formatter(formatToSrt)
         .execute()
-      const endTime = Date.now()

       expect(result).toBeInstanceOf(Array)
       expect((result as string[]).length).toBeGreaterThan(0)
-      expect(endTime - startTime).toBeLessThan(1000) // Should complete within 1 second
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (5)

4-16: style(docs): reduce obvious inline comments; prefer concise JSDoc on public APIs

Field-level comments restating names/types add noise and drift risk. Keep top-level JSDoc where it adds context; remove obvious per-field comments.

Example clean-up:

-/**
- * Represents a single subtitle item with timing and text content
- */
 export const SubtitleItem = …

Also applies to: 18-43, 51-75, 77-94


44-49: fix(schema): use type-level of SubtitleFormat in TaggedClass

Using the runtime value in the generic will not type-check; use S.Type.

 export class SubtitleConversionResult extends Data.TaggedClass(
   'SubtitleConversionResult',
 )<{
-  format: SubtitleFormat
+  format: S.Type<typeof SubtitleFormat>
   content: string
 }> {}

59-61: fix(schema): align MultipleFormatResult to S alias

-export const MultipleFormatResult = Schema.Struct({
-  results: Schema.Array(SubtitleConversionResultSchema),
-})
+export const MultipleFormatResult = S.Struct({
+  results: S.Array(SubtitleConversionResultSchema),
+})

1-2: style(schema): import Schema as S and standardise usage

Adopt the repository convention to import Schema as S and update all builders accordingly.

-import { Schema } from 'effect'
-import { Data } from 'effect'
+import { Schema as S, Data } from 'effect'

95-104: refactor(types): replace all Schema.Schema.Type with S.Type

To align with our code-style rules and fix the compile-time errors, every occurrence of

Schema.Schema.Type<typeof X>

should be changed to

S.Type<typeof X>

and each file importing Schema must instead alias it as S (for example: import * as S from '@effect/schema').

Affected locations include, but may not be limited to:

  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts (lines 96–104)
  • src/domain/media/subtitle-formats/endpoints.ts (lines 109–110)
  • src/domain/media/subtitle-formats/subtitle-converter.ts (around line 719)
  • src/usecases/media/transcribe-workflow.usecase.ts (line 12)
  • src/steps/media/download-link.step.ts (line 4)
  • src/steps/media/transcribe-media.step.ts (line 4)
  • src/handlers/media/transcribe-workflow.handler.ts (line 7)
  • src/stores/workflow/workflow-store.ts (lines 10 & 18)
  • src/stores/media/media.store.ts (line 24)

Example diff in subtitle-formats.schema.ts:

- export type SubtitleItem = Schema.Schema.Type<typeof SubtitleItem>
+ export type SubtitleItem = S.Type<typeof SubtitleItem>

Please apply this refactoring across the codebase to restore correct typing and comply with our “Schema as S” import convention.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3c8e3c9 and f8b7dfb.

📒 Files selected for processing (11)
  • package.json (1 hunks)
  • src/domain/media/subtitle-formats/README.md (1 hunks)
  • src/domain/media/subtitle-formats/endpoints.test.ts (1 hunks)
  • src/domain/media/subtitle-formats/endpoints.ts (1 hunks)
  • src/domain/media/subtitle-formats/run-tests.sh (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-processor-enhanced.handler.ts (1 hunks)
  • src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts (1 hunks)
  • src/domain/media/subtitle-formats/test-enhanced-endpoints.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
src/**/*.ts

📄 CodeRabbit Inference Engine (.rules/code-style.mdc)

src/**/*.ts: Always use relative paths for internal imports; never use src/ aliases for internal imports in TypeScript files.
Use package imports only for external dependencies in TypeScript files.
Avoid obvious comments; never add comments that just repeat what the code is doing. Only add comments for complex business logic that needs explanation.
Use JSDoc for public APIs, not inline comments for obvious operations.
Add comments only for complex business logic, non-obvious workarounds, edge cases, or important context that affects future changes.
Use Effect.Service class extension pattern for services, include dependencies in service definition, and provide makeTestService for testing.
Use E.gen(function* () {}) for complex flows; yield effects directly without helper functions; use yield* for effects that return values.
Use Data.Tagged for domain errors, create specific error types for different failure cases, and handle errors explicitly with E.catchTags.
Use Effect's logging utilities instead of console.log; include relevant context in log messages and use appropriate log levels.
Add spans for observability using E.withSpan, include relevant attributes, and use descriptive span names.
Use PascalCase for service classes (e.g., BlogStore, SearchStore).
Use camelCase for functions and variables (e.g., getBlogById, blogPost).
Use UPPERCASE for constants (e.g., TABLE_NAME, DEFAULT_LIMIT).
Use descriptive names for functions and methods that indicate purpose.
Prefix usecases with action (e.g., createBlogPostUsecase, getBlogByIdUsecase).
Prefix handlers with HTTP method context (e.g., _handler, handler).
Use Effect Schema for all data validation; define schemas with explicit types and use branded types for domain identifiers.
Use branded types for domain identifiers and create factory functions with validation.
Access configuration through the Effect system, not directly via process.env.
Use pipe syntax for effect composition;...

Files:

  • src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts
  • src/domain/media/subtitle-formats/test-enhanced-endpoints.test.ts
  • src/domain/media/subtitle-formats/endpoints.test.ts
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
  • src/domain/media/subtitle-formats/endpoints.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
  • src/domain/media/subtitle-formats/subtitle-processor-enhanced.handler.ts
src/**/*

📄 CodeRabbit Inference Engine (.rules/code-style.mdc)

Use kebab-case for file names (e.g., blog.store.ts, get-blog-by-id.ts).

Files:

  • src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts
  • src/domain/media/subtitle-formats/test-enhanced-endpoints.test.ts
  • src/domain/media/subtitle-formats/endpoints.test.ts
  • src/domain/media/subtitle-formats/run-tests.sh
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
  • src/domain/media/subtitle-formats/endpoints.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
  • src/domain/media/subtitle-formats/subtitle-processor-enhanced.handler.ts
  • src/domain/media/subtitle-formats/README.md
src/**/*.test.ts

📄 CodeRabbit Inference Engine (.rules/code-style.mdc)

Always use @effect/vitest for Effect-based tests; use appropriate test variants (it.effect, it.scoped, it.live); mock services using makeTestService.

Files:

  • src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts
  • src/domain/media/subtitle-formats/test-enhanced-endpoints.test.ts
  • src/domain/media/subtitle-formats/endpoints.test.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.rules/effect-service-architecture.mdc)

Always use relative paths for internal imports; never use src/ aliases for internal imports. Use package imports only for external dependencies.

Files:

  • src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts
  • src/domain/media/subtitle-formats/test-enhanced-endpoints.test.ts
  • src/domain/media/subtitle-formats/endpoints.test.ts
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
  • src/domain/media/subtitle-formats/endpoints.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
  • src/domain/media/subtitle-formats/subtitle-processor-enhanced.handler.ts
**/*.test.ts

📄 CodeRabbit Inference Engine (.rules/effect-testing-patterns.mdc)

**/*.test.ts: Always use @effect/vitest for tests involving Effect-based code; import test functions (describe, expect, it) from @effect/vitest, not regular vitest.
Always use relative paths for internal imports in test files; never use src/ aliases.
Use package imports for external dependencies in test files.
Test tagged errors with proper Effect patterns (using E.exit and getExitError).
Use it.effect for tests that need TestContext (TestClock, etc.) in Effect-TS test files.
Use it.scoped for tests involving resources that need proper cleanup in Effect-TS test files.
Use it.live for tests that need real system time and live services in Effect-TS test files.
Use FetchHttpClientTest from @t6c/lib-api for mocking HTTP responses in API client tests.
Use mock data from domain packages when available in tests.
Create reusable mock objects for complex data structures in tests.
Use proper branded type constructors for mock data in tests.
Declare variables outside generator functions when used in FetchHttpClientTest to prevent scoping issues in tests.
Use .pipe(E.exit) directly on service calls for cleaner error testing in tests; avoid nested E.exit(E.gen(...)) patterns.
Use Exit.isFailure(result) to check for failures and getExitError(result) to extract error details in tests.
Test all public methods of services, including both success and error scenarios, default parameters, edge cases, and validation.
Use appropriate HTTP status codes (200, 201, 404, 500) and match response structure to actual API responses in mock responses for tests.
Use descriptive test names that explain the scenario, following the pattern: 'should [action] [condition]'.
Group related tests in describe blocks by method.
Reuse mock data across tests when possible.
Create typed mock objects with proper constructors for tests.
Always provide required layers in correct order in tests (e.g., DefaultWithoutDependencies for services, MockConfigLayer for configurat...

Files:

  • src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts
  • src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts
  • src/domain/media/subtitle-formats/test-enhanced-endpoints.test.ts
  • src/domain/media/subtitle-formats/endpoints.test.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
🧠 Learnings (22)
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use proper branded type constructors for mock data in tests.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
📚 Learning: 2025-08-10T21:45:41.119Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/api-client-testing.mdc:0-0
Timestamp: 2025-08-10T21:45:41.119Z
Learning: Applies to **/*api-client.test.ts : API client test files must import { describe, expect, it } from 'effect/vitest', { FetchHttpClientTest, getExitError } from 't6c/lib-api', { Effect as E, Exit } from 'effect', and { MockConfigLayer } from '../config.js'.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts
  • src/domain/media/subtitle-formats/test-enhanced-endpoints.test.ts
  • src/domain/media/subtitle-formats/endpoints.test.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Test all public methods of services, including both success and error scenarios, default parameters, edge cases, and validation.

Applied to files:

  • src/domain/media/subtitle-formats/test-enhanced-endpoints.test.ts
📚 Learning: 2025-08-10T21:45:41.119Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/api-client-testing.mdc:0-0
Timestamp: 2025-08-10T21:45:41.119Z
Learning: Applies to **/*api-client.test.ts : Test all public service methods in API client test files, including both success and error scenarios, default parameters, edge cases, and validation.

Applied to files:

  • src/domain/media/subtitle-formats/test-enhanced-endpoints.test.ts
  • src/domain/media/subtitle-formats/endpoints.test.ts
📚 Learning: 2025-08-10T21:45:41.119Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/api-client-testing.mdc:0-0
Timestamp: 2025-08-10T21:45:41.119Z
Learning: Applies to **/*api-client.test.ts : API client test files should follow the standard test structure: use 'it.effect' with an Effect generator, provide layers in the correct order, and assert results using 'expect'.

Applied to files:

  • src/domain/media/subtitle-formats/endpoints.test.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:48:12.331Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/schema-validation.mdc:0-0
Timestamp: 2025-08-10T21:48:12.331Z
Learning: Applies to **/schema/**/*.ts|**/schemas/**/*.ts|**/*-schema.ts|**/*-schemas.ts : Test schema validation with valid and invalid inputs using effect/vitest and Effect-based test patterns

Applied to files:

  • src/domain/media/subtitle-formats/endpoints.test.ts
  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.test.ts : Always use `effect/vitest` for Effect-based tests; use appropriate test variants (`it.effect`, `it.scoped`, `it.live`); mock services using `makeTestService`.

Applied to files:

  • src/domain/media/subtitle-formats/endpoints.test.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Always use `effect/vitest` for tests involving Effect-based code; import test functions (`describe`, `expect`, `it`) from `effect/vitest`, not regular `vitest`.

Applied to files:

  • src/domain/media/subtitle-formats/endpoints.test.ts
  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use Effect Schema for all data validation; define schemas with explicit types and use branded types for domain identifiers.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
📚 Learning: 2025-08-10T21:46:10.977Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/cursor-rule-maintainer.mdc:0-0
Timestamp: 2025-08-10T21:46:10.977Z
Learning: Applies to .cursor/rules/schema-validation.mdc : Add new Effect Schema patterns, validation improvements, branded type patterns, and error handling for schemas to `schema-validation.mdc`

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
📚 Learning: 2025-08-10T21:48:12.331Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/schema-validation.mdc:0-0
Timestamp: 2025-08-10T21:48:12.331Z
Learning: Applies to **/*.types.ts|**/schema/**/*.ts|**/schemas/**/*.ts|**/*-schema.ts|**/*-schemas.ts|**/branded.ts|**/domain/**/*.ts : Always use `Schema as S` for Effect Schema imports

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
📚 Learning: 2025-08-10T21:48:12.331Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/schema-validation.mdc:0-0
Timestamp: 2025-08-10T21:48:12.331Z
Learning: Applies to **/*.types.ts|**/schema/**/*.ts|**/schemas/**/*.ts|**/*-schema.ts|**/*-schemas.ts|**/branded.ts|**/domain/**/*.ts : Use `Data.TaggedError` for domain-specific error types and include relevant identifiers and context in error data

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
📚 Learning: 2025-08-10T21:03:54.779Z
Learnt from: BleedingDev
PR: BleedingDev/coursition-effect-experiment#18
File: src/domain/workflow/workflow.errors.ts:3-6
Timestamp: 2025-08-10T21:03:54.779Z
Learning: In Effect-based applications, Schema.TaggedError is used for serializable errors that can bubble up to clients/external systems, while Data.TaggedError is used for internal errors that stay within the application boundaries. This is an intentional architectural pattern to differentiate between public API errors and internal implementation errors.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use `Data.Tagged` for domain errors, create specific error types for different failure cases, and handle errors explicitly with `E.catchTags`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Never use `throw new Error()` in Effect-TS applications; always create and use `Data.TaggedError` for error handling.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
📚 Learning: 2025-08-10T21:48:12.331Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/schema-validation.mdc:0-0
Timestamp: 2025-08-10T21:48:12.331Z
Learning: Applies to **/*.types.ts|**/schema/**/*.ts|**/schemas/**/*.ts|**/*-schema.ts|**/*-schemas.ts|**/branded.ts|**/domain/**/*.ts : Use `S.pipe` with `S.brand` to create type-branded primitives and export both the schema and the type

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
📚 Learning: 2025-08-10T21:45:28.136Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/api-client-patterns.mdc:0-0
Timestamp: 2025-08-10T21:45:28.136Z
Learning: Applies to **/*.api-client.ts : Import types with the `type` keyword when used only for typing; import schemas without `type` when used for encoding/decoding.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-formats.schema.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use `it.effect` for tests that need TestContext (TestClock, etc.) in Effect-TS test files.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use `it.live` for tests that need real system time and live services in Effect-TS test files.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use `it.scoped` for tests involving resources that need proper cleanup in Effect-TS test files.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:46:38.101Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-service-architecture.mdc:0-0
Timestamp: 2025-08-10T21:46:38.101Z
Learning: Applies to **/stores/*-store.test.{ts,js} : Store testing should use Effect-TS testing utilities (e.g., `effect/vitest`), provide mock implementations for dependencies, and test store methods in isolation.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Test tagged errors with proper Effect patterns (using `E.exit` and `getExitError`).

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts
🧬 Code Graph Analysis (7)
src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts (5)
src/domain/media/subtitle-formats/subtitle-converter.ts (1)
  • SubtitleConverterLive (659-737)
src/domain/media/subtitle-formats/subtitle-processor-enhanced.handler.ts (4)
  • enhancedProcessSubtitlesHandler (30-93)
  • processSubtitlesHandler (98-174)
  • getSupportedFormatsHandler (268-278)
  • healthCheckHandler (281-293)
src/domain/media/subtitle-formats/endpoints.ts (2)
  • ProcessSubtitlesRequest (19-28)
  • ProcessSubtitlesRequest (109-109)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (4)
  • SubtitleItem (7-16)
  • SubtitleItem (96-96)
  • SubtitleFormat (26-26)
  • SubtitleFormat (98-98)
src/domain/media/subtitle-formats/subtitle-formats.errors.ts (4)
  • SubtitleDataInvalid (4-7)
  • SubtitleFormatUnsupported (9-15)
  • SubtitleConversionFailed (22-27)
  • SubtitleProcessingFailed (29-34)
src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts (4)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (4)
  • SubtitleItem (7-16)
  • SubtitleItem (96-96)
  • SubtitleFormat (26-26)
  • SubtitleFormat (98-98)
src/domain/media/subtitle-formats/subtitle-converter.ts (6)
  • SubtitleItem (740-740)
  • validateSubtitleData (106-150)
  • processSubtitles (304-343)
  • SubtitleConverterLive (659-737)
  • runSubtitleProcessingStream (385-393)
  • runSubtitleConversionStream (480-487)
src/domain/media/subtitle-formats/subtitle-formats.errors.ts (2)
  • UnsupportedFormatError (43-48)
  • InvalidTimingError (50-52)
src/domain/media/subtitle-formats/subtitle-filters.ts (6)
  • replaceText (19-37)
  • addTimingOffset (45-51)
  • addPrefix (84-89)
  • filterBySpeaker (59-62)
  • applyFiltersToArray (225-237)
  • streamSubtitles (246-259)
src/domain/media/subtitle-formats/endpoints.test.ts (2)
src/domain/media/subtitle-formats/endpoints.ts (7)
  • ProcessSubtitlesRequest (19-28)
  • ProcessSubtitlesRequest (109-109)
  • ProcessSubtitlesResponse (33-44)
  • ProcessSubtitlesResponse (110-110)
  • subtitleGroup (98-103)
  • createSubtitleApi (127-128)
  • addSubtitleEndpoints (120-121)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (4)
  • SubtitleFormat (26-26)
  • SubtitleFormat (98-98)
  • SubtitleItem (7-16)
  • SubtitleItem (96-96)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (2)
src/domain/media/subtitle-formats/subtitle-converter.ts (1)
  • SubtitleItem (740-740)
src/domain/media/subtitle-formats/subtitle-processor-enhanced.handler.ts (2)
  • EnhancedProcessSubtitlesRequest (12-17)
  • MultiFormatResponse (19-28)
src/domain/media/subtitle-formats/endpoints.ts (2)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (10)
  • SubtitleFormat (26-26)
  • SubtitleFormat (98-98)
  • SubtitleItem (7-16)
  • SubtitleItem (96-96)
  • ConversionOptions (31-42)
  • ConversionOptions (99-99)
  • EnhancedProcessSubtitlesRequest (66-75)
  • EnhancedProcessSubtitlesRequest (103-103)
  • MultiFormatResponse (80-93)
  • MultiFormatResponse (104-104)
src/domain/media/subtitle-formats/subtitle-formats.errors.ts (4)
  • SubtitleDataInvalid (4-7)
  • SubtitleFormatUnsupported (9-15)
  • SubtitleConversionFailed (22-27)
  • SubtitleProcessingFailed (29-34)
src/domain/media/subtitle-formats/subtitle-pipeline-simple.test.ts (3)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (2)
  • SubtitleItem (7-16)
  • SubtitleItem (96-96)
src/domain/media/subtitle-formats/subtitle-pipeline-simple.ts (13)
  • createPipeline (305-307)
  • SubtitlePipeline (144-296)
  • createArrayPipeline (312-318)
  • PipelineConfig (43-47)
  • createCollector (121-128)
  • applyFilters (79-93)
  • formatToSrt (327-345)
  • formatToVtt (350-363)
  • formatToPlainText (368-370)
  • formatToJson (375-377)
  • processToSrt (412-421)
  • processToVtt (426-435)
  • processWithConfig (440-454)
src/domain/media/subtitle-formats/subtitle-filters.ts (9)
  • toUpperCase (152-155)
  • applyFilters (305-320)
  • removeEmptySubtitles (182-185)
  • addPrefix (84-89)
  • filterBySpeakers (70-76)
  • addTimingOffset (45-51)
  • transformText (140-145)
  • capitalize (172-175)
  • validateSubtitle (202-216)
src/domain/media/subtitle-formats/subtitle-processor-enhanced.handler.ts (4)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (10)
  • EnhancedProcessSubtitlesRequest (66-75)
  • EnhancedProcessSubtitlesRequest (103-103)
  • SubtitleItem (7-16)
  • SubtitleItem (96-96)
  • ConversionOptions (31-42)
  • ConversionOptions (99-99)
  • MultiFormatResponse (80-93)
  • MultiFormatResponse (104-104)
  • SubtitleFormat (26-26)
  • SubtitleFormat (98-98)
src/domain/media/subtitle-formats/subtitle-converter.ts (1)
  • SubtitleConverterLive (659-737)
src/domain/media/subtitle-formats/subtitle-formats.errors.ts (4)
  • SubtitleDataInvalid (4-7)
  • SubtitleFormatUnsupported (9-15)
  • SubtitleConversionFailed (22-27)
  • SubtitleProcessingFailed (29-34)
src/domain/media/subtitle-formats/endpoints.ts (4)
  • ProcessSubtitlesRequest (19-28)
  • ProcessSubtitlesRequest (109-109)
  • ProcessSubtitlesResponse (33-44)
  • ProcessSubtitlesResponse (110-110)
🪛 markdownlint-cli2 (0.17.2)
src/domain/media/subtitle-formats/README.md

7-7: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


8-8: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


13-13: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


14-14: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


19-19: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


20-20: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


25-25: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


26-26: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


33-33: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


34-34: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


36-36: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


39-39: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


40-40: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


42-42: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


45-45: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


46-46: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


70-70: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


71-71: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


100-100: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


101-101: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


104-104: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


106-106: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


111-111: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


112-112: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


121-121: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


122-122: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


159-159: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


192-192: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


193-193: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


206-206: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


207-207: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


223-223: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


224-224: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


241-241: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


242-242: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


254-254: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


255-255: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


261-261: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


263-263: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


268-268: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


270-270: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


288-288: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)


288-288: Files should end with a single newline character

(MD047, single-trailing-newline)

🔇 Additional comments (5)
src/domain/media/subtitle-formats/run-tests.sh (1)

1-48: chore(test): harden and modernise run-tests.sh for portability, pnpm-first use, and correct exit-code propagation

This update addresses the following:

  • Adds #!/usr/bin/env bash and set -euo pipefail for strict error handling
  • Computes REPO_ROOT and SUB_DIR dynamically so the script can be run from any CWD
  • Enforces use of pnpm (matching workspace packageManager) and fails fast if missing
  • Captures integration and unit test exit codes into INTEG_RC/UNIT_RC instead of assuming success
  • Prints a summary that reflects actual test outcomes (❌ or ✅ with exit code)
  • Only returns success (0) if both test suites pass

Please apply the following diff to src/domain/media/subtitle-formats/run-tests.sh, then run the updated script and confirm that:

  1. It still checks the server health on $PORT (defaults to 3001)
  2. It fails immediately if pnpm isn’t installed
  3. Integration/unit tests are run via pnpm run test:subtitles:integration and pnpm run test:subtitles:unit
  4. The summary shows ❌ with the actual exit code when tests fail
  5. The final exit code is non-zero if any test suite fails
--- a/src/domain/media/subtitle-formats/run-tests.sh
+++ b/src/domain/media/subtitle-formats/run-tests.sh
@@ -1,6 +1,10 @@
-#!/bin/bash
-
-# Enhanced Subtitle Tests Runner
-# Run this script from the subtitle-formats directory
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Enhanced Subtitle Tests Runner (works from any directory)
+REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../../../.." && pwd)"
+SUB_DIR="$REPO_ROOT/src/domain/media/subtitle-formats"
+PORT="${PORT:-3001}"
+HEALTH_URL="http://localhost:${PORT}/subtitles/health"

-echo "🚀 Running Enhanced Subtitle Tests..."
-echo "======================================"
+echo "🚀 Running Enhanced Subtitle Tests..."
+echo "repo: $REPO_ROOT"
+echo "dir : $SUB_DIR"
+echo "port: $PORT"
+echo "======================================"

-# Check if server is running
-echo "🔍 Checking if server is running..."
-if curl -s http://localhost:3001/subtitles/health > /dev/null; then
-    echo "✅ Server is running on localhost:3001"
-else
-    echo "❌ Server is not running. Please start it first:"
-    echo "   bun src/server.ts"
-    echo "   or"
-    echo "   npm run start:server"
-    exit 1
-fi
+if ! command -v pnpm >/dev/null 2>&1; then
+  echo "❌ pnpm not found. Install it to match this workspace’s packageManager."
+  exit 1
+fi
+
+echo "🔍 Checking server at ${HEALTH_URL}..."
+if curl -sf "${HEALTH_URL}" >/dev/null; then
+  echo "✅ Server is running on localhost:${PORT}"
+else
+  echo "❌ Server is not running. Start it first:"
+  echo "   bun src/server.ts"
+  echo "   or"
+  echo "   pnpm run start:server"
+  exit 1
+fi

 echo
-echo "🧪 Running Integration Tests..."
-echo "================================"
-
-# Run the enhanced endpoints test
-npm test -- test-enhanced-endpoints.test.ts
+echo "🧪 Running Integration Tests..."
+echo "================================"
+pushd "$REPO_ROOT" >/dev/null
+pnpm run test:subtitles:integration || INTEG_RC=$? || true
+INTEG_RC="${INTEG_RC:-0}"
+popd >/dev/null

 echo
-echo "🎯 Running Unit Tests..."
-echo "========================"
-
-# Run the unit tests (may have mocking issues but worth trying)
-npm test -- subtitle-processor-enhanced.test.ts
+echo "🎯 Running Unit Tests..."
+echo "========================"
+pushd "$REPO_ROOT" >/dev/null
+pnpm run test:subtitles:unit || UNIT_RC=$? || true
+UNIT_RC="${UNIT_RC:-0}"
+popd >/dev/null

 echo
-echo "🎉 Test run completed!"
-echo ""
-echo "📊 Summary:"
-echo "   - Integration tests: ✅ All working"
-echo "   - Unit tests: ⚠️  May have mocking issues"
-echo "   - Server status: ✅ Running"
+echo "🎉 Test run completed!"
+echo
+echo "📊 Summary:"
+if [[ "$INTEG_RC" -eq 0 ]]; then
+  echo "   - Integration tests: ✅ Passed"
+else
+  echo "   - Integration tests: ❌ Failed (exit $INTEG_RC)"
+fi
+if [[ "$UNIT_RC" -eq 0 ]]; then
+  echo "   - Unit tests       : ✅ Passed"
+else
+  echo "   - Unit tests       : ❌ Failed (exit $UNIT_RC)"
+fi
+echo "   - Server status    : ✅ Running"
+echo
+echo "💡 Watch mode: pnpm run test:subtitles:watch"
+echo "💡 All subtitle tests: pnpm run test:subtitles"

-echo ""
-echo "💡 To run tests in watch mode:"
-echo "   npm run test:subtitles:watch"
-echo ""
-echo "💡 To run all subtitle tests:"
-echo "   npm run test:subtitles"
+[[ "$INTEG_RC" -eq 0 && "$UNIT_RC" -eq 0 ]]

Once applied, please re-run:

bash src/domain/media/subtitle-formats/run-tests.sh

and confirm that the summary accurately reflects test outcomes and the final exit code is non-zero if any suite fails.

src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts (1)

18-28: test(mocking): good use of vi.mock with deferred import

Mock is declared before dynamic import, which ensures the stubbed SubtitleConverterLive.convert is used. Nicely done.

src/domain/media/subtitle-formats/subtitle-converter-clean.test.ts (1)

1-1: LGTM! Correct import from @effect/vitest

The test file correctly imports from @effect/vitest as required by the coding guidelines for Effect-based tests.

src/domain/media/subtitle-formats/endpoints.ts (1)

127-128: Return type missing for factory function

The createSubtitleApi function should have an explicit return type.

-export const createSubtitleApi = () => 
+export const createSubtitleApi = (): HttpApi.HttpApi => 
   HttpApi.make('subtitleApi').add(subtitleGroup)

Likely an incorrect or invalid review comment.

src/domain/media/subtitle-formats/subtitle-formats.schema.ts (1)

26-27: docs(schema): formats literal looks good

Using a literal union for formats is aligned with downstream conversion logic.

Comment thread package.json
Comment on lines +13 to +14
"test:subtitles": "vitest run src/domain/media/subtitle-formats/",
"test:subtitles:watch": "vitest src/domain/media/subtitle-formats/",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

test(scripts): align subtitle test commands with pnpm and add focused unit/integration runners

Given the repo uses pnpm (packageManager is pnpm@10.7.0), it’s clearer if the runner script and docs call pnpm script aliases. Recommend adding explicit unit/integration scripts so run-tests.sh and README don’t hardcode test file names.

Apply this diff to extend scripts:

   "scripts": {
     "dev:server": "bun --inspect-wait --watch src/server.ts",
     "start:server": "bun src/server.ts",
     "test": "vitest run",
     "test:watch": "vitest",
     "test:ui": "vitest --ui",
     "test:subtitles": "vitest run src/domain/media/subtitle-formats/",
     "test:subtitles:watch": "vitest src/domain/media/subtitle-formats/",
+    "test:subtitles:integration": "vitest run src/domain/media/subtitle-formats/test-enhanced-endpoints.test.ts",
+    "test:subtitles:unit": "vitest run src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts",
     "check": "biome check --write .",
     "typecheck": "tspc --noEmit",
     "build": "tspc"
   },

This keeps CI stable and lets the shell script and README call pnpm run test:subtitles:* instead of invoking npm test -- ... directly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"test:subtitles": "vitest run src/domain/media/subtitle-formats/",
"test:subtitles:watch": "vitest src/domain/media/subtitle-formats/",
"scripts": {
"dev:server": "bun --inspect-wait --watch src/server.ts",
"start:server": "bun src/server.ts",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:subtitles": "vitest run src/domain/media/subtitle-formats/",
"test:subtitles:watch": "vitest src/domain/media/subtitle-formats/",
"test:subtitles:integration": "vitest run src/domain/media/subtitle-formats/test-enhanced-endpoints.test.ts",
"test:subtitles:unit": "vitest run src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts",
"check": "biome check --write .",
"typecheck": "tspc --noEmit",
"build": "tspc"
},
🤖 Prompt for AI Agents
In package.json around lines 13-14, the current subtitle test scripts call
vitest directly and don't provide pnpm-friendly aliases or explicit
unit/integration runners; update the scripts section to (1) expose these scripts
as pnpm-invokable entries (so callers use `pnpm run test:subtitles:*`), and (2)
add explicit focused runners such as `test:subtitles:unit` and
`test:subtitles:integration` (and watch counterparts) that run vitest against
the appropriate unit or integration test globs/dirs; ensure names and semantics
match run-tests.sh and README so they no longer hardcode npm invocations or file
paths.

Comment on lines +1 to +2
import { describe, it, expect, vi } from 'vitest'
import { Effect as E } from 'effect'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

test: consider @effect/vitest for consistency; remove unused Effect import

  • tests here don’t run Effects, so vitest is acceptable; however, for repo-wide consistency you may import from @effect/vitest.
  • Effect as E is imported but unused.

Apply this minimal diff:

-import { describe, it, expect, vi } from 'vitest'
-import { Effect as E } from 'effect'
+import { describe, it, expect, vi } from 'vitest'

Or, for consistency:

-import { describe, it, expect, vi } from 'vitest'
+import { describe, it, expect, vi } from '@effect/vitest'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { describe, it, expect, vi } from 'vitest'
import { Effect as E } from 'effect'
import { describe, it, expect, vi } from 'vitest'
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/endpoints.test.ts around lines 1 to 2, the
test imports come from 'vitest' and also import `Effect as E` from 'effect'
which is unused; switch to the repo-consistent test helper by importing
describe, it, expect, vi from '@effect/vitest' and remove the unused `import {
Effect as E } from 'effect'` line so the file only imports the needed vitest
utilities from @effect/vitest.

Comment on lines +160 to +171
describe('Endpoint Configuration', () => {
it('should have correct HTTP methods', () => {
// The processSubtitles endpoint should be a POST endpoint
expect(subtitleGroup.endpoints).toBeDefined()
})

it('should have proper error handling configured', () => {
// This test ensures the endpoints are properly configured
// The actual error handling is tested in the handler tests
expect(subtitleGroup).toBeDefined()
})
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

test(endpoints): strengthen assertions on API group shape

Current checks only assert that properties exist. Consider asserting the group contains the expected endpoints and prefix to guard accidental regressions.

Example follow-up:

expect(subtitleGroup).toHaveProperty('prefix')
expect(String(subtitleGroup.prefix)).toContain('/subtitles')
expect(JSON.stringify(subtitleGroup.endpoints)).toMatch(/process|process-enhanced|formats|health/)

This keeps the test lightweight while catching mis-wiring.

🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/endpoints.test.ts around lines 160 to 171,
the tests only assert existence of subtitleGroup and its endpoints; strengthen
assertions to verify the group's prefix contains the expected '/subtitles' route
and that endpoints include the expected names (e.g. process, process-enhanced,
formats, health). Update the tests to assert subtitleGroup has a 'prefix'
property whose string contains '/subtitles' and that subtitleGroup.endpoints (or
its JSON/string form) matches the expected endpoint keys or patterns to catch
wiring regressions.

Comment on lines +120 to +121
export const addSubtitleEndpoints = (api: HttpApi.HttpApi) =>
api.add(subtitleGroup)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Return type missing for helper function

The addSubtitleEndpoints function should have an explicit return type for better type safety.

-export const addSubtitleEndpoints = (api: HttpApi.HttpApi) => 
+export const addSubtitleEndpoints = (api: HttpApi.HttpApi): HttpApi.HttpApi => 
   api.add(subtitleGroup)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const addSubtitleEndpoints = (api: HttpApi.HttpApi) =>
api.add(subtitleGroup)
export const addSubtitleEndpoints = (api: HttpApi.HttpApi): HttpApi.HttpApi =>
api.add(subtitleGroup)
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/endpoints.ts around lines 120-121, the
helper function addSubtitleEndpoints lacks an explicit return type; change its
signature to include a clear TypeScript return type (e.g. : HttpApi.HttpApi) so
the function's return is type-safe and supports chaining. Update the function
declaration to declare that return type, ensure HttpApi is correctly
imported/available in the file, and adjust any consuming code if it relied on
implicit typing.

Comment on lines +33 to +37
### Health Check
```http
GET /subtitles/health
```
Returns service status and timestamp.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

docs(markdown): fix markdownlint violations (blank lines around headings/fences, trailing spaces, EOF newline)

markdownlint-cli2 flagged multiple MD022/MD031/MD032/MD009/MD047 issues. Insert blank lines around headings and fenced blocks, and remove trailing whitespace. A few representative fixes are shown; please apply consistently across the file.

Apply representative diffs:

 ### Health Check
-```http
+```http
 GET /subtitles/health

Returns service status and timestamp.

Get Supported Formats

-http +http
GET /subtitles/formats

Returns array of supported subtitle formats.

### Legacy Single Format Processing
-```http
+```http
POST /subtitles/process
Content-Type: application/json
 #### **Option 1: Test Runner Script (Recommended)**
-```bash
+```bash
 cd src/domain/media/subtitle-formats
 ./run-tests.sh

```diff
 ### Basic Single Format
-```typescript
+```typescript
 import { enhancedProcessSubtitlesHandler } from './subtitle-processor-enhanced.handler'
 
 const result = await enhancedProcessSubtitlesHandler({
 ### With Options
-```typescript
+```typescript
 const result = await enhancedProcessSubtitlesHandler({
 ### Validation Errors
-```typescript
+```typescript
 try {
-## 🔮 Future Enhancements
-
-- **Batch processing**: Process multiple subtitle files
+## 🔮 Future Enhancements
+
+- **Batch processing**: Process multiple subtitle files
 ...
- - **WebSocket support**: Real-time processing updates 
+ - **WebSocket support**: Real-time processing updates
+

After applying, run: pnpm dlx markdownlint-cli2 src/domain/media/subtitle-formats/README.md

Also applies to: 39-43, 45-49, 70-76, 100-106, 111-116, 121-126, 192-205, 206-215, 223-237, 241-252, 254-258, 261-267, 268-274, 282-288


🏁 Script executed:

#!/usr/bin/env bash
pnpm dlx markdownlint-cli2 src/domain/media/subtitle-formats/README.md || true

Length of output: 8207


docs(markdown): fix markdownlint violations in src/domain/media/subtitle-formats/README.md

Please address the 47 markdownlint errors reported by pnpm dlx markdownlint-cli2 src/domain/media/subtitle-formats/README.md. In particular:

  • Add a blank line before and after every heading (MD022) and list (MD032).
  • Surround each fenced code block with blank lines (MD031).
  • Remove all trailing whitespace (MD009).
  • Wrap or truncate lines to ≤ 80 characters (MD013), or explicitly disable that rule where longer lines are unavoidable.
  • Ensure the file ends with exactly one newline (MD047).

After applying these changes file-wide, rerun:

pnpm dlx markdownlint-cli2 src/domain/media/subtitle-formats/README.md

to verify that all violations are resolved.

🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

33-33: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


34-34: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


36-36: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/README.md around lines 33 to 37, the file
currently triggers many markdownlint violations (MD022, MD032, MD031, MD009,
MD013, MD047); fix them by adding a blank line before and after each heading and
list, surrounding every fenced code block with blank lines, removing trailing
whitespace from all lines, wrapping or truncating lines to <=80 characters (or
add a rule-disable comment for unavoidable long lines), and ensure the file ends
with exactly one newline; after making these edits run pnpm dlx
markdownlint-cli2 src/domain/media/subtitle-formats/README.md to confirm all
violations are resolved.

Comment on lines +135 to +151
it('should fail with SubtitleFormatUnsupported for mixed valid/invalid formats', async () => {
const request = {
title: 'Mixed Valid/Invalid Test',
outputFormat: 'srt,invalid,vtt',
subtitleData: [
{
start: 0,
end: 1000,
text: 'Test'
}
]
}

await expect(
E.runPromise(enhancedProcessSubtitlesHandler(request))
).rejects.toThrow()
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

test(errors): assert mixed valid/invalid formats error as tagged

Mirror the previous change for the mixed-format case.

Apply this diff:

-      await expect(
-        E.runPromise(enhancedProcessSubtitlesHandler(request))
-      ).rejects.toThrow()
+      const result = await E.runPromise(
+        enhancedProcessSubtitlesHandler(request).pipe(E.either)
+      )
+      expect(result._tag).toBe('Left')
+      // @ts-expect-error - runtime check
+      expect(result.left._tag).toBe('SubtitleFormatUnsupported')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should fail with SubtitleFormatUnsupported for mixed valid/invalid formats', async () => {
const request = {
title: 'Mixed Valid/Invalid Test',
outputFormat: 'srt,invalid,vtt',
subtitleData: [
{
start: 0,
end: 1000,
text: 'Test'
}
]
}
await expect(
E.runPromise(enhancedProcessSubtitlesHandler(request))
).rejects.toThrow()
})
it('should fail with SubtitleFormatUnsupported for mixed valid/invalid formats', async () => {
const request = {
title: 'Mixed Valid/Invalid Test',
outputFormat: 'srt,invalid,vtt',
subtitleData: [
{
start: 0,
end: 1000,
text: 'Test'
}
]
}
const result = await E.runPromise(
enhancedProcessSubtitlesHandler(request).pipe(E.either)
)
expect(result._tag).toBe('Left')
// @ts-expect-error - runtime check
expect(result.left._tag).toBe('SubtitleFormatUnsupported')
})
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts around
lines 135 to 151, the test currently uses a generic await
expect(...).rejects.toThrow() for the mixed valid/invalid formats case; change
it to assert the specific tagged error (SubtitleFormatUnsupported) like the
earlier single-format test: replace the generic rejects.toThrow() with an
assertion that the promise rejects with the SubtitleFormatUnsupported error
(matching by error class/name or message used elsewhere in the suite) so the
test explicitly verifies the tagged error for mixed-format input.

Comment on lines +251 to +259
// Mock conversion failure
mockConvert.mockReturnValue(E.fail(
new Error('Conversion failed')
))

await expect(
E.runPromise(enhancedProcessSubtitlesHandler(request))
).rejects.toThrow()
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

test(errors): mock typed ConversionError and assert mapping to SubtitleConversionFailed

Currently the test mocks a generic Error. Mocking a typed ConversionError ensures the handler’s E.catchTags path is exercised and mapped correctly to SubtitleConversionFailed.

Apply this diff:

+import { ConversionError } from './subtitle-formats.errors'
@@
-      // Mock conversion failure
-      mockConvert.mockReturnValue(E.fail(
-        new Error('Conversion failed')
-      ))
+      // Mock typed conversion failure that the handler maps to SubtitleConversionFailed
+      mockConvert.mockReturnValue(
+        E.fail(new ConversionError({ format: 'srt' }))
+      )
@@
-      await expect(
-        E.runPromise(enhancedProcessSubtitlesHandler(request))
-      ).rejects.toThrow()
+      const result = await E.runPromise(
+        enhancedProcessSubtitlesHandler(request).pipe(E.either)
+      )
+      expect(result._tag).toBe('Left')
+      // @ts-expect-error - runtime check
+      expect(result.left._tag).toBe('SubtitleConversionFailed')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Mock conversion failure
mockConvert.mockReturnValue(E.fail(
new Error('Conversion failed')
))
await expect(
E.runPromise(enhancedProcessSubtitlesHandler(request))
).rejects.toThrow()
})
// at top of file, alongside other imports
import { ConversionError } from './subtitle-formats.errors'
// … rest of imports and setup …
describe('enhancedProcessSubtitlesHandler', () => {
// … earlier tests …
it.effect('errors: maps ConversionError to SubtitleConversionFailed', async () => {
// Mock typed conversion failure that the handler maps to SubtitleConversionFailed
mockConvert.mockReturnValue(
E.fail(new ConversionError({ format: 'srt' }))
)
const result = await E.runPromise(
enhancedProcessSubtitlesHandler(request).pipe(E.either)
)
expect(result._tag).toBe('Left')
// @ts-expect-error - runtime check of runtime error tag
expect(result.left._tag).toBe('SubtitleConversionFailed')
})
})
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts around
lines 251 to 259, the test currently mocks a generic Error for conversion
failure; change the mock to return a typed ConversionError (e.g. E.fail(new
ConversionError(...))) so the handler’s E.catchTags branch is exercised, and
update the assertion to expect the rejection to be a SubtitleConversionFailed
(e.g. assert the thrown error is an instance/type or message matching
SubtitleConversionFailed) instead of a generic toThrow.

Comment on lines +336 to +359
describe('Type Safety', () => {
it('should enforce type safety for subtitle data', () => {
// This test ensures TypeScript compilation works correctly
const validSubtitleItem: SubtitleItem = {
start: 0,
end: 1000,
text: 'Valid subtitle',
speaker: 1
}

expect(validSubtitleItem.start).toBe(0)
expect(validSubtitleItem.end).toBe(1000)
expect(validSubtitleItem.text).toBe('Valid subtitle')
expect(validSubtitleItem.speaker).toBe(1)
})

it('should enforce type safety for format enum', () => {
const validFormats: SubtitleFormat[] = ['json', 'srt', 'vtt', 'plain-text']

validFormats.forEach(format => {
expect(['json', 'srt', 'vtt', 'plain-text']).toContain(format)
})
})
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

test(types): avoid runtime tests for compile-time-only guarantees

These assertions validate TypeScript-only properties at runtime. Prefer tsd (or a dedicated type test) for compile-time checks, or drop them.

Option A: move to tsd-based tests.
Option B: remove the block to keep the suite focused on runtime behaviour.

🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-processor-enhanced.test.ts around
lines 336-359, the test block asserts TypeScript compile-time guarantees at
runtime (type safety for SubtitleItem and SubtitleFormat); remove this
runtime-only test block or move its intent into a tsd-based type test suite:
delete the entire "Type Safety" describe block from the Jest tests, or create
corresponding .test-d.ts tsd files that use expectType/expectAssignable for
SubtitleItem and SubtitleFormat and add tsd to devDependencies and CI.

Comment on lines +27 to +44
it('should test health check endpoint', async () => {
if (!serverRunning) {
console.log('⏭️ Skipping test - server not running')
return
}

console.log('1️⃣ Testing Health Check...')
const healthResponse = await fetch(`${BASE_URL}/subtitles/health`)
const healthData = await healthResponse.json()

expect(healthResponse.status).toBe(200)
expect(healthData.status).toBe('healthy')
expect(healthData.service).toBe('subtitle-processor')
expect(healthData.timestamp).toBeDefined()

console.log('✅ Health Check Response:', healthData)
console.log('Status:', healthResponse.status)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Test cleanup needed in the lifecycle hooks

The afterAll hook is empty and the test skipping pattern in lines 28-31 could be improved. Consider using Vitest's built-in test skipping functionality.

-  afterAll(() => {
-    // Cleanup if needed
-  })
+  afterAll(async () => {
+    // Add any necessary cleanup here
+  })

   it('should test health check endpoint', async () => {
-    if (!serverRunning) {
-      console.log('⏭️  Skipping test - server not running')
-      return
-    }
+    it.skipIf(!serverRunning)('should test health check endpoint', async () => {

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +362 to +373
it('should test all endpoints comprehensively', async () => {
if (!serverRunning) {
console.log('⏭️ Skipping test - server not running')
return
}

console.log('🎯 Running comprehensive endpoint test...')

// This test runs all the above tests in sequence
// The individual tests above will handle the assertions
console.log('🎉 All endpoint tests completed successfully!')
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Incomplete test implementation

This test appears to be a placeholder that doesn't add value. It should either be implemented with actual comprehensive testing logic or removed.

Either implement the comprehensive test properly or remove it entirely:

-  it('should test all endpoints comprehensively', async () => {
-    if (!serverRunning) {
-      console.log('⏭️  Skipping test - server not running')
-      return
-    }
-
-    console.log('🎯 Running comprehensive endpoint test...')
-    
-    // This test runs all the above tests in sequence
-    // The individual tests above will handle the assertions
-    console.log('🎉 All endpoint tests completed successfully!')
-  })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should test all endpoints comprehensively', async () => {
if (!serverRunning) {
console.log('⏭️ Skipping test - server not running')
return
}
console.log('🎯 Running comprehensive endpoint test...')
// This test runs all the above tests in sequence
// The individual tests above will handle the assertions
console.log('🎉 All endpoint tests completed successfully!')
})
// The comprehensive endpoint test placeholder has been removed.
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/test-enhanced-endpoints.test.ts around
lines 362 to 373, the test labeled "should test all endpoints comprehensively"
is just a placeholder that logs messages and returns when serverRunning is
false; either implement it to run the actual sequence of endpoint tests and
assert results, or delete the test entirely to avoid meaningless coverage. If
implementing, invoke the existing endpoint test functions in sequence (awaiting
each), collect/propagate any assertion failures, and remove the console-only
branches; if removing, delete the entire it(...) block and any references to
this wrapper so the test suite relies on the individual tests already present.

@sjiamnocna sjiamnocna requested a review from BleedingDev August 21, 2025 08:01
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/domain/media/subtitle-formats/subtitle-converter.ts (1)

1-745: chore: enforce consistent Schema import and consolidate duplicate helpers

  • Replace incorrect import type { Schema } from 'effect' with a real import alias in:
    • src/domain/media/subtitle-formats/subtitle-converter.ts
    • src/handlers/media/transcribe-workflow.handler.ts
  • import type { Schema } from 'effect'
  • import { Schema as S } from 'effect'
    • Update all calls to `Schema.validate(...)` → `S.validate(...)`
    
    
  • Consolidate subtitle-timing helpers
    We currently have two identical implementations:

    • applyTimingOffset in subtitle-converter.ts
    • addTimingOffset in subtitle-filters.ts and referenced in tests
      Action:
      • Remove one duplicate (prefer applyTimingOffset)
      • Update subtitle-filters.ts, subtitle-pipeline-simple.ts and all tests to import & use applyTimingOffset only
  • DRY up time-formatting utilities
    The functions formatTimeSrt and formatTimeVtt are duplicated in:

    • subtitle-converter.ts
    • subtitle-filters.ts
    • subtitle-pipeline-simple.ts
      Action:
      • Extract both into a new module, e.g. src/utils/time-formats.ts
      • Import them where needed and remove their local definitions

Ensure all corresponding tests import from the updated locations. This refactor will eliminate runtime errors (due to the type-only import) and reduce duplication across the codebase.

♻️ Duplicate comments (7)
src/domain/media/subtitle-formats/subtitle-converter.ts (7)

1-16: fix: replace invalid Effect Schema import; import schema values correctly

  • Importing type { Schema } and later using it as a value breaks at compile time.
  • Per repo rules, use Schema as S.
  • Also, SubtitleConversionResultSchema is a schema value, not a type; importing it with type is incorrect.
  • To use schema values alongside homonymous exported types (e.g., SubtitleJson), import the schema value under an alias.

Apply:

-import { Effect as E, Option, Stream } from 'effect'
-import type { Schema } from 'effect'
+import { Effect as E, Option, Stream, Schema as S } from 'effect'
@@
-import type {
-  ConversionOptions,
-  MultipleFormatResult,
-  SubtitleConversionResultSchema,
-  SubtitleFormat,
-  SubtitleItem,
-  SubtitleJson,
-} from './subtitle-formats.schema'
+import type {
+  ConversionOptions,
+  MultipleFormatResult,
+  SubtitleFormat,
+  SubtitleItem,
+  SubtitleJson,
+} from './subtitle-formats.schema'
+// schema values (runtime) — alias to avoid clashing with the homonymous types
+import {
+  SubtitleJson as SubtitleJsonSchema,
+  SubtitleItem as SubtitleItemSchema,
+  SubtitleConversionResultSchema,
+} from './subtitle-formats.schema'

152-165: refactor: de-duplicate timing offset logic; reuse shared filter

applyTimingOffset duplicates single-item logic already present in subtitle-filters.ts (addTimingOffset). Centralise in one place to prevent drift.

Proposed minimal change:

-/**
- * Applies timing offset to subtitle items using generator for streaming processing
- */
-export const applyTimingOffset =
-  (offset: number) =>
-  (subtitle: SubtitleItem): SubtitleItem => ({
-    ...subtitle,
-    start: Math.max(0, subtitle.start + offset),
-    end: Math.max(0, subtitle.end + offset),
-  })
+// Reuse canonical transformation from filters
+import { addTimingOffset as applyTimingOffset } from './subtitle-filters'

If you prefer to keep it here, delete the duplicate in the filters module and export from a single canonical location.


351-379: perf: validate items directly instead of wrapping each in an array

Wrapping each item in a one-element array to call validateSubtitleData([item]) is unnecessary overhead and obscures item-level failures. Decode a single SubtitleItem with Effect Schema.

   return Stream.fromIterable(subtitles).pipe(
-    Stream.mapEffect((item) =>
-      validateSubtitleData([item], allowEmptyText).pipe(E.map((arr) => arr[0])),
-    ),
+    Stream.mapEffect((item) =>
+      S.decodeUnknown(SubtitleItemSchema)(item).pipe(
+        E.mapError(
+          (cause) =>
+            new InvalidSubtitleDataError({
+              cause: cause instanceof Error ? cause : new Error(String(cause)),
+            }),
+        ),
+      ),
+    ),

495-515: refactor: extract time-format utilities into a shared module

formatTimeSrt and formatTimeVtt are duplicated elsewhere. Extract to src/domain/media/subtitle-formats/subtitle-time-utils.ts and import here and in other consumers.

Suggested extraction:

-export const formatTimeSrt = (ms: number): string => {
+// subtitle-time-utils.ts
+export const formatTimeSrt = (ms: number): string => {
@@
-}
+}
@@
-export const formatTimeVtt = (ms: number): string => {
+export const formatTimeVtt = (ms: number): string => {
@@
-}
+}

Then:

+import { formatTimeSrt, formatTimeVtt } from './subtitle-time-utils'

660-678: docs: collapse duplicate JSDoc blocks for convert

There are two consecutive doc blocks describing the same method. Keep a single authoritative block to avoid drift.

-  /**
-   * Converts subtitle data to a specific format
-   *
-   * @param subtitles - Universal subtitle data (text with timing)
-   * @param format - Target format for conversion (json, srt, vtt, plain-text)
-   * @param options - Processing options to apply before conversion
-   * @returns Effect that succeeds with converted content or fails with conversion error
-   */

684-701: fix: schema validation usage and error construction are incorrect

  • Schema.validate is not the recommended API; use S.decodeUnknown to produce an Effect.
  • SubtitleJsonSchema isn’t imported; the current code won’t compile.
  • InvalidSubtitleDataError requires { cause: Error } per your error type; passing a string breaks the tag constructor.
-    E.gen(function* () {
-      // Validate subtitles against the schema
-      const validationResult = Schema.validate(SubtitleJsonSchema, subtitles)
-      if (validationResult._tag === 'Left') {
-        throw new InvalidSubtitleDataError('Invalid subtitle data: ' + validationResult.left)
-      }
-      // Proceed with conversion if validation succeeds
-      return yield* convertSubtitleFormat(subtitles, format, options)
-    }).pipe(
+    E.gen(function* () {
+      // Validate against Effect Schema (returns Effect)
+      yield* S.decodeUnknown(SubtitleJsonSchema)(subtitles).pipe(
+        E.mapError(
+          (cause) =>
+            new InvalidSubtitleDataError({
+              cause: cause instanceof Error ? cause : new Error(String(cause)),
+            }),
+        ),
+      )
+      // Proceed with conversion if validation succeeds
+      return yield* convertSubtitleFormat(subtitles, format, options)
+    }).pipe(

717-731: fix: incorrect Schema namespace usage in type; align with S and ensure value import

Use S.Schema.Type<typeof SubtitleConversionResultSchema> and ensure SubtitleConversionResultSchema is imported as a value (addressed in the import fix).

-      const results: Schema.Schema.Type<
-        typeof SubtitleConversionResultSchema
-      >[] = []
+      const results: S.Schema.Type<typeof SubtitleConversionResultSchema>[] = []
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f8b7dfb and d3a3505.

📒 Files selected for processing (1)
  • src/domain/media/subtitle-formats/subtitle-converter.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.ts

📄 CodeRabbit inference engine (.rules/code-style.mdc)

src/**/*.ts: Always use relative paths for internal imports; never use src/ aliases for internal imports in TypeScript files.
Use package imports only for external dependencies in TypeScript files.
Avoid obvious comments; never add comments that just repeat what the code is doing. Only add comments for complex business logic that needs explanation.
Use JSDoc for public APIs, not inline comments for obvious operations.
Add comments only for complex business logic, non-obvious workarounds, edge cases, or important context that affects future changes.
Use Effect.Service class extension pattern for services, include dependencies in service definition, and provide makeTestService for testing.
Use E.gen(function* () {}) for complex flows; yield effects directly without helper functions; use yield* for effects that return values.
Use Data.Tagged for domain errors, create specific error types for different failure cases, and handle errors explicitly with E.catchTags.
Use Effect's logging utilities instead of console.log; include relevant context in log messages and use appropriate log levels.
Add spans for observability using E.withSpan, include relevant attributes, and use descriptive span names.
Use PascalCase for service classes (e.g., BlogStore, SearchStore).
Use camelCase for functions and variables (e.g., getBlogById, blogPost).
Use UPPERCASE for constants (e.g., TABLE_NAME, DEFAULT_LIMIT).
Use descriptive names for functions and methods that indicate purpose.
Prefix usecases with action (e.g., createBlogPostUsecase, getBlogByIdUsecase).
Prefix handlers with HTTP method context (e.g., _handler, handler).
Use Effect Schema for all data validation; define schemas with explicit types and use branded types for domain identifiers.
Use branded types for domain identifiers and create factory functions with validation.
Access configuration through the Effect system, not directly via process.env.
Use pipe syntax for effect composition;...

Files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
src/**/*

📄 CodeRabbit inference engine (.rules/code-style.mdc)

Use kebab-case for file names (e.g., blog.store.ts, get-blog-by-id.ts).

Files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.rules/effect-service-architecture.mdc)

Always use relative paths for internal imports; never use src/ aliases for internal imports. Use package imports only for external dependencies.

Files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
🧠 Learnings (12)
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use Effect Schema for all data validation; define schemas with explicit types and use branded types for domain identifiers.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:48:12.331Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/schema-validation.mdc:0-0
Timestamp: 2025-08-10T21:48:12.331Z
Learning: Applies to **/*.types.ts|**/schema/**/*.ts|**/schemas/**/*.ts|**/*-schema.ts|**/*-schemas.ts|**/branded.ts|**/domain/**/*.ts : Always use `Schema as S` for Effect Schema imports

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:46:10.977Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/cursor-rule-maintainer.mdc:0-0
Timestamp: 2025-08-10T21:46:10.977Z
Learning: Applies to .cursor/rules/schema-validation.mdc : Add new Effect Schema patterns, validation improvements, branded type patterns, and error handling for schemas to `schema-validation.mdc`

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Use proper Effect error handling patterns in utility functions.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:48:12.331Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/schema-validation.mdc:0-0
Timestamp: 2025-08-10T21:48:12.331Z
Learning: Applies to **/schema/**/*.ts|**/schemas/**/*.ts|**/*-schema.ts|**/*-schemas.ts : Test schema validation with valid and invalid inputs using effect/vitest and Effect-based test patterns

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Use Effect-TS patterns for async operations in utility functions.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:45:28.136Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/api-client-patterns.mdc:0-0
Timestamp: 2025-08-10T21:45:28.136Z
Learning: Applies to **/*.api-client.ts : Import types with the `type` keyword when used only for typing; import schemas without `type` when used for encoding/decoding.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Always use `effect/vitest` for tests involving Effect-based code; import test functions (`describe`, `expect`, `it`) from `effect/vitest`, not regular `vitest`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use proper branded type constructors for mock data in tests.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:47:21.625Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Use `it.effect` for tests that need TestContext (TestClock, etc.) in Effect-TS test files.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:46:54.925Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-store-patterns.mdc:0-0
Timestamp: 2025-08-10T21:46:54.925Z
Learning: Applies to **/*.store.ts : Convert external errors (e.g., DynamoDB, S3) to domain errors using Effect-TS error handling patterns

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
📚 Learning: 2025-08-10T21:45:57.853Z
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Access configuration through the Effect system, not directly via `process.env`.

Applied to files:

  • src/domain/media/subtitle-formats/subtitle-converter.ts
🧬 Code graph analysis (1)
src/domain/media/subtitle-formats/subtitle-converter.ts (2)
src/domain/media/subtitle-formats/subtitle-formats.errors.ts (4)
  • InvalidSubtitleDataError (37-41)
  • InvalidTimingError (50-52)
  • UnsupportedFormatError (43-48)
  • ConversionError (54-57)
src/domain/media/subtitle-formats/subtitle-formats.schema.ts (11)
  • SubtitleJson (21-21)
  • SubtitleJson (97-97)
  • SubtitleItem (7-16)
  • SubtitleItem (96-96)
  • ConversionOptions (31-42)
  • ConversionOptions (99-99)
  • SubtitleFormat (26-26)
  • SubtitleFormat (98-98)
  • SubtitleConversionResultSchema (54-57)
  • MultipleFormatResult (59-61)
  • MultipleFormatResult (100-102)
🔇 Additional comments (4)
src/domain/media/subtitle-formats/subtitle-converter.ts (4)

542-564: style: SRT generation is correct and readable

Block numbering, timing lines, and blank line separation are handled properly. Good use of helpers and spans.


572-593: style: VTT generation with header looks good

Includes WEBVTT header, proper cue formatting, and observability span.


626-658: docs: solid, focused service description

Good separation of concerns and explicit non-goals. Clear examples and effect-aware usage.


396-440: nit: prefer E.catchTags for domain errors in convertSubtitleFormat

The function returns domain-tagged errors; consider E.catchTags where you need targeted handling, especially for UnsupportedFormatError, to enrich logs/spans.

No code change required; just a recommendation to keep error handling idiomatic to Effect.

⛔ Skipped due to learnings
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/code-style.mdc:0-0
Timestamp: 2025-08-10T21:45:57.853Z
Learning: Applies to src/**/*.ts : Use `Data.Tagged` for domain errors, create specific error types for different failure cases, and handle errors explicitly with `E.catchTags`.
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-store-patterns.mdc:0-0
Timestamp: 2025-08-10T21:46:54.925Z
Learning: Applies to **/*.store.ts : Convert external errors (e.g., DynamoDB, S3) to domain errors using Effect-TS error handling patterns
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-store-patterns.mdc:0-0
Timestamp: 2025-08-10T21:46:54.925Z
Learning: Applies to **/*-store.ts : Convert external errors (e.g., DynamoDB, S3) to domain errors using Effect-TS error handling patterns
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-store-patterns.mdc:0-0
Timestamp: 2025-08-10T21:46:54.925Z
Learning: Applies to **/stores/**/*.ts : Convert external errors (e.g., DynamoDB, S3) to domain errors using Effect-TS error handling patterns
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Never use `throw new Error()` in Effect-TS applications; always create and use `Data.TaggedError` for error handling.
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-service-architecture.mdc:0-0
Timestamp: 2025-08-10T21:46:38.101Z
Learning: Use tagged errors for type-safe error handling. Convert external errors to domain errors at store boundaries, add context and logging at the usecase level, and map to appropriate HTTP responses at the handler level.
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-handler-patterns.mdc:0-0
Timestamp: 2025-08-10T21:46:24.385Z
Learning: Applies to **/lambdas/**/handler.ts,**/*-handler.ts : Handlers must map domain errors to HTTP responses using 'E.catchTags' with appropriate error-to-response mappings.
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Applies to **/*.test.ts : Test tagged errors with proper Effect patterns (using `E.exit` and `getExitError`).
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/schema-validation.mdc:0-0
Timestamp: 2025-08-10T21:48:12.331Z
Learning: Applies to **/*.types.ts|**/schema/**/*.ts|**/schemas/**/*.ts|**/*-schema.ts|**/*-schemas.ts|**/branded.ts|**/domain/**/*.ts : Use `Data.TaggedError` for domain-specific error types and include relevant identifiers and context in error data
Learnt from: CR
PR: BleedingDev/coursition-effect-experiment#0
File: .rules/effect-testing-patterns.mdc:0-0
Timestamp: 2025-08-10T21:47:21.625Z
Learning: Use proper Effect error handling patterns in utility functions.

Comment on lines +143 to +150
E.tapError(E.logError),
E.withSpan('validateSubtitleData', {
attributes: {
count: Array.isArray(subtitles) ? subtitles.length : 0,
hasOptions: allowEmptyText !== undefined,
},
}),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

chore: correct span attribute; current expression is always true

hasOptions: allowEmptyText !== undefined is always true because allowEmptyText has a default. Use the boolean flag directly or rename for clarity.

-        hasOptions: allowEmptyText !== undefined,
+        allowEmptyText,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
E.tapError(E.logError),
E.withSpan('validateSubtitleData', {
attributes: {
count: Array.isArray(subtitles) ? subtitles.length : 0,
hasOptions: allowEmptyText !== undefined,
},
}),
)
E.tapError(E.logError),
E.withSpan('validateSubtitleData', {
attributes: {
count: Array.isArray(subtitles) ? subtitles.length : 0,
allowEmptyText,
},
}),
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-converter.ts around lines 143 to
150, the span attribute uses `hasOptions: allowEmptyText !== undefined` which is
always true due to the default parameter; replace that expression with a boolean
or clearer name: either set `hasOptions: !!allowEmptyText` to reflect its
truthiness, or rename the attribute to `hasAllowEmptyText: allowEmptyText`
(preferably the latter for clarity) so the span accurately reports whether the
allowEmptyText flag is enabled.

Comment on lines +200 to +269
/**
* Merges adjacent subtitles that are close in timing using generator for streaming processing
*
* @param subtitles - Array of subtitle items to merge
* @param threshold - Maximum gap in milliseconds to consider subtitles adjacent
* @returns Effect that succeeds with merged subtitles or fails with processing error
*/
export const mergeAdjacentSubtitles = (
subtitles: SubtitleItem[],
threshold: number,
) => {
if (subtitles.length <= 1) {
return E.succeed(subtitles)
}

const merged: SubtitleItem[] = []
const first = subtitles[0]
if (!first) {
return E.succeed(subtitles)
}
let current: SubtitleItem = {
start: first.start,
end: first.end,
text: first.text,
speaker: first.speaker,
}

// Process subtitles one by one
for (let i = 1; i < subtitles.length; i++) {
const next = subtitles[i]
if (!next) {
continue
}

const gap = next.start - current.end

if (gap <= threshold) {
// Merge subtitles
const mergedSubtitle: SubtitleItem = {
start: current.start,
end: next.end,
text: `${current.text} ${next.text}`,
speaker: current.speaker === next.speaker ? current.speaker : undefined,
}
current = mergedSubtitle
} else {
// Add current to merged array and start new current
merged.push(current)
current = {
start: next.start,
end: next.end,
text: next.text,
speaker: next.speaker,
}
}
}

// Add the last subtitle
merged.push(current)

return E.succeed(merged).pipe(
E.tapError(E.logError),
E.withSpan('mergeAdjacentSubtitles', {
attributes: {
originalCount: subtitles.length,
threshold,
},
}),
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

behaviour: merging concatenates with a space; consider preserving line breaks

For adjacent cues, preserving a newline tends to read better in SRT/VTT outputs. Optional, but improves readability.

-        text: `${current.text} ${next.text}`,
+        text: `${current.text}\n${next.text}`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Merges adjacent subtitles that are close in timing using generator for streaming processing
*
* @param subtitles - Array of subtitle items to merge
* @param threshold - Maximum gap in milliseconds to consider subtitles adjacent
* @returns Effect that succeeds with merged subtitles or fails with processing error
*/
export const mergeAdjacentSubtitles = (
subtitles: SubtitleItem[],
threshold: number,
) => {
if (subtitles.length <= 1) {
return E.succeed(subtitles)
}
const merged: SubtitleItem[] = []
const first = subtitles[0]
if (!first) {
return E.succeed(subtitles)
}
let current: SubtitleItem = {
start: first.start,
end: first.end,
text: first.text,
speaker: first.speaker,
}
// Process subtitles one by one
for (let i = 1; i < subtitles.length; i++) {
const next = subtitles[i]
if (!next) {
continue
}
const gap = next.start - current.end
if (gap <= threshold) {
// Merge subtitles
const mergedSubtitle: SubtitleItem = {
start: current.start,
end: next.end,
text: `${current.text} ${next.text}`,
speaker: current.speaker === next.speaker ? current.speaker : undefined,
}
current = mergedSubtitle
} else {
// Add current to merged array and start new current
merged.push(current)
current = {
start: next.start,
end: next.end,
text: next.text,
speaker: next.speaker,
}
}
}
// Add the last subtitle
merged.push(current)
return E.succeed(merged).pipe(
E.tapError(E.logError),
E.withSpan('mergeAdjacentSubtitles', {
attributes: {
originalCount: subtitles.length,
threshold,
},
}),
)
}
export const mergeAdjacentSubtitles = (
subtitles: SubtitleItem[],
threshold: number,
) => {
if (subtitles.length <= 1) {
return E.succeed(subtitles)
}
const merged: SubtitleItem[] = []
const first = subtitles[0]
if (!first) {
return E.succeed(subtitles)
}
let current: SubtitleItem = {
start: first.start,
end: first.end,
text: first.text,
speaker: first.speaker,
}
// Process subtitles one by one
for (let i = 1; i < subtitles.length; i++) {
const next = subtitles[i]
if (!next) {
continue
}
const gap = next.start - current.end
if (gap <= threshold) {
// Merge subtitles
const mergedSubtitle: SubtitleItem = {
start: current.start,
end: next.end,
text: `${current.text}\n${next.text}`,
speaker: current.speaker === next.speaker ? current.speaker : undefined,
}
current = mergedSubtitle
} else {
// Add current to merged array and start new current
merged.push(current)
current = {
start: next.start,
end: next.end,
text: next.text,
speaker: next.speaker,
}
}
}
// Add the last subtitle
merged.push(current)
return E.succeed(merged).pipe(
E.tapError(E.logError),
E.withSpan('mergeAdjacentSubtitles', {
attributes: {
originalCount: subtitles.length,
threshold,
},
}),
)
}
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-converter.ts around lines 200 to
269, the merge logic currently concatenates adjacent subtitle texts with a
single space which loses original line breaks and reduces readability in SRT/VTT
outputs; change the merge to preserve/newline separate cues by joining texts
with '\n' (optionally trimming trailing/leading whitespace on parts to avoid
double spaces/newlines) when building mergedSubtitle and when initializing
current text for new groups, keeping speaker merge logic unchanged.

Comment on lines +278 to +303
// Helper function to process a single subtitle item
const processSingleSubtitle = (
item: SubtitleItem,
options?: ConversionOptions,
): SubtitleItem => {
let processedItem = item

// 1. Apply timing offset first
if (options?.timingOffset) {
processedItem = applyTimingOffset(options.timingOffset)(processedItem)
}

// 2. Clean text second
if (options?.cleanText !== false) {
// Default to true
processedItem = cleanSubtitleText(processedItem)
}

// 3. Add speaker info last
if (options?.includeSpeaker) {
processedItem = addSpeakerInfo(true)(processedItem)
}

return processedItem
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

nit: timing offset guard should check for defined, not truthy

if (options?.timingOffset) skips when 0 is provided. While a zero offset is a no-op, make the intent explicit and future-proof by checking for definedness.

-  if (options?.timingOffset) {
+  if (options?.timingOffset !== undefined) {
     processedItem = applyTimingOffset(options.timingOffset)(processedItem)
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Helper function to process a single subtitle item
const processSingleSubtitle = (
item: SubtitleItem,
options?: ConversionOptions,
): SubtitleItem => {
let processedItem = item
// 1. Apply timing offset first
if (options?.timingOffset) {
processedItem = applyTimingOffset(options.timingOffset)(processedItem)
}
// 2. Clean text second
if (options?.cleanText !== false) {
// Default to true
processedItem = cleanSubtitleText(processedItem)
}
// 3. Add speaker info last
if (options?.includeSpeaker) {
processedItem = addSpeakerInfo(true)(processedItem)
}
return processedItem
}
// Helper function to process a single subtitle item
const processSingleSubtitle = (
item: SubtitleItem,
options?: ConversionOptions,
): SubtitleItem => {
let processedItem = item
// 1. Apply timing offset first
if (options?.timingOffset !== undefined) {
processedItem = applyTimingOffset(options.timingOffset)(processedItem)
}
// 2. Clean text second
if (options?.cleanText !== false) {
// Default to true
processedItem = cleanSubtitleText(processedItem)
}
// 3. Add speaker info last
if (options?.includeSpeaker) {
processedItem = addSpeakerInfo(true)(processedItem)
}
return processedItem
}
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-converter.ts around lines 278 to
303, the guard `if (options?.timingOffset)` incorrectly treats 0 as falsy and
will skip applying a zero offset; change the check to test for definedness
(e.g., `options?.timingOffset !== undefined` or `options?.timingOffset != null`)
so a 0 offset is handled explicitly, ensuring applyTimingOffset is invoked when
timingOffset is provided even if it is zero.

Comment on lines +351 to +379
export const processSubtitlesStream = (
subtitles: SubtitleJson,
options?: ConversionOptions,
) => {
// Validate input data first (allow empty text if cleanText is enabled)
const allowEmptyText = options?.cleanText === true
return Stream.fromIterable(subtitles).pipe(
Stream.mapEffect((item) =>
validateSubtitleData([item], allowEmptyText).pipe(E.map((arr) => arr[0])),
),
// Apply transformations in parallel
Stream.map((item) => {
let processedItem = item
if (options?.timingOffset) {
processedItem = applyTimingOffset(options.timingOffset)(processedItem)
}
if (options?.cleanText !== false) {
processedItem = cleanSubtitleText(processedItem)
}
if (options?.includeSpeaker) {
processedItem = addSpeakerInfo(true)(processedItem)
}
return processedItem
}),
// Filter out empty text if cleanText is enabled
options?.cleanText === true
? Stream.filter((item) => item.text.trim().length > 0)
: (s) => s,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

consistency: streaming contract vs PR objective

Both processSubtitlesStream and convertSubtitleFormatStream should accept/return true streams (generator/async iterator in, Stream<string> out). With the proposed changes above, the output side is addressed; consider adding overloads to accept AsyncIterable<SubtitleItem> to fully meet the “accepts a stream” objective.

Happy to provide a follow-up diff adding an overload:

  • processSubtitlesStream(input: AsyncIterable<SubtitleItem> | SubtitleJson, options?: ConversionOptions): Stream<SubtitleItem>
  • Normalise AsyncIterable via Stream.fromAsyncIterable.

Also applies to: 444-474

🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-converter.ts around lines 351 to
379 (and similarly adjust lines 444–474), the function currently only accepts a
SubtitleJson array input; add overloads so it also accepts
AsyncIterable<SubtitleItem> and normalize that input into a Stream using
Stream.fromAsyncIterable (or Stream.fromIterable for arrays) so the function
signature becomes processSubtitlesStream(input: AsyncIterable<SubtitleItem> |
SubtitleJson, options?: ConversionOptions): Stream<SubtitleItem>; update
internal code to branch on input type to create the base Stream, keep the
existing pipeline intact, and ensure the exported return type is
Stream<SubtitleItem> to satisfy the streaming contract.

🧹 Nitpick (assertive)

observability: add a span around stream processing

Other functions include spans; processSubtitlesStream currently has none. Add a withSpan (e.g., via Stream.unwrapScoped or wrapping the validation mapEffect) to provide parity in tracing.

Example:

-  return Stream.fromIterable(subtitles).pipe(
+  return Stream.fromIterable(subtitles).pipe(
+    // annotate processing
+    Stream.tap(() =>
+      E.withSpan('processSubtitlesStream', {
+        attributes: { count: subtitles.length, hasOptions: options !== undefined },
+      })(E.unit),
+    ),

Use any Stream-friendly pattern your team prefers for spans.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const processSubtitlesStream = (
subtitles: SubtitleJson,
options?: ConversionOptions,
) => {
// Validate input data first (allow empty text if cleanText is enabled)
const allowEmptyText = options?.cleanText === true
return Stream.fromIterable(subtitles).pipe(
Stream.mapEffect((item) =>
validateSubtitleData([item], allowEmptyText).pipe(E.map((arr) => arr[0])),
),
// Apply transformations in parallel
Stream.map((item) => {
let processedItem = item
if (options?.timingOffset) {
processedItem = applyTimingOffset(options.timingOffset)(processedItem)
}
if (options?.cleanText !== false) {
processedItem = cleanSubtitleText(processedItem)
}
if (options?.includeSpeaker) {
processedItem = addSpeakerInfo(true)(processedItem)
}
return processedItem
}),
// Filter out empty text if cleanText is enabled
options?.cleanText === true
? Stream.filter((item) => item.text.trim().length > 0)
: (s) => s,
)
export const processSubtitlesStream = (
subtitles: SubtitleJson,
options?: ConversionOptions,
) => {
// Validate input data first (allow empty text if cleanText is enabled)
const allowEmptyText = options?.cleanText === true
return Stream.fromIterable(subtitles).pipe(
// annotate processing
Stream.tap(() =>
E.withSpan('processSubtitlesStream', {
attributes: { count: subtitles.length, hasOptions: options !== undefined },
})(E.unit),
),
Stream.mapEffect((item) =>
validateSubtitleData([item], allowEmptyText).pipe(E.map((arr) => arr[0])),
),
// Apply transformations in parallel
Stream.map((item) => {
let processedItem = item
if (options?.timingOffset) {
processedItem = applyTimingOffset(options.timingOffset)(processedItem)
}
if (options?.cleanText !== false) {
processedItem = cleanSubtitleText(processedItem)
}
if (options?.includeSpeaker) {
processedItem = addSpeakerInfo(true)(processedItem)
}
return processedItem
}),
// Filter out empty text if cleanText is enabled
options?.cleanText === true
? Stream.filter((item) => item.text.trim().length > 0)
: (s) => s,
)
}

Comment on lines +444 to +474
export const convertSubtitleFormatStream = (
subtitles: SubtitleJson,
format: SubtitleFormat,
options?: ConversionOptions,
) =>
processSubtitlesStream(subtitles, options).pipe(
Stream.runCollect,
E.map((chunk) => Array.from(chunk)),
E.flatMap((arr) => {
switch (format) {
case 'json':
return convertToJson(arr)
case 'srt':
return convertToSrt(arr)
case 'vtt':
return convertToVtt(arr)
case 'plain-text':
return convertToPlainText(arr)
default:
return E.fail(
new ConversionError({
format: String(format),
cause: new UnsupportedFormatError({
format,
supportedFormats: ['json', 'srt', 'vtt', 'plain-text'],
}),
}),
)
}
}),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

feat: return a real stream of incremental output; avoid runCollect

The current implementation collects the stream into an array and returns a single Effect<string>, defeating incremental streaming. Return Stream<string> and emit format-specific chunks as items arrive. This aligns with the PR goal (“produces incremental, valid subtitle output”).

-export const convertSubtitleFormatStream = (
+export const convertSubtitleFormatStream = (
   subtitles: SubtitleJson,
   format: SubtitleFormat,
   options?: ConversionOptions,
 ) =>
-  processSubtitlesStream(subtitles, options).pipe(
-    Stream.runCollect,
-    E.map((chunk) => Array.from(chunk)),
-    E.flatMap((arr) => {
-      switch (format) {
-        case 'json':
-          return convertToJson(arr)
-        case 'srt':
-          return convertToSrt(arr)
-        case 'vtt':
-          return convertToVtt(arr)
-        case 'plain-text':
-          return convertToPlainText(arr)
-        default:
-          return E.fail(
-            new ConversionError({
-              format: String(format),
-              cause: new UnsupportedFormatError({
-                format,
-                supportedFormats: ['json', 'srt', 'vtt', 'plain-text'],
-              }),
-            }),
-          )
-      }
-    }),
-  )
+  {
+    const processed$ = processSubtitlesStream(subtitles, options)
+    switch (format) {
+      case 'json': {
+        // Stream a valid JSON array: [item0,item1,...]
+        const body$ = processed$.pipe(
+          Stream.map((item) => JSON.stringify(item)),
+          Stream.mapAccum(false as boolean, (seen, s) => [true, (seen ? ',' : '') + s]),
+        )
+        return Stream.concat(Stream.succeed('['), Stream.concat(body$, Stream.succeed(']')))
+      }
+      case 'srt': {
+        return processed$.pipe(
+          Stream.mapAccum(1, (index, item) => [
+            index + 1,
+            `${index}\n${formatTimeSrt(item.start)} --> ${formatTimeSrt(item.end)}\n${item.text}\n\n`,
+          ]),
+        )
+      }
+      case 'vtt': {
+        const body$ = processed$.pipe(
+          Stream.map(
+            (item) =>
+              `${formatTimeVtt(item.start)} --> ${formatTimeVtt(item.end)}\n${item.text}\n\n`,
+          ),
+        )
+        return Stream.concat(Stream.succeed('WEBVTT\n\n'), body$)
+      }
+      case 'plain-text': {
+        return processed$.pipe(Stream.map((item) => `${item.text}\n\n`))
+      }
+      default: {
+        return Stream.fail(
+          new ConversionError({
+            format: String(format),
+            cause: new UnsupportedFormatError({
+              format,
+              supportedFormats: ['json', 'srt', 'vtt', 'plain-text'],
+            }),
+          }),
+        )
+      }
+    }
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const convertSubtitleFormatStream = (
subtitles: SubtitleJson,
format: SubtitleFormat,
options?: ConversionOptions,
) =>
processSubtitlesStream(subtitles, options).pipe(
Stream.runCollect,
E.map((chunk) => Array.from(chunk)),
E.flatMap((arr) => {
switch (format) {
case 'json':
return convertToJson(arr)
case 'srt':
return convertToSrt(arr)
case 'vtt':
return convertToVtt(arr)
case 'plain-text':
return convertToPlainText(arr)
default:
return E.fail(
new ConversionError({
format: String(format),
cause: new UnsupportedFormatError({
format,
supportedFormats: ['json', 'srt', 'vtt', 'plain-text'],
}),
}),
)
}
}),
)
export const convertSubtitleFormatStream = (
subtitles: SubtitleJson,
format: SubtitleFormat,
options?: ConversionOptions,
) => {
const processed$ = processSubtitlesStream(subtitles, options)
switch (format) {
case 'json': {
// Stream a valid JSON array: [item0,item1,...]
const body$ = processed$.pipe(
Stream.map((item) => JSON.stringify(item)),
Stream.mapAccum(false as boolean, (seen, s) => [true, (seen ? ',' : '') + s]),
)
return Stream.concat(
Stream.succeed('['),
Stream.concat(body$, Stream.succeed(']')),
)
}
case 'srt': {
return processed$.pipe(
Stream.mapAccum(1, (index, item) => [
index + 1,
`${index}\n${formatTimeSrt(item.start)} --> ${formatTimeSrt(item.end)}\n${item.text}\n\n`,
]),
)
}
case 'vtt': {
const body$ = processed$.pipe(
Stream.map(
(item) =>
`${formatTimeVtt(item.start)} --> ${formatTimeVtt(item.end)}\n${item.text}\n\n`,
),
)
return Stream.concat(Stream.succeed('WEBVTT\n\n'), body$)
}
case 'plain-text': {
return processed$.pipe(
Stream.map((item) => `${item.text}\n\n`),
)
}
default: {
return Stream.fail(
new ConversionError({
format: String(format),
cause: new UnsupportedFormatError({
format,
supportedFormats: ['json', 'srt', 'vtt', 'plain-text'],
}),
}),
)
}
}
}
🤖 Prompt for AI Agents
In src/domain/media/subtitle-formats/subtitle-converter.ts around lines 444 to
474, the current implementation collects the entire subtitle stream with
Stream.runCollect and returns a single Effect, preventing incremental output;
replace that collection with a streaming pipeline that yields format-specific
strings as items arrive. Concretely: remove Stream.runCollect and the downstream
Effect/E.map/E.flatMap usage and instead pipe processSubtitlesStream(subtitles,
options) into Stream.map or Stream.flatMap so each incoming chunk (use
Array.from(chunk) if conversion helpers expect arrays) is converted immediately
with convertToJson/convertToSrt/convertToVtt/convertToPlainText and emitted as
Stream<string>; for the unsupported-format branch return a failing Stream (e.g.,
Stream.fail with the ConversionError) so the function's return type becomes
Stream<string> producing incremental output.

Comment on lines +480 to +487
export const runSubtitleConversionStream = (
subtitles: SubtitleJson,
format: SubtitleFormat,
options?: ConversionOptions,
) =>
convertSubtitleFormatStream(subtitles, format, options).pipe(
E.catchAll((err) => E.succeed({ error: err })),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

fix: run the stream in the endpoint; fold to string while preserving streaming API

After returning a Stream<string> from convertSubtitleFormatStream, this endpoint must execute it. Fold chunks into the final string (or keep as-is and expose the stream upstream).

 export const runSubtitleConversionStream = (
   subtitles: SubtitleJson,
   format: SubtitleFormat,
   options?: ConversionOptions,
 ) =>
-  convertSubtitleFormatStream(subtitles, format, options).pipe(
-    E.catchAll((err) => E.succeed({ error: err })),
-  )
+  convertSubtitleFormatStream(subtitles, format, options).pipe(
+    Stream.runFold('', (acc, chunk) => acc + chunk),
+    E.catchAll((err) => E.succeed({ error: err })),
+  )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const runSubtitleConversionStream = (
subtitles: SubtitleJson,
format: SubtitleFormat,
options?: ConversionOptions,
) =>
convertSubtitleFormatStream(subtitles, format, options).pipe(
E.catchAll((err) => E.succeed({ error: err })),
)
export const runSubtitleConversionStream = (
subtitles: SubtitleJson,
format: SubtitleFormat,
options?: ConversionOptions,
) =>
convertSubtitleFormatStream(subtitles, format, options).pipe(
Stream.runFold('', (acc, chunk) => acc + chunk),
E.catchAll((err) => E.succeed({ error: err })),
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Subtitle Conversion Function Implementation

4 participants