Skip to content

PoC: client-side USFM document model + live checks via @sillsdev/lynx - #327

Open
henrique221 wants to merge 12 commits into
mainfrom
poc/lynx-client-usfm
Open

PoC: client-side USFM document model + live checks via @sillsdev/lynx#327
henrique221 wants to merge 12 commits into
mainfrom
poc/lynx-client-usfm

Conversation

@henrique221

@henrique221 henrique221 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Update: this branch now also contains the Rich Text Editor PoC. PR #376 was stacked on top of this branch (the editor PoC reuses the Lynx workspace from here), and after it was reviewed, approved and merged, its commits landed here. So the diff of this PR now carries both PoCs: the Lynx checking work described below, plus the /rte-poc page comparing SharedEditor and ProseMirror. The editor findings, measurements and comparison live in #376 — this description covers the Lynx part.

Summary

Proof of concept for "USFM, but on the client": an isolated authenticated route (/lynx-usfm) where fluent-web parses USFM into a typed ScriptureDocument in the browser using sillsdev/lynx (@sillsdev/lynx 0.3.5, -usfm 0.3.1, -punctuation-checker 0.2.0), runs five checkers live on every edit, and offers quick fixes, on-type smart quotes, and ignore/undo — no server round-trips.

Today Fluent only touches USFM server-side (usfm-grammar import, string-assembled export). This PoC gives the front end a real scripture document model and demonstrates the standard checking interface discussed in the 2026-02-10 Lynx | Fluent discovery session — the general form of the Checks tab/panel proposed for the Repeated Word Check (#305): DiagnosticProvider ↔ per-check hook, Diagnostic ranges ↔ verse-grouped findings, fixes/dismissal ↔ Ignore Here/Everywhere.

Docs on this branch:

  • docs/proposals/lynx-client-usfm-poc/design.md — architecture + the three browser packaging hazards (all predicted, all confirmed live, all worked around; each is an upstream contribution candidate for sillsdev/lynx + sillsdev/machine)
  • docs/proposals/lynx-client-usfm-poc/lynx-fluent-assessment.md — what Lynx can do for Fluent, ranked integration opportunities, risks, next steps

Not production code: everything is additive and isolated under src/features/lynx/ + one route file; no existing flows touched (only vite.config.ts gains browser aliases for @sillsdev/machine's Node built-ins, test mode unaffected).

Screenshots

1) Overview — sample with seeded issues

The PoC page after auto-loading the sample: USFM editor with inline diagnostic highlights on the left, Checks panel with per-check accordions on the right, stats chips showing counts and the client-side parse+check time (~3 ms).

01-overview-sample

2) Inline diagnostics in the editor

Severity-coded highlights rendered from Lynx Diagnostic ranges: red wavy = error (out-of-order \v 3, unmatched quote), amber = warning (missing-verse anchors, disallowed ©).

02-editor-highlights

3) Checks panel (echoes the #305 proposal)

One accordion per provider — Quotation marks, Allowed characters, Paired punctuation, Punctuation context, Verse order — with verse references resolved from the typed node tree, localized messages, fix buttons, Ignore, and a zero state.

03-checks-panel

4) Quick fix round-trip

After clicking Insert missing verse: the \v 4 marker is spliced into the USFM via a typed ScriptureVerse edit from UsfmEditFactory, and the missing-verse diagnostic resolves (verse-order count drops, warnings 5 → 4).

04-quickfix-inserted-v4

5) Ignore / Show ignored / Undo

App-side dismissal mirroring the Repeated-Word suppression UX: ignored findings are hidden from counts, revealed dimmed with an "Ignored" label and Undo ignore under the "Show ignored" toggle. (Published Lynx 0.3.5 has no dismissal store yet — it's in progress upstream; this maps Fluent's cascade onto it.)

05-ignore-undo

6) On-type smart quotes

A straight " typed after "named Boaz." was autocorrected on type to the context-correct closing curly quote by Lynx's QuotationCorrector (caret preserved).

06-ontype-smart-quote

7) The typed document model

Structure tab, both views rendered purely from the parsed node tree (no regex): a formatted scripture preview (chapter numerals, superscript verse numbers — note the out-of-order 1, 3, 2, 5 straight from the data) and the 25-node Book/Chapter/Paragraph/Verse/Text tree with source ranges.

07-structure-preview

8) Real data: Genesis 1 assembled and checked client-side

