Skip to content

Feat/spell check batch#650

Merged
UltiRequiem merged 8 commits into
bobadilla-tech:mainfrom
Kinnouts:feat/spell-check-batch
May 25, 2026
Merged

Feat/spell check batch#650
UltiRequiem merged 8 commits into
bobadilla-tech:mainfrom
Kinnouts:feat/spell-check-batch

Conversation

@Kinnouts

@Kinnouts Kinnouts commented May 20, 2026

Copy link
Copy Markdown
Contributor
  • Migrate spell-check engine from sajari/fuzzy to LanguageTool (self-hosted, HTTP)
  • Add POST /v1/text/spellcheck/batch endpoint (up to 50 texts per request)
  • Add suggestions field to corrections (up to 3 ranked candidates per misspelled word)
  • Add languagetool service to docker-compose.dev.yml and LANGUAGETOOL_URL to .env.example
  • Remove sajari/fuzzy from go.mod
  • Full test coverage: 8 service tests + 11 transport tests

Summary by CodeRabbit

  • New Features

    • Added batch spell-check endpoint (1–50 texts per request).
    • Results include up to 3 ranked alternative suggestions.
  • Improvements

    • Switched spell-check backend to LanguageTool for better corrections and Unicode-aware positions.
    • Per-item partial-failure behavior: batch returns one result per input even if some checks fail.
  • Documentation

    • Updated API docs, usage limits, FAQ, and added design/plan doc.
  • Chores

    • Added LanguageTool service to local dev compose and env example; removed prior fuzzy dependency.
  • Tests

    • Tests updated to exercise LanguageTool integration and batch behavior.

Review Change Stack

@Kinnouts
Kinnouts requested a review from a team as a code owner May 20, 2026 13:19
@Kinnouts
Kinnouts requested a review from crydafan May 20, 2026 13:19
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@Kinnouts has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 37 minutes and 4 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 666e0757-6da6-4a89-8dbc-619953c3a111

📥 Commits

Reviewing files that changed from the base of the PR and between 53eb750 and 115fa29.

📒 Files selected for processing (2)
  • apps/api/services/text/spellcheck/service.go
  • docs/apis/text/spell-check.md
📝 Walkthrough

Walkthrough

This PR replaces the embedded fuzzy-model spellcheck engine with an HTTP-backed LanguageTool service. Configuration is added to wire the LanguageTool URL, the core service is rewritten as an HTTP client, data types gain a Suggestions field and batch support, all tests are refactored for mock HTTP servers, a batch endpoint is added, and Docker Compose infrastructure is configured.

Changes

Spellcheck Engine Migration to LanguageTool

