Language Module Migration V1
Intent
Section titled “Intent”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).
Source system
Section titled “Source system”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.
Decisions
Section titled “Decisions”Endgame and custody
Section titled “Endgame and custody”- Absorption. Emergent absorbs the features; transcrobes eventually retires. The transcrobes pgxsinkit-modernisation work is convergence for this migration, not life support.
- 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.
Personal offering (core enabler)
Section titled “Personal offering (core enabler)”- 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-0precedent) anchoring all personal Offerings, soorganization_idstays NOT NULL everywhere; one persistent personal Offering auto-provisioned per person; manage authority via the existing offering-scoped teacher grant (person_role_grantorg-XOR-offering shape — no new authority mechanism). Remaining work items: carve the sentinel org out of thecatalog_resourceteacher 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. - Personal content rides the standard rails. Personal imports are
catalog_resourcerows (org = sentinel) attached viaoffering_resourceto the personal Offering, delivered through the ADR-0026 document-package machinery — one pipeline for both channels.
Learner model
Section titled “Learner model”- 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. - 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
lrsschema 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.
Feature landings
Section titled “Feature landings”- 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_eventon the rail — notitem_session. The line: deliberate assessment acts (quizzes, generated mini-quizzes) getpractice_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 Policyretentionobjective names the provenance, the scheduler is module code. - 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 thedocument_progressrail. - 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.
- 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_snapshotcomposed with existing widgets (streak,level-badge). - 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
EnrichmentManagerseam (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. - Browser extension (brocrobes). Parked to the backlog (backlog 0009); rebuilt last against frozen module contracts as a distinct thin-client profile.
- 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.
Presentation
Section titled “Presentation”- 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).
- 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 plan
Section titled “Phase plan”- 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,
retentionseam, 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).
Standing constraints
Section titled “Standing constraints”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).