"Assemble chapter → USFM" fetches the chapter from the existing bible-texts endpoint and assembles it with a client-side mirror of the server export's generateUSFMText — Gujarati IRV Genesis 1 parsed + checked in ~28 ms. The ~2,600 warnings are the English rule set flagging Gujarati characters: concrete evidence that rule sets must be configured per target language (they're builder-based configs).

08-real-chapter-gujarati

Test plan

  • 14 vitest specs (stylesheet without fs, USFM assembly golden output, workspace wiring: parse/diagnostics/fix round-trip/on-type, verse mapping) — whole-repo suite 52/52 green
  • tsc --noEmit, eslint, and vite build clean (PoC ships as a lazy route chunk, ~75 KB gzip; main bundle untouched)
  • Live smoke against the local stack (screenshots above): sample + real chapter, quick fix, ignore/undo, on-type, structure/preview
  • Confirmed no server calls during checking (only the initial chapter fetch)

Notes for reviewers

  • The three @sillsdev/* browser-packaging workarounds (lib/stylesheet.ts cast, vite.config.ts aliases → lib/node-shims.ts, vendored locale pre-registration in lib/workspace.ts) are each commented with the matching upstream ask.
  • lib/verse-order-provider.ts is vendored from Lynx's unpublished examples package (MIT, provenance header) and adapted to the published 0.3.5 API — it doubles as the template for a future Greek Room provider.

…v/lynx

- /lynx-usfm authenticated route: parse USFM in the browser into a typed
  ScriptureDocument (no server round-trip), live diagnostics from the four
  StandardRuleSets.English checkers + a vendored verse-order provider,
  quick fixes, on-type smart quotes, app-side ignore/undo
- browser-safe UsfmStylesheet built from vendored usfm.sty (?raw import)
- usfm-assembly mirrors fluent-api generateUSFMText so a chapter fetched
  from the existing bible-texts endpoint round-trips through the same
  canonical USFM the export produces
- checks panel deliberately echoes docs/proposals/repeated-word-check
- 14 vitest specs cover stylesheet, assembly, workspace wiring, verse map
Three packaging hazards confirmed in the browser (all worked around locally,
all upstream candidates for sillsdev/machine + sillsdev/lynx):

1. @sillsdev/machine corpora computes dirname(fileURLToPath(import.meta.url))
   at module scope; its browser-field stubs leave fileURLToPath undefined and
   the module throws on import. Fix: alias url/path/fs/fs-promises to inert
   shims (exact-match regex; test mode keeps real Node modules for vitest).
2. Vite 8 (rolldown) treats string alias keys as prefixes: 'fs' captured
   'fs/promises'. Regex aliases avoid it.
3. lynx-punctuation-checker loads locale JSON via template-literal dynamic
   import, unresolvable after dep optimization -> raw i18next keys in the UI.
   Fix: pre-register the five namespaces with vendored en.json resources
   (Localizer.addNamespace is first-write-wins).

Verified live: sample + real Genesis 1 (Gujarati IRV) via the bible-texts
endpoint, quick fix, ignore/undo, on-type smart quote, structure/preview.
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@henrique221, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 57 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f5d10bce-7b99-4522-a6e4-f9092e18491b

📥 Commits

Reviewing files that changed from the base of the PR and between 57c730a and 666354b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (55)
  • docs/proposals/lynx-client-usfm-poc/design.md
  • docs/proposals/lynx-client-usfm-poc/lynx-fluent-assessment.md
  • docs/proposals/rte-poc/design.md
  • package.json
  • src/features/lynx/assets/usfm.sty
  • src/features/lynx/components/ChecksPanel.tsx
  • src/features/lynx/components/LynxUsfmPocPage.tsx
  • src/features/lynx/components/StructurePanel.tsx
  • src/features/lynx/components/UsfmEditor.tsx
  • src/features/lynx/hooks/useLynxDocument.ts
  • src/features/lynx/lib/chapter-source.ts
  • src/features/lynx/lib/locales/allowed-characters.en.json
  • src/features/lynx/lib/locales/paired-punctuation.en.json
  • src/features/lynx/lib/locales/punctuation-context.en.json
  • src/features/lynx/lib/locales/quotation.en.json
  • src/features/lynx/lib/locales/standard-fixes.en.json
  • src/features/lynx/lib/locales/verse-order.en.json
  • src/features/lynx/lib/locales/verse-order.es.json
  • src/features/lynx/lib/node-shims.ts
  • src/features/lynx/lib/sample-usfm.ts
  • src/features/lynx/lib/stylesheet.test.ts
  • src/features/lynx/lib/stylesheet.ts
  • src/features/lynx/lib/text-edits.ts
  • src/features/lynx/lib/usfm-assembly.test.ts
  • src/features/lynx/lib/usfm-assembly.ts
  • src/features/lynx/lib/verse-map.test.ts
  • src/features/lynx/lib/verse-map.ts
  • src/features/lynx/lib/verse-order-provider.ts
  • src/features/lynx/lib/workspace.test.ts
  • src/features/lynx/lib/workspace.ts
  • src/features/rte/components/DerivedVersesPanel.tsx
  • src/features/rte/components/FormatBar.tsx
  • src/features/rte/components/PmEditor.tsx
  • src/features/rte/components/RteEditor.tsx
  • src/features/rte/components/RtePocPage.tsx
  • src/features/rte/hooks/useRtePoc.ts
  • src/features/rte/lib/chapter-api.ts
  • src/features/rte/lib/lynx-annotations.test.ts
  • src/features/rte/lib/lynx-annotations.ts
  • src/features/rte/lib/pericope-slice.test.ts
  • src/features/rte/lib/pericope-slice.ts
  • src/features/rte/lib/pm-doc.test.ts
  • src/features/rte/lib/pm-doc.ts
  • src/features/rte/lib/usfm-to-usj.test.ts
  • src/features/rte/lib/usfm-to-usj.ts
  • src/features/rte/lib/usj-verses.test.ts
  • src/features/rte/lib/usj-verses.ts
  • src/features/rte/styles/annotations.css
  • src/features/rte/styles/editor.css
  • src/features/rte/styles/pm-editor.css
  • src/features/rte/styles/usj-nodes.css
  • src/routeTree.gen.ts
  • src/routes/_authenticated/lynx-usfm.tsx
  • src/routes/_authenticated/rte-poc.tsx
  • vite.config.ts
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch poc/lynx-client-usfm

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.

@henrique221 henrique221 self-assigned this Jul 1, 2026
…— issue #375 (#376)

* docs(rte-poc): design for the SharedEditor PoC (issue #375) + install platform-editor

* feat(rte-poc): TDD lib layer — usfm-to-usj, usj-verses, pericope-slice, lynx-annotations

* feat(rte-poc): /rte-poc page — Editorial editor, pericope slicing, derived-verses save path, Lynx highlights

* fix(rte-poc): browser-verified integration — annotation timing, mark CSS, editor corruption mitigations

Findings from live verification (platform-editor 0.8.14), mitigated here:
- setAnnotation races Lexical's async content commit: re-apply on editor
  commits + bounded DOM verification (marks-exist check, not count — counts
  legitimately shrink when adjacent marks merge).
- TypedMark renders class editor-typed-mark-external-<type>, not <type>.
- Typing while marks are present corrupts text (spaces at mark boundaries):
  marks are stripped on the first editing keystroke, re-applied when idle.
- A mark ending exactly at a text piece's end appends a space to the document
  text: mapper keeps marks one char short of piece ends.
- Verse-level (marker-anchored) diagnostics map to whole-verse spans; charset
  floods (>10/line = wrong-language charset config) are dropped.

Known upstream residue (documented for the #375 comparison): editing commits
materialize trailing spaces on touched/previously-marked verses (harmless to
the save path - usjToVerses trims) and, in complex scripts, boundary spaces
inside untouched verses (real corruption; English is unaffected).

* feat(rte-poc): in-app ProseMirror counterpart behind the same RteEditor interface

Editor toggle on /rte-poc: SharedEditor (Editorial) vs PmEditor share the page,
hook, lib, panels, and FormatBar — only the editor component swaps (design.md
comparison harness). PmEditor: minimal scripture schema (para/book/chapter/verse
atoms), TDD'd usjToDoc/docToUsj round-trip + jsonPath→position mapping
(pm-doc.ts), Lynx annotations as inline decorations applied in one transaction
(ephemeral: no document or undo-history impact).

Browser-verified head-to-head vs Editorial 0.8.14:
- Text integrity while typing with checks live: PM exact +N for N typed chars,
  single verse touched, English AND Gujarati — Editorial gains boundary spaces
  and corrupts untouched complex-script verses on the same test.
- Decorations: single-transaction apply (~0.5 ms for the sample; no clear-and-
  reapply cycles, no undo pollution, no apply/commit race).
- Undo: single-step revert works with decorations present.

* feat(lynx): per-content-language rule sets — no charset checking for unknown languages (#385)

The workspace hardcoded StandardRuleSets.English, so the English
allowed-character set flagged nearly every character of non-Latin text
(~2,600 warnings on one Gujarati chapter), drowning real signal and, in the
RTE PoC, amplifying an editor bug at scale.

createLynxWorkspace now takes { locale, contentLanguage }: a language with a
configured set (today: 'en') gets its full rule set; unknown languages run
the script-agnostic rules only (quotes, paired punctuation, punctuation
context — via createSelectedDiagnosticProviders) plus verse-order. loadSource
carries the content language and rebuilds the workspace transparently when it
changes. Sample loads pass 'en'; API chapters are unknown until a real
project-language lookup exists.

Verified live: Gujarati GEN 2 drops from ~2,100 warnings to 4 real ones;
the English sample keeps charset checking (© still flagged).
dropCharsetFloods stays as a belt in the annotation mapper.
@henrique221
henrique221 requested a review from kaseywright July 30, 2026 19:13
@henrique221
henrique221 marked this pull request as ready for review July 30, 2026 19:26
@henrique221

Copy link
Copy Markdown
Contributor Author

Brought up to date with main (was 24 commits behind) and marked ready for review.

The only conflict was src/routeTree.gen.ts, which is generated — regenerated it rather than hand-resolving, so it now carries both main's routes and the PoC ones. package.json and the lockfile merged cleanly.

Note this branch now also contains the SharedEditor RTE PoC from #376, which was merged into it rather than into main, so this PR covers two routes: /lynx-usfm (client-side USFM + live Lynx checks) and /rte-poc (pericope-scoped rich text editing).

Verified on Node 24.13 after the merge:

  • typecheck, lint clean; 285 tests pass
  • production build succeeds, both lynx-usfm and rte-poc chunks emit
  • logged in locally and loaded /lynx-usfm: sample parses, checks run (2 errors / 5 warnings, ~9 ms), quick fixes and Ignore render as expected

Reviewers: worth deciding up front whether this is meant to merge at all, or stay a reference branch with only the design docs landing. It is ~11k lines of PoC across two routes.

They came in with #376 unformatted, which fails the pre-merge format:check
gate now that this branch is up for review.
@kaseywright

Copy link
Copy Markdown
Contributor

This is great work toward getting the RTE in place. This branch should stay a reference branch with only the design docs landing.

henrique221 added a commit that referenced this pull request Aug 5, 2026
* docs: add Lynx client-USFM and RTE PoC design docs

Lands the three design docs from the PoC work; the code stays on the
reference branches (poc/lynx-client-usfm, PR #327, and the stacked
poc/rte-shared-editor) rather than merging to main.

Headers point at the reference branches so the docs read correctly on
main, where the /lynx-usfm and /rte-poc routes don't exist.

* docs: state the PoC limitations reviewers asked about

Review follow-ups. Verified each claim against the PoC code on
poc/lynx-client-usfm rather than describing intent:

Lynx design doc
- step 4 said getDiagnosticFixes returns TextEdit[]; it returns
  DiagnosticFix[] and the clicked fix's .edits are applied (the
  Published-API note two paragraphs later already said so)
- state the dismissal key (source|code|anchor|occurrence) and that the
  positional fallback does not survive edits
- verse resolution returns undefined outside verses; findings are listed
  per provider, so unscoped ones render without a chip rather than being
  dropped. Corrected the 'grouped by verse' wording accordingly
- qualify the 28 ms number: one browser, no percentiles, main thread, and
  it overruns a 60 Hz frame
- say the assembly test asserts structure, not byte equality with
  generateUSFMText
- new section 8 for what the PoC leaves open (diagnostic lifecycle,
  unscoped diagnostics, multi-edit fixes, canonical USFM, rule sets)

Assessment: mark the dismissal rows Lynx-HEAD-only and say 0.3.5
contracts dismissal app-side.

RTE design doc: known-limitations section (converter drops unknown
markers, pericope fallback cannot tell 'none' from 'not loaded',
save contract is one-POST-per-row with no retry/duplicate guard,
format bar needs a pointer/AT-reachable trigger) + fence language.
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