Skip to content

Language Module Migration V1

Record the decisions and sequencing for absorbing the end-user functionality of transcrobes (the predecessor second-language-learning platform; sibling repo stc/transcrobes) into emergent as the first domain specialization. This is the decision record of a full design review held 2026-07-16; the deepest data-model decision is promoted to ADR-0052.

Feasibility verdict: high. The stacks are already convergent (Bun/Turbo, React 19, TanStack Router, PGlite + Electric + pgxsinkit, Drizzle, Supabase), and emergent had pre-reserved the exact seams these features need: the token-grained Language Reader lane (ADR-0026), spaced-repetition and pronunciation Native Extensions (ADR-0002 / CONTEXT.md), the Selection Policy retention objective (ADR-0005), and the language Subject Pack with word-card/flashcard/ dictation already in the widget/activity vocabulary (ADR-0050).

transcrobes is local-first (PGlite in a dedicated worker, pgxsinkit sync) and person-centric. Its learner tools: an enriched EPUB reader (boocrobes), an enriched-subtitle video player (moocrobes), paste-text enrichment (textcrobes), dictionary/token lookup (notrobes), bulk vocabulary self-grading (listrobes), SM-2 spaced repetition (repetrobes), word lists and goals, personal dictionaries, stats, a content import/enrichment pipeline (CoreNLP + Bing, en ↔ zh-Hans), and an MV3 browser extension (brocrobes). Its learner model was always conceptually event-based: an archive of input events with state as running totals.

  1. Absorption. Emergent absorbs the features; transcrobes eventually retires. The transcrobes pgxsinkit-modernisation work is convergence for this migration, not life support.
  2. Dual-channel content, person-scoped knowledge model. Content enters via Offering provisioning or personal import; the learner model is person-scoped and spans all Offerings. Personal imports are strictly person-private (private-hosting posture — the platform stores, the person copies; no share/redistribute path), which is the predecessor’s proven custody model.
  1. Default-organization design. The documented-but-unbuilt “personal offering” (core-model.md → Independent mode) is realised as: one platform-managed sentinel organization (ADR-0034 customer-0 precedent) anchoring all personal Offerings, so organization_id stays NOT NULL everywhere; one persistent personal Offering auto-provisioned per person; manage authority via the existing offering-scoped teacher grant (person_role_grant org-XOR-offering shape — no new authority mechanism). Remaining work items: carve the sentinel org out of the catalog_resource teacher read filter (an offering-scoped grant currently reads the whole org catalog one hop through offering → org, which would leak personal catalog metadata across persons), and add a person-usable import/authoring workflow (resource creation is admin-only today). This decision should get its own ADR when Phase 1 is implemented.
  2. Personal content rides the standard rails. Personal imports are catalog_resource rows (org = sentinel) attached via offering_resource to the personal Offering, delivered through the ADR-0026 document-package machinery — one pipeline for both channels.
  1. Competency-set-centric, per ADR-0052. One referent space (every knowledge item a competency_node, lazily minted, provenance-tiered, mergeable); tiered operational machinery keyed by node ID; evidence summarisation boundary; goals as core constructs over competency sets. Word lists are competency-set definitions; curated sets export as CASE.
  2. Events-canonical with pragmatic lanes. Capture = append-only encounter/review events on the sync rail (offline-safe, client retention window). OLTP = running-total projections (word knowledge, scheduling state) maintained incrementally client-side for instant offline UX and canonically by the single server-side projector. Archive = a plain native-event archive schema (the lrs schema pattern); the rail table is a retention-trimmed buffer. xAPI/Caliper statements are emitted selectively (session summaries, reviews) as projections, never as the archive itself.
  1. Spaced repetition (repetrobes). QTI Native Extension for the interaction contract only: one templated namespaced item per card type (meaning / sound / graph / phrase), Mantine-skin rendered, payload resolved from the node at delivery. The review record is a lean module review_event on the rail — not item_session. The line: deliberate assessment acts (quizzes, generated mini-quizzes) get practice_session/item_session; high-frequency drill acts get events. Measured single-learner scale (~104k cards, ~10⁵ reviews/year) is why drill telemetry must not land in core assessment tables. The SM-2 fold and summarised evidence consume both identically via the projector; the Selection Policy retention objective names the provenance, the scheduler is module code.
  2. Language Reader. Port the boocrobes reader as the ADR-0026 Language Reader: the iframe-sandboxed frame + postMessage bridge + chapter-enrichment runtime + enriched-text renderer port near-intact as the “library owns anchoring + rendering” half; the host side is rebuilt as features/language-reader (Mantine chrome, config UI) with persistence/telemetry on the rail. Two readers coexist by design (foliate-js generic reader for ordinary documents). Planning task: evaluate official EPUB CFI (canonical spec: https://w3c.github.io/epub-specs/epub33/epubcfi/) against the ported positioning scheme — foliate-js anchors with CFI natively, so adopting it would unify locator semantics across both readers and the document_progress rail.
  3. Video. No second player. The module contributes an enriched-VTT track type, a token-interactive subtitle overlay (same enriched-text renderer), and learner fine-controls (sub delay, playback rate, sub switch) as extensions to the existing media-player feature (ADR-0045), with word-encounter events alongside Caliper media events. Verify first that the player exposes/can grow time-synced cue callbacks, pause-on-interaction, and rate control from a plugin component; fallback is a player variant reusing emergent’s media session/telemetry plumbing.
  4. Light tools. Dictionary (notrobes) = module surface over node inventory + gloss cache, sharing one TokenDetails component with reader/video popups; personal dictionaries = a person-scoped read-write-synced module table; gloss resolution order: personal dictionary → curated dictionaries → cache/AI engine. Paste-enrichment (textcrobes) = thin UI over the enrichment endpoint, emitting encounter events. Bulk self-grading (listrobes) = declarations (declaration-strength evidence; the cold-start seeding path). Stats = projections + progress_snapshot composed with existing widgets (streak, level-badge).
  5. Enrichment pipeline. First-party from day one: the enrichment code (portable TS) moves into the monorepo; the import path becomes upload → document-package decompose → language enrichment stage (tokenise, per-token node refs, gloss provisioning) → enriched webpub/VTT artefacts + catalog registration. The EnrichmentManager seam (enricher / parser / lemmatizer / translators / transliterator per language pair) is the module’s provider contract; CoreNLP + Bing ship unchanged as provider v1; engine modernisation is a separate post-migration project behind the stable interface.
  6. Browser extension (brocrobes). Parked to the backlog (backlog 0009); rebuilt last against frozen module contracts as a distinct thin-client profile.
  7. Predecessor data. One-off import job: userword/card → node mapping (exercising lazy minting + merge on real messy data), operational tier written directly, one provenance-marked baseline evidence row per person. The raw transcrobes event archive is dumped into the archive schema for posterity, not replayed.
  1. Conception. Tools you do are activity kinds addable to Offerings (language-reader activity, spaced-repetition practice, seeding, quizzes) — identical in teacher-provisioned and personal Offerings. Things that assist are ambient infrastructure (the dictionary popup inside every activity). Things you see are surface widgets (stats, goals, streaks).
  2. Transcrobes look-alike Surface Experience. A home experience (tools grid, due cards, goals, daily stats) built on the ADR-0050 widget catalog — deliberately the first real workout for the surface-layout engine and a familiar face for migrating users. Guardrail: compose the shell, don’t absorb the activities — reader/player/practice stay viewport-hosted activity kinds, and module features remain reachable as ordinary routes so surface-layout immaturity can never block the migration.
  • Phase 0 — convergence (external, running): transcrobes pgxsinkit modernisation.
  • Phase 1 — core enablers: personal offering (decision 3, own ADR), ADR-0052 mechanics (bulk node authoring, node client projection, minting/merge lifecycle, summarisation convention, goal construct), archive lane (schema + forwarder seam).
  • Phase 2 — module foundation (parallel tracks): (a) module tables, sync-registry entries, the projector; (b) enrichment pipeline port + import-workflow stage.
  • Phase 3 — first learner-visible slice: enriched-text renderer + TokenDetails, dictionary, paste-enrichment, bulk self-grading, list import. Surface-experience work starts here (stats widgets).
  • Phase 4 — Language Reader: full content pipeline (personal + Offering channels) + the ported reader + CFI evaluation.
  • Phase 5 — spaced repetition, goals, stats: review events, SM-2 fold, retention seam, Native-Extension card types, goal construct surfaced, dashboards. Phases 4 and 5 touch disjoint surfaces and may run in parallel; if serial, reader first.
  • Phase 6 — media subtitle layer (after the player extension-point verification).
  • Phase 7 — predecessor data import (after the learner model has survived Phases 3–5).
  • Phase 8 — browser extension (backlog 0009 reopen trigger).

Neutral vocabulary everywhere including UI copy; no React Query in feature data paths (live-rows / sync layer); Mantine 9 for all host UI (ported frame internals are exempt — they render inside the sandboxed iframe); drizzle 1.0.0-rc.4 + pgxsinkit dev-channel pins; consent, erasure, and pseudonymous-telemetry governance registered for every new person-scoped table (event lane, operational tier, archive).