This file provides guidance to AI agents when working with code in this repository.
# Build
npm run build # Build both main and web-view bundles
npm run build:main # Build main extension only
npm run build:web-view # Build React WebView only
npm run watch # Continuous rebuild on changes
# Lint & Format
npm run lint # Run ESLint + stylelint + tsc --noEmit
npm run lint-fix # Auto-fix linting issues
npm run format # Format with Prettier
# Test
npm test # Run full Jest suite
npm run test:coverage # Run with coverage (100% threshold enforced)
npm test -- path/to/file.test.ts # Run a single test file
npm test -- --testNamePattern="pattern" # Run tests matching nameThis is a Platform.Bible extension for interlinear Bible text alignment. Platform.Bible (PAPI) is an Electron-based application; extensions run in a sandboxed context and communicate with the host via papi.* APIs.
src/main.ts — called by Platform.Bible on activation. Exports two lifecycle functions:
activate(context)— stores theExecutionToken, registers theinterlinearizer.mainWebViewWebView provider, command handlers, theinterlinearizer.continuousScrollproject settings validator, and theonDidOpenWebView/onDidCloseWebViewsubscriptions. All registrations are added tocontext.registrationsso the platform disposes them on deactivation.deactivate()— clearsopenWebViewsByProjectand returnstrue.
openWebViewsByProject (Map<string, string>) tracks one open WebView ID per project to prevent duplicates; reopening an already-open project brings that tab to front via the existingId option.
src/interlinearizer.web-view.tsx — entry point rendered inside Platform.Bible's WebView iframe; delegates to InterlinearizerLoader when a projectId is present. useWebViewScrollGroupScrRef and useWebViewState are props injected by the PAPI host (not hook imports).
InterlinearizerLoader — real top of the React tree: owns modal state, persists the active interlinear project, fetches and tokenizes book data, and routes top-menu commands to the appropriate modal.
Interlinearizer — renders the interlinear view from the loaded book data.
The WebView is injected into the main bundle via Webpack's ?inline query:
import interlinearizerReact from './interlinearizer.web-view?inline';
import interlinearizerStyles from './interlinearizer.web-view.scss?inline';src/webpack-env.d.ts declares the *?inline, *?raw, and *.scss module types that make these imports type-safe.
Two separate Webpack configs handle this: webpack.config.web-view.ts builds the React component into temp-build/, then webpack.config.main.ts copies it into dist/ alongside contributions, public assets, and type declarations.
The WebView root component is assigned to globalThis.webViewComponent (not exported) — this is the PAPI WebView contract. Tests must require() the module and read globalThis.webViewComponent to get the component.
src/components/ProjectModals.tsx — single mount point for all project-related dialogs, switching between 'select' | 'create' | 'metadata' | 'none' states. The three modal components (SelectInterlinearProjectModal, CreateProjectModal, ProjectMetadataModal) call backend commands to list, create, update, and delete projects.
src/services/projectStorage.ts — owns all papi.storage reads and writes for interlinearizer projects. Two serialization queues prevent interleaved read-modify-write races. Tests must call resetQueuesForTesting() between tests because module state is not cleared by resetMocks.
All UI uses Tailwind CSS (via src/tailwind.css). Every Tailwind class is prefixed tw: to avoid collisions with Platform.Bible's own styles (configured in tailwind.config.ts). For modifier variants the prefix comes first: tw:hover:px-3, not hover:tw-px-3.
Data flows from Platform.Bible's USJ (Unified Scripture JSON) format through two stages:
- src/parsers/papi/usjBookExtractor.ts — converts USJ to the internal
RawBooktype - src/parsers/papi/bookTokenizer.ts — segments and tokenizes the book into
Segment/Tokenstructures with character offsets
src/parsers/pt9/interlinearXmlParser.ts — separately parses Paratext 9 interlinear XML into the alignment model. The XML schema is documented in pt9-xml.md.
Data model (src/types/interlinearizer.d.ts)
The core types are:
InterlinearProject— persisted envelope: id, createdAt, optional name/description,sourceProjectId, optionaltargetProjectId,analysisLanguages,analysis: TextAnalysis, and optionallinks. Only this is serialized to storage; theBookhierarchy is rebuilt from USJ on each load.ActiveProject— runtime pairing ofproject: InterlinearProjectwith reconstructedsourceand optionaltargetbooks.Book → Segment → Token— the text hierarchyTextAnalysis— flat analysis layer keyed by id (does not mirror text hierarchy)TokenAnalysis / MorphemeAnalysis— parse and 1:1 glosses; multiple analyses per token are allowed, distinguished bystatusAlignmentLink— directional links between source and target endpointsAlignmentEndpoint— has either token-level or morpheme-level specificity
Key invariants: Segment.baselineText.slice(charStart, charEnd) === Token.surfaceText; at most one linked analysis per token/segment may have status: 'approved'. MultiString values are keyed by BCP 47 tags. TokenSnapshot.surfaceText detects drift when baseline text changes.
@main→src/mainparsers/*→src/parsers/*
Jest with ts-jest, jsdom environment. PAPI is fully mocked in __mocks__/. Coverage is enforced at 100% on all src/** files (branches, functions, lines, statements), aside for select explicit exclusions.
resetMocks: true is set globally — mock implementations are cleared before every test, so each test must set up its own mocks (typically in beforeEach). Never rely on implementation state leaking from a prior test.
@papi/backend and @papi/frontend mocks are mutually exclusive: backend tests use papi-backend.ts, WebView tests use papi-frontend.ts + papi-frontend-react.ts. Each mock file ends with export {} so TypeScript treats it as a module.
Key semantic properties of the mock setup:
resetMocks: true— Mock implementations are cleared before every test. Each test must set up its own mocks (typically inbeforeEach); never rely on state leaking from a prior test.- Backend vs. frontend exclusivity — Backend tests use
papi-backend.ts, WebView tests usepapi-frontend.ts+papi-frontend-react.ts. Each mock file ends withexport {}to be treated as a module. globalThis.webViewComponentcontract — The WebView root component is assigned to the global (not exported). Tests mustrequire()the module and readglobalThis.webViewComponentto get the component.
Mock files:
__mocks__/fileMock.ts— Stub static asset imports.__mocks__/lucide-react.tsx— Stubs icon components used in modals.__mocks__/papi-backend.ts— Mocks with jest fns. Re-exports internal jest fns on the default export as__mock*properties (e.g.,papi.__mockRegisterCommand) so tests can assert on them without re-importing. See file for full list.__mocks__/papi-core.ts— Empty module; exists only for module resolution since@papi/coreis types-only at runtime.__mocks__/papi-frontend.ts— Stubslogger(debug/error/info/warn as jest fns) andpapi.commands.sendCommand/papi.notifications.send.__mocks__/papi-frontend-react.ts— Stubs PAPI React hooks.__mocks__/platform-bible-react.tsx— Stubs components with appropriatedata-testidattributes. See file for test IDs.__mocks__/platform-bible-utils.ts— Stubs util functions.__mocks__/styleInlineMock.tsand__mocks__/styleMock.ts— Stub.scss?inlineand.scss.__mocks__/web-view-inline.ts— Stubs*.web-view?inlineimports as a null-returning React component.src/__tests__/test-helpers.ts— ExportscreateTestActivationContext()for testingactivate()without type assertions.
The ESLint rule no-type-assertion/no-type-assertion is enforced. Never use as casts in tests. Workarounds:
- Inject typed WebView state via function overloads in
useWebViewStatestubs (seemakePropsininterlinearizer.web-view.test.tsx). - Narrow mock call args with
typeof x === 'string'instead ofas string.
Every function and method — exported or internal — must have a JSDoc block with:
- A summary sentence describing what the function does and why it exists (non-obvious behavior only; don't restate the name).
@paramfor every parameter.@returnsdescribing the return value (omit only forvoid/Promise<void>).@throwsfor every error condition the caller must handle; omit if the function never throws.
Type declarations (interfaces, type aliases, enums) must have a JSDoc summary on the type itself and on each field or member whose purpose is not self-evident from its name and type.