From 9bf64bc0d7e110be5e4d4683c78c175f6b342760 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 26 Oct 2026 11:33:27 -0400 Subject: [PATCH 1/2] Add edge-case guidance to AGENTS.md Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 6e34f197c..64031abee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,6 +130,16 @@ Key invariants: `Segment.baselineText.slice(charStart, charEnd) === Token.surfac - `@main` → `src/main` - `parsers/*` → `src/parsers/*` +## Edge cases + +Weigh an edge case's cost before handling it. Each one handled is paid for across code, tests, and documentation, in maintenance and in the context every agent loads to work here. Handle it fully only when that cost buys something: + +- **Danger**: what actually goes wrong if it's unhandled? Lost or corrupted data and silently wrong output justify the cost; an awkward result in a state no real workflow reaches does not. +- **Use**: does a real user or caller produce it, or only a contrived input? +- **Blocking**: does the task at hand need it handled to work, or can it wait for a follow-up issue? + +When it isn't worth handling, throw a clear error naming the unsupported case. A throw costs a line, marks the boundary, and turns a silently wrong result into a visible one that can be addressed once it matters. + ## 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. From c5c93d1a878ebaf45617696c57e834833228a149 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 26 Oct 2026 11:37:16 -0400 Subject: [PATCH 2/2] Correct stale facts and hard-to-parse sentences in AGENTS.md Fix claims that drifted from the code (activate/deactivate, modal paths and states, storage queues, InterlinearProject fields, mock files), drop the mock-setup bullets duplicated from the Testing intro, and rewrite three passages that misled on first read. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 64031abee..a6e944d36 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,7 @@ npm run perf:shape # Compare analysis shape across tiers and stored projec npm run perf:indexes # Check derived indexes and lookups against the records, after random edits ``` -Only a template merge moves the dependency baseline `npm run lint:dependencies` checks against; refresh it in that same commit with `npm run template:baseline`, which writes both the copy and the `MERGED_TEMPLATE_COMMIT` id recorded beside it in [scripts/check-dependency-scope.cjs](scripts/check-dependency-scope.cjs). [README.md](README.md) has the full procedure. +Only a template merge moves the dependency baseline that `npm run lint:dependencies` checks against. Refresh it in that same commit with `npm run template:baseline`, which writes both the baseline (a copy of the template's `package.json`) and the `MERGED_TEMPLATE_COMMIT` id in [scripts/check-dependency-scope.cjs](scripts/check-dependency-scope.cjs). [README.md](README.md) has the full procedure. ## Architecture @@ -44,8 +44,8 @@ This is a **Platform.Bible extension** for interlinear Bible text alignment. Pla [src/main.ts](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.onUndoFromNotification` and `interlinearizer.onAnalysisChanged` network event emitters, the `interlinearizer.continuousScroll` project settings validator, and the `onDidOpenWebView` / `onDidUpdateWebView` / `onDidCloseWebView` subscriptions. All registrations are added to `context.registrations` so the platform disposes them on deactivation. -- `deactivate()` — clears `openWebViewsByProject`, lets go of every book `bookService` keeps, and returns `true`. +- `activate(context)` — stores the `ExecutionToken`, registers the `interlinearizer.mainWebView` WebView provider, command handlers, network event emitters, project settings validators, and the `onDidOpenWebView` / `onDidUpdateWebView` / `onDidCloseWebView` subscriptions. All registrations are added to `context.registrations` so the platform disposes them on deactivation. +- `deactivate()` — clears `openWebViewsByProject`, closes every session, lets go of every book `bookService` keeps, and returns `true`. `openWebViewsByProject` (`Map`) tracks one open WebView ID per project to prevent duplicates; reopening an already-open project brings that tab to front via the `existingId` option. @@ -72,11 +72,11 @@ The WebView root component is assigned to `globalThis.webViewComponent` (not exp ### Project modals -[src/components/ProjectModals.tsx](src/components/ProjectModals.tsx) — single mount point for all project-related dialogs, switching between `'select' | 'create' | 'metadata' | 'none'` states. The three modal components ([SelectInterlinearProjectModal](src/components/SelectInterlinearProjectModal.tsx), [CreateProjectModal](src/components/CreateProjectModal.tsx), [ProjectMetadataModal](src/components/ProjectMetadataModal.tsx)) call backend commands to list, create, update, and delete projects. +[src/components/modals/ProjectModals.tsx](src/components/modals/ProjectModals.tsx) — mount point for the project dialogs, switching on its `ModalState`. The dialogs it mounts call backend commands to list, create, update, and delete projects. ### Project storage -[src/services/projectStorage.ts](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`. +[src/services/projectStorage.ts](src/services/projectStorage.ts) — owns all `papi.storage` reads and writes for interlinearizer projects. Serialization queues prevent interleaved read-modify-write races. Tests must call `resetQueuesForTesting()` between tests because module state is not cleared by `resetMocks`. Drafts are loaded and saved through [AnalysisRepository](src/types/analysis-repository.ts), the seam a later storage backend implements. [DraftRepository](src/services/draftRepository.ts) implements it over the draft envelope and its per-book shards, and rewrites only the books that changed. It imports nothing from PAPI and reaches storage only through a `KeyValueStorage`. That lets the perf harness run it in Node, straight over the storage files ([perf/storage.ts](perf/storage.ts)). [Pt9ImportRepository](src/services/pt9ImportRepository.ts) serves a Paratext 9 import through the same seam, read-only. Both answer the seam's lookups from an [AnalysisIndex](src/services/analysisIndex.ts) built from the records on first use. `DraftRepository` keeps it current through `apply`, and `checkIndexes` compares it with one built afresh. @@ -86,7 +86,7 @@ Saved projects are listed through [ProjectSummaries](src/services/projectSummari ### Sessions -[AnalysisSessions](src/services/analysisSessions.ts) keeps the host's sessions, behind the `openSession`, `closeSession` and `setAnalysisLanguage` commands. A session is a handle on one source project's draft, paired with the interlinear project it saves to, or on a Paratext 9 import alone, read-only. A source has at most one session: opening another closes it. Each session runs its commands one at a time, through `run`, or `runEdit` for a command that changes stored analysis, which a read-only session refuses. An edit reaches the repository as a `ChangeSet` of whole-record puts and deletes, applied through the session's [EditHistory](src/services/editHistory.ts) so that undo can revert it; boundary changes ride in the change set's scope record. The WebView doesn't use sessions yet. +[AnalysisSessions](src/services/analysisSessions.ts) keeps the host's sessions, behind the `openSession`, `closeSession` and `setAnalysisLanguage` commands. A session is a handle on either one source project's draft, paired with the interlinear project it saves to, or a read-only Paratext 9 import. A source has at most one session: opening another closes it. Each session runs its commands one at a time through `run`. A command that changes stored analysis goes through `runEdit` instead, which a read-only session refuses. An edit reaches the repository as a `ChangeSet` of whole-record puts and deletes, applied through the session's [EditHistory](src/services/editHistory.ts) so that undo can revert it; boundary changes ride in the change set's scope record. The WebView doesn't use sessions yet. ### Components @@ -94,7 +94,7 @@ Prefer Platform.Bible's `platform-bible-react` components (`Button`, `Input`, `T 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](src/components/modals/ModalShell.tsx) 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. +Modals go through [ModalShell](src/components/modals/ModalShell.tsx) 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. A modal that is mid-submission passes none, so neither route can abandon in-flight work. Withhold it only while dismissing would abandon real work; a read-only load has nothing to abandon, so its modal stays 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 @@ -113,9 +113,9 @@ Data flows from Platform.Bible's USJ (Unified Scripture JSON) format through two 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. +- `InterlinearProject` — persisted envelope: project metadata, source and optional target project ids, and the `analysis: TextAnalysis`. 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. Section headings are segments too (`Segment.heading`), filed under the verse before them as Paratext files them (or under verse 0 when ahead of a chapter's first verse), with no verse starts; they never merge or split with verse text. A heading within a verse's text splits that verse, and the text resuming after it is a segment keyed and flagged like a split's later piece. Text ahead of the first chapter or verse is kept apart as `Book.frontMatter`, tokenized but never segmented or displayed. +- `Book → Segment → Token` — the text hierarchy. Section headings are segments too (`Segment.heading`), with no verse starts. As in Paratext, each heading is filed under the verse before it, or under verse 0 when it precedes a chapter's first verse. Headings never merge or split with verse text. A heading within a verse's text splits that verse, and the text resuming after it is a segment keyed and flagged like a split's later piece. Text ahead of the first chapter or verse is kept apart as `Book.frontMatter`, tokenized but never segmented or displayed. - `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 @@ -165,22 +165,17 @@ This is purely about which branches may be explicitly ignored from coverage. It ### 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.ts`** — Stubs `logger` and the `papi` services WebView code calls. See file for full list. - **`__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__/platform-bible-react-experimental.ts`** and **`__mocks__/platform-bible-utils-experimental.ts`** — Stub the `/experimental` entry points of those packages. - **`__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.