Skip to content

Latest commit

 

History

History
178 lines (111 loc) · 14.4 KB

File metadata and controls

178 lines (111 loc) · 14.4 KB

AGENTS.md

This file provides guidance to AI agents creating and editing code in this repository.

Agents reviewing code should also read REVIEW.md, which documents existing conventions that commonly trigger false-positive findings.

Commands

# 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 name

Architecture

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.

Extension entry point

src/main.ts — called by Platform.Bible on activation. Exports two lifecycle functions:

  • activate(context) — stores the ExecutionToken, registers the interlinearizer.mainWebView WebView provider, command handlers, the interlinearizer.continuousScroll project settings validator, and the onDidOpenWebView / onDidCloseWebView subscriptions. All registrations are added to context.registrations so the platform disposes them on deactivation.
  • deactivate() — clears openWebViewsByProject and returns true.

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.

WebView UI

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.

Project modals

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.

Project storage

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.

Components

Prefer Platform.Bible's platform-bible-react components (Button, Input, Textarea, Label, Switch, RadioGroup, Popover, Tooltip, etc.) over hand-rolled raw HTML (<button>, <input>, <textarea>, <label>, custom dropdowns) wherever an equivalent exists — this keeps the UI visually and behaviorally consistent with the host app and gives us theming and accessibility for free. Reserve raw elements for cases where the platform component can't preserve behavior the extension depends on (e.g. field-sizing: content gloss inputs); when you do, add a comment explaining why. When a raw element is replaced by a platform component in tests, extend __mocks__/platform-bible-react.tsx to stub the newly-used component.

Size icons inside a Button with size-* (e.g. tw:size-3), never h-*/w-*buttonVariants forces any child SVG lacking a size- class to size-4, silently overriding h-*/w-*.

Modals go through ModalShell rather than building their own overlay — it supplies the platform Dialog, which brings a focus trap, scroll lock, focus restore, and Escape-to-dismiss. Whether a modal passes onClose is the single switch governing every dismissal route: supplying it enables both Escape and outside-click, and a modal that is mid-submission passes none, so neither route can abandon in-flight work. Suppress it only for work that is genuinely being abandoned — a read-only load has nothing to abandon and leaves the modal dismissable. Modals tag their title with a data-testid because end-to-end tests locate them that way; never give the title an id instead, since that displaces the one the platform Dialog generates and the dialog then logs that its title is missing. The platform Dialog and Popover both render role="dialog" — select a modal by [data-slot="dialog-content"] when the two must be told apart.

Styling

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.

Parser pipeline

Data flows from Platform.Bible's USJ (Unified Scripture JSON) format through two stages:

  1. src/parsers/papi/usjBookExtractor.ts — converts USJ to the internal RawBook type
  2. src/parsers/papi/bookTokenizer.ts — segments and tokenizes the book into Segment/Token structures 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.

The core types are:

  • InterlinearProject — persisted envelope: id, createdAt, modelVersion, optional name/description, sourceProjectId, optional targetProjectId, analysisLanguages, analysis: TextAnalysis, and optional links. Only this is serialized to storage; the Book hierarchy is rebuilt from USJ on each load.
  • ActiveProject — runtime pairing of project: InterlinearProject with reconstructed source and optional target books.
  • Book → Segment → Token — the text hierarchy
  • TextAnalysis — 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 by status
  • AlignmentLink — directional links between source and target endpoints
  • AlignmentEndpoint — has either token-level or morpheme-level specificity

All character offsets (charStart, charEnd, charIndex) are zero-based UTF-16 code-unit indices — plain JavaScript string indices, not code points and not grapheme clusters — with end offsets exclusive.

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. Every project and draft record carries a modelVersion; every write stamps CURRENT_MODEL_VERSION, and a record stamped higher than that is refused (assertSupportedModelVersion) rather than read or overwritten.

TypeScript path aliases

  • @mainsrc/main
  • parsers/*src/parsers/*

Testing

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.

Testing hooks

Test context hooks and dispatchers directly with renderHook + a wrapper, not a throwaway button-and-click component. See renderStoreHook in AnalysisStore.test.tsx for the provider-wrapping pattern: return the hooks under test from the callback, drive dispatchers inside act(...), and assert on state read off result.current. Reserve rendered components for behavior that genuinely needs the DOM — re-render / render-count assertions and mount/unmount lifecycle.

Branches not worth testing

100% coverage is enforced, but coverage is a floor, not a goal — chasing it produces low-value tests that bloat the suite and slow it down. Do not write a dedicated test for a branch whose only purpose is to satisfy the coverage gate when the branch carries no real behavior. Such branches should instead be excluded from coverage (via /* v8 ignore next */ with a one-line reason, or the explicit exclusions in the Jest config) rather than tested. Branches not worth a dedicated test:

  • Defensive/unreachable guards — null/undefined checks for values that the type system or call sites already guarantee, default cases on exhaustive switches, and "this should never happen" throws.
  • Trivial pass-throughs — branches that only forward arguments, return a constant, or pick between two equivalent literals with no logic.
  • Framework/wiring glue — branches that exist only to satisfy a library's API shape (e.g., optional-prop fallbacks that mirror the library default).
  • Logging-only branches — code paths that differ only in what they log.

This is purely about which branches may be explicitly ignored from coverage. It is not license to consolidate tests: where a branch does carry real behavior, still prefer a separate, dedicated test for each side of the condition.

Mock internals

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 in beforeEach); never rely on state leaking from a prior test.
  • 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.
  • 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.

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/core is types-only at runtime.
  • __mocks__/papi-frontend.ts — Stubs logger (debug/error/info/warn as jest fns) and papi.commands.sendCommand / papi.notifications.send.
  • __mocks__/papi-frontend-react.ts — Stubs PAPI React hooks.
  • __mocks__/platform-bible-react.tsx — Stubs components with appropriate data-testid attributes. See file for test IDs.
  • __mocks__/platform-bible-utils.ts — Stubs util functions.
  • __mocks__/styleInlineMock.ts and __mocks__/styleMock.ts — Stub .scss?inline and .scss.
  • __mocks__/web-view-inline.ts — Stubs *.web-view?inline imports as a null-returning React component.
  • src/__tests__/test-helpers.ts and src/__tests__/components/test-helpers.tsx — Shared fixture builders and component-test scaffolding. Read these before hand-rolling a token, segment, book, or project literal.

No 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 useWebViewState stubs (see makeProps in interlinearizer.web-view.test.tsx).
  • Narrow mock call args with typeof x === 'string' instead of as string.

Documentation

All comment and JSDoc rules for TypeScript live in .claude/skills/comment-rules/SKILL.md. All agents must follow it when writing or editing comments.

Spelling

Use American English throughout — in code, comments, JSDoc, and documentation:

  • center not centre, color not colour, behavior not behaviour
  • canceled/canceling not cancelled/cancelling, leveled/leveling not levelled/levelling
  • neighboring not neighbouring, favor not favour, signaled not signalled

UX decisions

When key UX decisions are being made, discuss with a developer whether something should be added to user-questions.md for review with people outside the development team.