|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +This file provides guidance to AI agents when working with code in this repository. |
| 4 | + |
| 5 | +## Commands |
| 6 | + |
| 7 | +```bash |
| 8 | +# Build |
| 9 | +npm run build # Build both main and web-view bundles |
| 10 | +npm run build:main # Build main extension only |
| 11 | +npm run build:web-view # Build React WebView only |
| 12 | +npm run watch # Continuous rebuild on changes |
| 13 | + |
| 14 | +# Lint & Format |
| 15 | +npm run lint # Run ESLint + stylelint + tsc --noEmit |
| 16 | +npm run lint-fix # Auto-fix linting issues |
| 17 | +npm run format # Format with Prettier |
| 18 | + |
| 19 | +# Test |
| 20 | +npm test # Run full Jest suite |
| 21 | +npm run test:coverage # Run with coverage (100% threshold enforced) |
| 22 | +npm test -- path/to/file.test.ts # Run a single test file |
| 23 | +npm test -- --testNamePattern="pattern" # Run tests matching name |
| 24 | +``` |
| 25 | + |
| 26 | +## Architecture |
| 27 | + |
| 28 | +This 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. |
| 29 | + |
| 30 | +### Extension entry point |
| 31 | + |
| 32 | +`src/main.ts` — called by Platform.Bible on activation. Exports two lifecycle functions: |
| 33 | + |
| 34 | +- `activate(context)` — registers the `interlinearizer.mainWebView` WebView provider, the `interlinearizer.openForWebView` command, and `onDidOpenWebView` / `onDidCloseWebView` subscriptions. All registrations are added to `context.registrations` so the platform disposes them on deactivation. |
| 35 | +- `deactivate()` — clears `openWebViewsByProject` and returns `true`. |
| 36 | + |
| 37 | +`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. |
| 38 | + |
| 39 | +### WebView UI |
| 40 | + |
| 41 | +`src/interlinearizer.web-view.tsx` — React component rendered inside Platform.Bible's WebView iframe. `useWebViewScrollGroupScrRef` is a **prop injected by the PAPI host** (not a hook import). Uses PAPI frontend hooks (`useProjectData`, `useProjectSetting`, `useLocalizedStrings`, `useRecentScriptureRefs`) to fetch live data. Renders verse segments as token chips with Tailwind utility classes (all prefixed `tw-`). |
| 42 | + |
| 43 | +The WebView is injected into the main bundle via Webpack's `?inline` query: |
| 44 | + |
| 45 | +```ts |
| 46 | +import interlinearizerReact from './interlinearizer.web-view?inline'; |
| 47 | +import interlinearizerStyles from './interlinearizer.web-view.scss?inline'; |
| 48 | +``` |
| 49 | + |
| 50 | +`src/webpack-env.d.ts` declares the `*?inline`, `*?raw`, and `*.scss` module types that make these imports type-safe. |
| 51 | + |
| 52 | +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. |
| 53 | + |
| 54 | +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. |
| 55 | + |
| 56 | +### Styling |
| 57 | + |
| 58 | +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`). |
| 59 | + |
| 60 | +### Parser pipeline |
| 61 | + |
| 62 | +Data flows from Platform.Bible's USJ (Unified Scripture JSON) format through two stages: |
| 63 | + |
| 64 | +1. `src/parsers/papi/usjBookExtractor.ts` — converts USJ to the internal `Book` type |
| 65 | +2. `src/parsers/papi/bookTokenizer.ts` — segments and tokenizes the book into `Segment`/`Token` structures with character offsets |
| 66 | + |
| 67 | +`src/parsers/pt9/interlinearXmlParser.ts` — separately parses Paratext 9 interlinear XML into the alignment model. The XML schema is documented in `src/parsers/pt9/pt9-xml.md`. |
| 68 | + |
| 69 | +### Data model (`src/types/interlinearizer.d.ts`) |
| 70 | + |
| 71 | +The core types are: |
| 72 | + |
| 73 | +- `InterlinearAlignment` — top-level bilingual container (source + target `InterlinearText`) |
| 74 | +- `Book → Segment → Token` — the text hierarchy |
| 75 | +- `TextAnalysis` — flat analysis layer keyed by id (does **not** mirror text hierarchy) |
| 76 | +- `TokenAnalysis / Morpheme` — parse and 1:1 glosses; multiple analyses per token are allowed, distinguished by `status` |
| 77 | +- `AlignmentLink` — links between source and target tokens/morphemes |
| 78 | +- `AlignmentEndpoint` — has either token-level or morpheme-level specificity, never both |
| 79 | + |
| 80 | +Key invariants: `Segment.baselineText.slice(charStart, charEnd) === Token.surfaceText`; at most one `TokenAnalysis` per token may have `status: 'approved'`. Multi-string content is tagged by BCP47 writing-system codes. `tokenSnapshot` fields detect drift when baseline text changes. |
| 81 | + |
| 82 | +### TypeScript path aliases |
| 83 | + |
| 84 | +- `@main` → `src/main` |
| 85 | +- `parsers/*` → `src/parsers/*` |
| 86 | + |
| 87 | +## Testing |
| 88 | + |
| 89 | +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). |
| 90 | + |
| 91 | +`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. |
| 92 | + |
| 93 | +`@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. |
| 94 | + |
| 95 | +### Mock internals |
| 96 | + |
| 97 | +Key semantic properties of the mock setup: |
| 98 | + |
| 99 | +- **`resetMocks: true`** — Mock implementations are cleared before every test. Each test must set up its own mocks (typically in `beforeEach`); never rely on state leaking from a prior test. |
| 100 | +- **Backend vs. frontend exclusivity** — Backend tests use `papi-backend.ts`, WebView tests use `papi-frontend.ts` + `papi-frontend-react.ts`. Each mock file ends with `export {}` to be treated as a module. |
| 101 | +- **`globalThis.webViewComponent` contract** — The WebView root component is assigned to the global (not exported). Tests must `require()` the module and read `globalThis.webViewComponent` to get the component. |
| 102 | + |
| 103 | +Mock files: |
| 104 | + |
| 105 | +- **`__mocks__/fileMock.ts`** — Stub static asset imports. |
| 106 | +- **`__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. |
| 107 | +- **`__mocks__/papi-core.ts`** — Empty module; exists only for module resolution since `@papi/core` is types-only at runtime. |
| 108 | +- **`__mocks__/papi-frontend.ts`** — Stubs `logger` (debug/error/info/warn as jest fns). |
| 109 | +- **`__mocks__/papi-frontend-react.ts`** — Stubs PAPI React hooks. |
| 110 | +- **`__mocks__/platform-bible-react.tsx`** — Stubs components with appropriate `data-testid` attributes. See file for test IDs. |
| 111 | +- **`__mocks__/platform-bible-utils.ts`** — Stubs util functions. |
| 112 | +- **`__mocks__/styleInlineMock.ts`** and **`__mocks__/styleMock.ts`** — Stub `.scss?inline` and `.scss`. |
| 113 | +- **`__mocks__/web-view-inline.ts`** — Stubs `*.web-view?inline` imports as a null-returning React component. |
| 114 | +- **`src/__tests__/test-helpers.ts`** — Exports `createTestActivationContext()` for testing `activate()` without type assertions. |
| 115 | + |
| 116 | +### No type assertions |
| 117 | + |
| 118 | +The ESLint rule `no-type-assertion/no-type-assertion` is enforced. **Never use `as` casts in tests.** Workarounds: |
| 119 | + |
| 120 | +- Inject typed WebView state via function overloads in `useWebViewState` stubs (see `makeProps` in `interlinearizer.web-view.test.tsx`). |
| 121 | +- Narrow mock call args with `typeof x === 'string'` instead of `as string`. |
| 122 | + |
| 123 | +## Documentation |
| 124 | + |
| 125 | +Every function and method — exported or internal — must have a JSDoc block with: |
| 126 | + |
| 127 | +- A summary sentence describing what the function does and why it exists (non-obvious behavior only; don't restate the name). |
| 128 | +- `@param` for every parameter. |
| 129 | +- `@returns` describing the return value (omit only for `void`/`Promise<void>`). |
| 130 | +- `@throws` for every error condition the caller must handle; omit if the function never throws. |
| 131 | + |
| 132 | +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. |
0 commit comments