You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Update components, storage, main, and types to match the model introduced in #63: rename commands, fix return types, update JSDoc, and adjust tests throughout.
Copy file name to clipboardExpand all lines: AGENTS.md
+72-21Lines changed: 72 additions & 21 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -29,16 +29,29 @@ This is a **Platform.Bible extension** for interlinear Bible text alignment. Pla
29
29
30
30
### Extension entry point
31
31
32
-
`src/main.ts` — called by Platform.Bible on activation. Exports two lifecycle functions:
32
+
[src/main.ts](src/main.ts) — called by Platform.Bible on activation. Exports two lifecycle functions:
33
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.
34
+
-`activate(context)` — stores the `ExecutionToken`, registers the `interlinearizer.mainWebView` WebView provider, the command handlers below, and the`onDidOpenWebView` / `onDidCloseWebView` subscriptions. All registrations are added to `context.registrations` so the platform disposes them on deactivation.
35
35
-`deactivate()` — clears `openWebViewsByProject` and returns `true`.
36
36
37
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
38
39
+
Registered commands:
40
+
41
+
-`interlinearizer.openForWebView` — opens the Interlinearizer for the WebView's project (or a picker if no ID is given).
42
+
-`interlinearizer.createProject` — backend handler that delegates to [projectStorage.createProject](src/services/projectStorage.ts); returns a JSON-serialized `InterlinearProject` or `undefined` on failure.
43
+
-`interlinearizer.getProjectsForSource` — returns a JSON-stringified `InterlinearProject[]` filtered by source project.
44
+
-`interlinearizer.updateProjectMetadata` — updates name/description/analysisLanguages/targetProjectId for a project.
45
+
-`interlinearizer.deleteProject` — deletes a project; no-ops silently when the ID is unknown.
46
+
-`interlinearizer.openSelectProjectModal`, `interlinearizer.openNewProjectModal`, `interlinearizer.openProjectInfoModal` — registered server-side as no-op handlers so the platform menu system knows about them; the actual behavior lives in the WebView, which listens for the matching menu-item activation.
47
+
39
48
### WebView UI
40
49
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:`).
50
+
[src/interlinearizer.web-view.tsx](src/interlinearizer.web-view.tsx) is the entry point that the PAPI host renders inside its WebView iframe. It just delegates to [InterlinearizerLoader](src/components/InterlinearizerLoader.tsx) when a `projectId` is present. `useWebViewScrollGroupScrRef` and `useWebViewState` are **props injected by the PAPI host** (not hook imports).
51
+
52
+
[InterlinearizerLoader](src/components/InterlinearizerLoader.tsx) is the real top of the React tree: it owns modal state, persists the active interlinear project via `useWebViewState`, fetches and tokenizes book data, renders the `TabToolbar` + `ScriptureNavControls` + `ContinuousScrollToggle`, and routes top-menu commands (`openSelectProjectModal` / `openNewProjectModal` / `openProjectInfoModal`) to the appropriate modal. The "View Project Info" item is filtered out of the menu when no project is active.
53
+
54
+
[Interlinearizer](src/components/Interlinearizer.tsx) renders the actual interlinear view: an optional `ContinuousView` strip above a list of `SegmentView`s for the current chapter.
42
55
43
56
The WebView is injected into the main bundle via Webpack's `?inline` query:
44
57
@@ -47,37 +60,74 @@ import interlinearizerReact from './interlinearizer.web-view?inline';
`src/webpack-env.d.ts` declares the `*?inline`, `*?raw`, and `*.scss` module types that make these imports type-safe.
63
+
[src/webpack-env.d.ts](src/webpack-env.d.ts) declares the `*?inline`, `*?raw`, and `*.scss` module types that make these imports type-safe.
51
64
52
65
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
66
54
67
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
68
69
+
### Project modals
70
+
71
+
[src/components/ProjectModals.tsx](src/components/ProjectModals.tsx) is the single mount point for all project-related dialogs; it switches between `'select' | 'create' | 'metadata' | 'none'` based on a `modal` prop owned by `InterlinearizerLoader`:
72
+
73
+
-[SelectInterlinearProjectModal](src/components/SelectInterlinearProjectModal.tsx) — lists existing projects for the source via `interlinearizer.getProjectsForSource`, with an info icon that opens the metadata modal.
74
+
-[CreateProjectModal](src/components/CreateProjectModal.tsx) — collects name, description, and analysis-language tags, then calls `interlinearizer.createProject`.
75
+
-[ProjectMetadataModal](src/components/ProjectMetadataModal.tsx) — edits / deletes an existing project via `interlinearizer.updateProjectMetadata` and `interlinearizer.deleteProject`.
76
+
77
+
The active project is persisted in WebView state under the `activeProject` key so it survives tab restores. The `isInterlinearProjectSummary` type guard in `SelectInterlinearProjectModal.tsx` validates JSON parsed from backend commands without `as` casts.
78
+
79
+
### Project storage
80
+
81
+
[src/services/projectStorage.ts](src/services/projectStorage.ts) owns all `papi.storage` reads and writes for interlinearizer projects:
82
+
83
+
- Projects are persisted under the `project:{uuid}` key; the ordered list of all UUIDs lives at `projectIds`.
84
+
- Two serialization queues prevent interleaved read-modify-write races: `indexQueue` guards the `projectIds` index and a per-project `projectQueues` map guards each project's record. `createProject` rolls the project write back when the index update fails.
85
+
- ENOENT (`isNotFound`) is treated as "key has never been written" rather than an error — used for both project reads and the initial-empty-index case.
86
+
- Tests must call `resetQueuesForTesting()` between tests because module state is not cleared by `resetMocks`.
87
+
56
88
### Styling
57
89
58
90
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`.
59
91
92
+
### Hooks
93
+
94
+
[src/hooks/useInterlinearizerBookData.ts](src/hooks/useInterlinearizerBookData.ts) — orchestrates the per-project book pipeline. Reads USJ via `useProjectData('platformScripture.USJ_Book', projectId)` and the writing system via `useProjectSetting('platform.languageTag', ...)`, runs them through `extractBookFromUsj` and `tokenizeBook`, and returns `{ book, chapterSegments, isLoading, bookError, tokenizeError }`. The hook only depends on `scrRef.book` (not chapter/verse) for loading; chapter scoping happens during filtering.
95
+
96
+
[src/hooks/useOptimisticBooleanSetting.ts](src/hooks/useOptimisticBooleanSetting.ts) — wraps `useProjectSetting` with optimistic UI: a toggle update is shown immediately, the platform's confirmation is ignored for `TIMEOUT_MS` (15s) so the toggle does not visibly bounce, and if the platform never confirms the optimistic value persists rather than reverting.
97
+
60
98
### Parser pipeline
61
99
62
100
Data flows from Platform.Bible's USJ (Unified Scripture JSON) format through two stages:
63
101
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
102
+
1.[src/parsers/papi/usjBookExtractor.ts](src/parsers/papi/usjBookExtractor.ts) — walks USJ nodes (book / chapter / verse / para / note) into a `RawBook` (`bookCode`, `writingSystem`, `contentHash`, `verses`). Heading-class `para` markers are dropped so their text never bleeds into the verse baseline; `note` content is also skipped. The `contentHash` is an FNV-1a 32-bit hash of a stably-stringified `usj.content`, used as `Book.textVersion` to detect baseline drift.
103
+
2.[src/parsers/papi/bookTokenizer.ts](src/parsers/papi/bookTokenizer.ts) — segments and tokenizes the book into `Segment`/`Token` structures. The tokenizer regex uses Unicode property classes (`\p{L}\p{N}\p{M}\p{Join_Control}`), absorbs apostrophes/right-single-quotes at word edges (for languages where they mark phonemic glottal stops), and treats `'`, `-`, Unicode dashes, and `’` as word-internal joiners only when surrounded by word characters. Whitespace is not tokenized; the invariant `Segment.baselineText.slice(charStart, charEnd) === Token.surfaceText` is preserved.
104
+
105
+
[src/parsers/pt9/interlinearXmlParser.ts](src/parsers/pt9/interlinearXmlParser.ts) — separately parses Paratext 9 interlinear XML via `fast-xml-parser`. Strict mode for clusters (required `Range` with non-negative integer `Index`/`Length`; lexemes require `Id`), lenient for punctuation (entries with missing/invalid `Range` are silently filtered). The XML schema is documented in [pt9-xml.md](src/parsers/pt9/pt9-xml.md). The exported `InterlinearXmlParser` class holds one configured `XMLParser` — reuse a single instance across `parse()` calls.
106
+
107
+
### Data model ([src/types/interlinearizer.d.ts](src/types/interlinearizer.d.ts))
108
+
109
+
The file declares two ambient modules:
66
110
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`.
111
+
-`papi-shared-types` — augments `ProjectSettingTypes` (`interlinearizer.continuousScroll: boolean`) and `CommandHandlers` (the seven interlinearizer commands described above) so Platform.Bible's typed APIs know about them.
112
+
-`interlinearizer` — the project's domain types.
68
113
69
-
### Data model (`src/types/interlinearizer.d.ts`)
114
+
The core domain types are:
70
115
71
-
The core types are:
116
+
-`InterlinearProject` — persisted envelope: id, createdAt, optional name/description, `sourceProjectId`, optional `targetProjectId`, `analysisLanguages`, `analysis: TextAnalysis`, and `links?: AlignmentLink[]`. Only this is serialized to storage; the `Book` hierarchy is rebuilt from USJ on each load.
117
+
-`ActiveProject` — runtime pairing of `project: InterlinearProject` with reconstructed `source: Book[]` and optional `target: Book[]`.
118
+
-`Book → Segment → Token` — the text hierarchy.
119
+
-`TextAnalysis` — flat analysis layer keyed by id (does **not** mirror the text hierarchy). Holds `segmentAnalyses`, `tokenAnalyses`, `phraseAnalyses`, plus a `*Links` array for each that attaches an analysis record to one or more text-layer targets.
120
+
-`TokenAnalysis / MorphemeAnalysis` — parse and 1:1 glosses; multiple analyses per token are allowed, distinguished by `status`.
121
+
-`AlignmentLink` — directional links between source and target endpoints.
122
+
-`AlignmentEndpoint` — has either token-level or morpheme-level specificity via the optional `morphemeLink` field (when present, both `tokenAnalysisId` and `morphemeId` are required).
123
+
-`EntryRef` / `SenseRef` / `AllomorphRef` / `GrammarRef` — references to the Lexicon extension. The file documents current gaps in `IEntryService` that the Lexicon extension is expected to close.
-`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
125
+
Key invariants:
79
126
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.
- At most one linked `TokenAnalysisLink` per token may have `status: 'approved'`; same for `SegmentAnalysisLink` per segment and for `PhraseAnalysisLink` per token covered.
129
+
-`MultiString` values are keyed by BCP 47 tags.
130
+
-`TokenSnapshot.surfaceText` is the drift-detection mechanism: when it no longer matches the current `Token.surfaceText`, consumers flip the link's `status` to `'stale'`.
81
131
82
132
### TypeScript path aliases
83
133
@@ -103,12 +153,13 @@ Key semantic properties of the mock setup:
-**`__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.
156
+
-**`__mocks__/lucide-react.tsx`** — Stubs the icon components used in modals (`Info`, `Trash2`).
157
+
-**`__mocks__/papi-backend.ts`** — Mocks with jest fns including `commands`, `dialogs`, `notifications`, `storage`, `webViewProviders`, and `webViews`. Re-exports internal jest fns on the default export as `__mock*` properties (e.g., `papi.__mockRegisterCommand`, `papi.__mockReadUserData`) so tests can assert on them without re-importing. See file for the full list.
107
158
-**`__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).
-**`__mocks__/platform-bible-react.tsx`** — Stubs components (`TabToolbar`, `BookChapterControl`, `ScrollGroupSelector`, `Button`, `Switch`, `Label`) with appropriate `data-testid` attributes. See file for test IDs.
162
+
-**`__mocks__/platform-bible-utils.ts`** — Stubs util functions including `isPlatformError`.
112
163
-**`__mocks__/styleInlineMock.ts`** and **`__mocks__/styleMock.ts`** — Stub `.scss?inline` and `.scss`.
113
164
-**`__mocks__/web-view-inline.ts`** — Stubs `*.web-view?inline` imports as a null-returning React component.
114
165
-**`src/__tests__/test-helpers.ts`** — Exports `createTestActivationContext()` for testing `activate()` without type assertions.
0 commit comments