Layer / File(s) Summary
Configuration and Route Wiring
apps/api/platform/config/config.go, apps/api/services/text/router.go, apps/api/app/routes_v1.go
Config gains LanguageToolURL field loaded from LANGUAGETOOL_URL (default http://localhost:8010); text service router accepts cfg and passes it to spellcheck service initialization.
Service Implementation and LanguageTool Integration
apps/api/go.mod, apps/api/services/text/spellcheck/service.go
Removed sajari/fuzzy dependency; Service refactored to use http.Client with baseURL; NewService(baseURL string) accepts URL; Check posts to LanguageTool /v2/check and decodes matches; CheckBatch runs up to 10 concurrent checks and preserves input order; buildResult applies replacements and captures up to 3 suggestions.
Types and Batch Transport
apps/api/services/text/spellcheck/type.go, apps/api/services/text/spellcheck/transport_http.go
Correction adds Suggestions []string; added BatchCheckRequest and BatchCheckResponse types; new POST /spellcheck/batch endpoint wired via httpx.HandleBatch returning per-text results.
Service Tests with Mock LanguageTool
apps/api/services/text/spellcheck/service_test.go
Service tests use an in-process mock LanguageTool server (newMockLT), inject mock URL into NewService, and add/adjust assertions for rune-positioning, suggestion truncation, non-200 handling, negative offsets, unreachable LT, and batch behavior.
Transport Tests with Parameterized Mock Setup
apps/api/services/text/spellcheck/transport_http_test.go
Router setup updated to setupRouter(ltURL string) and noopLT; HTTP tests inject mock LT, validate corrected output, batch ordering, X-Usage-Count, and request validation (empty/missing/over-limit).
Documentation and Infrastructure Setup
apps/dashboard/config/api_catalog.yml, apps/dashboard/config/api_docs/spell-check.yml, docs/apis/text/spell-check.md, docs/design-plans/2026-05-19-spellcheck-engine-languagetool.md, infra/docker/.env.example, infra/docker/docker-compose.dev.yml, infra/docker/docker-compose.yml
API docs updated to describe LanguageTool-backed corrections, up to 3 alternative suggestions, Unicode rune positions, and new batch endpoint (1–50 texts); design doc added; .env.example adds LANGUAGETOOL_URL; docker-compose adds languagetool service and healthchecks; API catalog endpoint count updated.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement, go

Suggested reviewers

  • crydafan
  • UltiRequiem

"🐰
I hopped through docs and HTTP calls,
swapped fuzzy lists for LanguageTool halls,
batches served with care and rune-wise spots,
containers ready, tests mock the bots —
this rabbit approves the new spell-checks a lot!"

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "Feat/spell check batch" is partially related to the changeset but is misleading about the primary change. While batch support is added, the main architectural change is migrating the spell-check engine from sajari/fuzzy to LanguageTool. Consider revising the title to better reflect the primary change, such as "Feat: migrate spell-check engine to LanguageTool with batch support" or "Feat: replace fuzzy matching with LanguageTool for spell-check"
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@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: 6

Caution

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

⚠️ Outside diff range comments (1)
docs/apis/text/spell-check.md (1)

68-70: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Remove incorrect ASCII-only documentation; the service supports full Unicode input.

Lines 68-70 claim only ASCII letter sequences are spell-checked, but this contradicts both spell-check.yml line 17 and the actual implementation. The code converts input to runes and works with Unicode offsets; the test suite explicitly validates this with non-ASCII characters like "é". Update lines 68-70 to correctly describe that the service supports Unicode input including accented and non-Latin characters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/apis/text/spell-check.md` around lines 68 - 70, Replace the incorrect
assertion that only ASCII letter sequences are spell-checked with a statement
that the spell-check service supports full Unicode input (including accented and
non-Latin characters); update the copy around the previous lines 68–70 in
spell-check.md to mention that input is converted to runes/unicode codepoints
and Unicode offsets are used for position counting (consistent with
spell-check.yml and the implementation and tests that validate characters like
"é").
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/services/text/spellcheck/service_test.go`:
- Around line 17-20: The mock LanguageTool server in the httptest.NewServer
handler currently accepts any request; update the handler inside the
httptest.NewServer(http.HandlerFunc(...)) to assert r.Method == "POST" and
r.URL.Path == "/v2/check" before returning the ltResponse{Matches: matches}; if
the method or path mismatch, return a non-200 (e.g. 405 for wrong method, 404
for wrong path) so tests fail when code calls the wrong endpoint. Locate the
handler closure in service_test.go where ltResponse and matches are used and add
these conditional checks and appropriate http.Error responses.
- Around line 83-84: The test currently calls
NewService("http://localhost:19999") which hardcodes a port and can be flaky;
replace that with a closed httptest server URL: create an
httptest.NewServer(nil) (or a simple handler), capture ts.URL, call ts.Close()
to guarantee the backend is unreachable, then call
NewService(ts.URL).Check("hello") and assert an error; update the test in
service_test.go to reference NewService and Check accordingly so no fixed
localhost port is used.

In `@apps/api/services/text/spellcheck/service.go`:
- Around line 64-67: Check HTTP status on the LanguageTool response before
attempting to decode: in the block that reads resp into ltResp, validate
resp.StatusCode is 2xx (e.g., >=200 && <300) and return an error that includes
the non-2xx status (and optionally response body) instead of proceeding to
json.Decode; reference the existing resp and ltResponse variables. Also harden
buildResult by validating that each match's Offset and Length are non-negative
and that Offset+Length does not exceed len(runes) before slicing or appending
(check Offset >= 0, Length >= 0, and Offset+Length <= len(runes)), and return a
clear error or skip invalid matches to avoid panics when constructing the
Result.
- Around line 93-95: The loop over matches uses external m.Offset and m.Length
directly for rune slicing and only checks the upper bound; add lower-bound
validation so you skip or clamp any spans where m.Offset < 0 or m.Length < 0
(and keep the existing m.Offset+m.Length <= len(runes)) before any
rune/corrected slicing. Update the validation around the matches iteration (the
block referencing matches, m.Offset, m.Length, and runes) and the later
corrected-slicing code that builds the corrected string (the code that slices
corrected using m.Offset/m.Length and applies m.Replacements) to ensure both
offset and length are non-negative and that m.Offset+m.Length does not exceed
the target slice length.

In `@apps/api/services/text/spellcheck/transport_http_test.go`:
- Around line 234-243: The test uses a hard-coded external address in
setupRouter("http://localhost:19999") which can intermittently hit a real
process; make the failure deterministic by creating an httptest server and using
its URL after closing it (e.g., create an httptest.NewServer(...), call Close()
and pass ts.URL into setupRouter) so the service URL always points to a closed
endpoint; update the test in transport_http_test.go to replace the literal
"http://localhost:19999" with the closed httptest server URL when calling
setupRouter.

In `@docs/apis/text/spell-check.md`:
- Line 136: Replace the incorrect sentence "This endpoint always returns `200`
with a result for every input text. There are no per-item errors —
spell-checking is pure local computation with no external dependencies." with a
corrected description that states the spell-check endpoint now calls an external
LanguageTool HTTP service (running in a separate container) and therefore can
fail if that service is unavailable; keep the note that an unavailable
LanguageTool results in a 500 for the entire request and clarify whether
per-item errors are returned or the request is aborted (choose and document the
actual behavior implemented by the migration to LanguageTool).

---

Outside diff comments:
In `@docs/apis/text/spell-check.md`:
- Around line 68-70: Replace the incorrect assertion that only ASCII letter
sequences are spell-checked with a statement that the spell-check service
supports full Unicode input (including accented and non-Latin characters);
update the copy around the previous lines 68–70 in spell-check.md to mention
that input is converted to runes/unicode codepoints and Unicode offsets are used
for position counting (consistent with spell-check.yml and the implementation
and tests that validate characters like "é").
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0965d225-0a8e-42c2-bae9-9a6764bb485f

📥 Commits

Reviewing files that changed from the base of the PR and between dd17256 and d124963.

📒 Files selected for processing (15)
  • apps/api/app/routes_v1.go
  • apps/api/go.mod
  • apps/api/platform/config/config.go
  • apps/api/services/text/router.go
  • apps/api/services/text/spellcheck/service.go
  • apps/api/services/text/spellcheck/service_test.go
  • apps/api/services/text/spellcheck/transport_http.go
  • apps/api/services/text/spellcheck/transport_http_test.go
  • apps/api/services/text/spellcheck/type.go
  • apps/dashboard/config/api_catalog.yml
  • apps/dashboard/config/api_docs/spell-check.yml
  • docs/apis/text/spell-check.md
  • docs/design-plans/2026-05-19-spellcheck-engine-languagetool.md
  • infra/docker/.env.example
  • infra/docker/docker-compose.dev.yml
💤 Files with no reviewable changes (1)
  • apps/api/go.mod

Comment thread apps/api/services/text/spellcheck/service_test.go
Comment thread apps/api/services/text/spellcheck/service_test.go Outdated
Comment thread apps/api/services/text/spellcheck/service.go
Comment thread apps/api/services/text/spellcheck/service.go
Comment thread apps/api/services/text/spellcheck/transport_http_test.go Outdated
Comment thread docs/apis/text/spell-check.md Outdated
@Kinnouts
Kinnouts force-pushed the feat/spell-check-batch branch from d124963 to 7c73d87 Compare May 20, 2026 13:28
@codecov

codecov Bot commented May 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.66667% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
apps/api/services/text/spellcheck/service.go 90.00% 4 Missing and 4 partials ⚠️
apps/api/services/text/router.go 0.00% 2 Missing ⚠️
apps/api/platform/config/config.go 0.00% 1 Missing ⚠️
apps/api/services/text/spellcheck/type.go 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@UltiRequiem UltiRequiem left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good work ! The migration from sajari/fuzzy to LanguageTool is the right call, and the design document does a solid job laying out the alternatives and trade-offs, including the future ByT5 path, which is useful context to have written down. The type separation into type.go is clean, and rewriting the tests to mock the LT HTTP contract with httptest.Server is the correct approach: they're now testing the actual integration boundary rather than a stub.

A few things to address before merging:

  1. CheckBatch is sequential. It calls Check in a loop, meaning 50 texts = 50 serial HTTP round-trips to LanguageTool. The batch method should fan out concurrently using a semaphore + sync.WaitGroup so callers get an actual throughput benefit. Use in-band errors (failed item returns a zero-value result, batch continues) — do not use errgroup, which would abort the entire batch on the first LT error:
func (s *Service) CheckBatch(texts []string) []Result {
    const maxWorkers = 10

    results := make([]Result, len(texts))
    sem := make(chan struct{}, maxWorkers)
    var wg sync.WaitGroup

    for i, text := range texts {
        wg.Add(1)
        sem <- struct{}{}
        go func(i int, text string) {
            defer wg.Done()
            defer func() { <-sem }()

            r, err := s.Check(text)
            if err != nil {
                results[i] = Result{} // zero-value, batch continues
                return
            }
            results[i] = r
        }(i, text)
    }

    wg.Wait()
    return results
}

The semaphore caps concurrency at 10 so a large batch can't flood the LanguageTool instance. Results are written to pre-allocated index slots — input order is always preserved without sorting. See networking/whois/service.go:LookupBatch for the canonical in-codebase reference.

  1. LanguageTool starts unconditionally in dev. The languagetool service in docker-compose.dev.yml adds ~2 GB RAM overhead for every developer regardless of whether they're touching spell-check. Wrapping it in a Compose profile (e.g. profiles: [languagetool]) would make it opt-in: COMPOSE_PROFILES=languagetool docker compose up for those who need it.

  2. condition: service_started races. LanguageTool takes ~10 seconds to be ready after the process starts (the design doc calls this out). A healthcheck block on the service plus condition: service_healthy on the api dependency would prevent startup failures. Note: the erikvl87/languagetool image is Alpine-based and does not include curl; use wget instead:

    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:8010/v2/languages > /dev/null 2>&1"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s
  3. No production compose change. The PR only touches docker-compose.dev.yml; we need to con figure language tools in the docker-compose.yml so it works in prod too.

None of these are hard blockers and the core of the work is solid. Great contribution.

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/services/text/spellcheck/service.go`:
- Around line 116-117: The span validation in buildResult is vulnerable to
integer overflow when computing m.Offset+m.Length before slicing; replace checks
like "m.Offset+m.Length > len(runes)" with overflow-safe guards: ensure m.Offset
>= 0, m.Offset <= len(runes), m.Length >= 0, and m.Length <= len(runes)-m.Offset
(and similarly use len(corrected) when slicing corrected). Update both
validation sites in buildResult (the checks around runes and corrected slices)
to use these safe comparisons, and also wrap goroutine calls in CheckBatch that
invoke s.Check with a defer-recover to avoid process-wide panics if a slice
still panics. Ensure you reference the symbols m.Offset, m.Length, runes,
corrected, buildResult, CheckBatch and s.Check when making the changes.

In `@docs/apis/text/spell-check.md`:
- Around line 136-148: Update the ambiguous statement "This endpoint always
returns `200`" in the spell-check batch docs so it explicitly distinguishes
validation/early-failure 500 responses from per-item failures: change the
sentence to say the endpoint returns `200` only after successful
validation/processing start (and will return `500` for validation or
pre-processing failures such as LanguageTool being completely unavailable), and
clarify that once processing has started, individual item failures yield
zero-value results (`corrected: ""`, `corrections: null`) while the overall
response is still `200`; ensure the "Batch Error Codes" table remains and add a
short sentence linking each code to these conditions (validation/missing body ->
`400`/`422`, unexpected/pre-processing failure -> `500`, per-item LanguageTool
failures -> zero-value items with `200`).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8a3f6c9a-7e9d-4349-ab67-70f4dd531c61

📥 Commits

Reviewing files that changed from the base of the PR and between d124963 and 53eb750.

📒 Files selected for processing (16)
  • apps/api/app/routes_v1.go
  • apps/api/go.mod
  • apps/api/platform/config/config.go
  • apps/api/services/text/router.go
  • apps/api/services/text/spellcheck/service.go
  • apps/api/services/text/spellcheck/service_test.go
  • apps/api/services/text/spellcheck/transport_http.go
  • apps/api/services/text/spellcheck/transport_http_test.go
  • apps/api/services/text/spellcheck/type.go
  • apps/dashboard/config/api_catalog.yml
  • apps/dashboard/config/api_docs/spell-check.yml
  • docs/apis/text/spell-check.md
  • docs/design-plans/2026-05-19-spellcheck-engine-languagetool.md
  • infra/docker/.env.example
  • infra/docker/docker-compose.dev.yml
  • infra/docker/docker-compose.yml
✅ Files skipped from review due to trivial changes (1)
  • apps/dashboard/config/api_docs/spell-check.yml

Comment thread apps/api/services/text/spellcheck/service.go Outdated
Comment thread docs/apis/text/spell-check.md Outdated
@UltiRequiem
UltiRequiem merged commit 6b9addc into bobadilla-tech:main May 25, 2026
19 checks passed
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.

2 participants