Skip to content

Check analyses against the whole source text (#143) - #399

Open
alex-rawlings-yyc wants to merge 6 commits into
mainfrom
feat/143-form-in-text
Open

alex-rawlings-yyc wants to merge 6 commits into
mainfrom
feat/143-form-in-text

Conversation

@alex-rawlings-yyc

@alex-rawlings-yyc alex-rawlings-yyc commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Closes #143.

The Analysis Catalog has a new "Word not in the text" filter. Rows whose form appears nowhere in the source text get a badge. The check covers the whole text: the loaded book as it is now, and other books as they were last read for the concordance. A word gone from the text is safe to discard, while one still in it can be re-applied.

06-catalog-after-read

The text is read the first time the new filter or Stale is turned on. A concordance already built in the tab also counts. Just opening the catalog doesn't read it. While the text is being read, the listing shows how many books are done, using the status the concordance already had.

If any book fails to read, no word is marked as not in the text. The catalog says the text could not be read instead. The concordance still lists the books that did read, with a notice above the list that some could not.

Each read also re-anchors every book the draft has analyses in, as that book arrives. This is the whole-draft re-anchoring #349 left here. Once every book is in, the Stale filter drops its "(found when a book is opened)" qualifier. Replacing the draft (New, Open, Wipe) reads the text again.

03-filters-before-read 04-filters-after-read

Links into a book the project no longer has go stale.

07-stale-filtered 08-missing-book-stale-place

A free translation of a verse the book no longer has, which #349 left unreachable, now appears in the segment ending the nearest earlier verse. If no earlier verse remains, it appears in the book's first segment.

01-deleted-verse-translation

Checked in the running app.


This change is Reviewable

Summary by CodeRabbit

  • New Features
    • Added catalog filters for analyses that are stale across the draft or whose word forms aren’t present in the text. Matching rows are marked, and text-reading progress is shown while results load.
    • Catalog filters now distinguish checks for the current book from checks covering the full draft.
  • Bug Fixes
    • Analyses linked to missing or changed book text are marked stale.
    • Translations for removed verses or headings are placed near the closest available text instead of being omitted.

@alex-rawlings-yyc alex-rawlings-yyc self-assigned this Oct 7, 2026
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: dbf61ba7-1913-40a8-abed-4856d7216f0f
📝 Walkthrough

Walkthrough

The concordance index now supports whole-project text reads and reports reading status and text forms. The loader uses completed reads to re-anchor linked analyses. The catalog adds filters for stale analyses and forms absent from source text. Translations associated with removed text receive fallback positions when the book contains text.

Changes

Project text coverage and analysis catalog

Layer / File(s) Summary
Concordance reading lifecycle
src/hooks/useConcordanceIndex.ts, src/components/TextReadingStatus.tsx, src/components/ConcordancePanel.tsx, src/__tests__/hooks/useConcordanceIndex.test.ts, src/__tests__/components/*
The concordance index supports explicit reading requests, read-key refreshes, book and completion callbacks, partial-read reporting, and text-form data. Concordance reading progress and errors use the shared status component.
Whole-project analysis re-anchoring
src/components/InterlinearizerLoader.tsx, src/utils/analysis-book.ts, src/utils/reanchor-draft.ts, src/__tests__/components/InterlinearizerLoader.test.tsx, src/__tests__/utils/reanchor-draft.test.ts, docs/reanchoring.md
The loader re-anchors linked analyses as books are read and marks linked analyses stale when books are absent after a complete read. It tracks the draft version covered and skips callback updates during loading or import display.
Catalog text filters and row markers
src/utils/analysis-query.ts, src/components/AnalysisCatalogPanel.tsx, src/components/CatalogFilterPopover.tsx, src/components/CatalogQueryControls.tsx, src/components/CatalogRowView.tsx, contributions/localizedStrings.json, __mocks__/lucide-react.tsx, src/__tests__/components/AnalysisCatalogPanel.test.tsx, src/__tests__/utils/analysis-query.test.ts, src/__tests__/*
Catalog rows include normalized forms that can be compared with source text forms. The catalog requests text for stale and not-in-text filters, shows read status while waiting, and marks rows whose forms are absent from the text.

Stale translation placement

Layer / File(s) Summary
Fallback positions for removed text
src/utils/stale-free-translations.ts, src/__tests__/utils/stale-free-translations.test.ts
Translations tied to missing verses or headings are placed at a fallback position when the book has text. They are omitted when the book has no text.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant AnalysisCatalogPanel
  participant useConcordanceIndex
  participant InterlinearizerLoader
  participant Draft re-anchoring
  AnalysisCatalogPanel->>useConcordanceIndex: request text reading
  useConcordanceIndex->>InterlinearizerLoader: report each read book
  InterlinearizerLoader->>Draft re-anchoring: re-anchor linked book
  useConcordanceIndex->>InterlinearizerLoader: report completed book IDs
  InterlinearizerLoader->>Draft re-anchoring: mark linked absent books stale
Loading

Merge Risk: 🔵 Low · up to 70c3a

A rapid draft replacement may receive stale re-anchoring results, and a failed book read may appear complete in the concordance. These bounded risks warrant fixes or explicit acceptance before merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The changes to src/utils/stale-free-translations.ts and its tests place free translations from deleted verses or headings into fallback segments. Free translation placement does not manage analyses … Move the stale free-translation implementation and its tests to a separate pull request, or link an active issue that requires this behavior. Keep this pull request focused on orphaned-analysis detection and management.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #143 requires a way to manage analyses whose surface text is absent from the source text. The PR adds normalized CatalogRow.form values, a notInText catalog filter, text-reading activation, …
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 47 functions across 26 files. (2 skipped: …
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: checking analyses against the full source text. It is concise and specific, although it does not mention the related stale-analysis and translation changes…
Full details: Out of Scope Changes check

Explanation

The changes to src/utils/stale-free-translations.ts and its tests place free translations from deleted verses or headings into fallback segments. Free translation placement does not manage analyses whose surface text is absent from the source text under issue #143. The PR also includes missing-book re-anchoring work, but that work has a clearer stale-analysis connection than the free-translation change.

✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]

This comment was marked as resolved.

@alex-rawlings-yyc
alex-rawlings-yyc marked this pull request as ready for review October 7, 2026 20:28

@imnasnainaec imnasnainaec left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review drafted with Claude Opus 5.5; inline comments are moderately reviewed by me.

<span className="tw:sr-only">{` ${staleCountLabel}`}</span>
</span>
)}
{isNotInText && (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conflicts with #400: its subgrid has 7 fixed columns, so this badge becomes an unplanned 8th. Coordinate on whichever merges second.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whichever of the two PRs lands second will add an 8th auto column to CatalogList's template when it rebases, so the badge gets its own aligned column.

Comment thread docs/reanchoring.md Outdated
## Algorithm

When it runs: on every book load, in `useReanchorToBook` (`src/components/AnalysisStore.tsx`), for an editable project only. An imported, read-only project is a record of what was imported and is never healed. A pass that moves nothing leaves the analysis identical, so opening a book neither dirties the draft nor writes storage.
When it runs: whenever the loaded book's text, its boundaries, or the draft changes, and for every other book the draft has records in each time the whole source text is read (for the concordance, or a catalog filter that needs the text), for an editable project only. A book the project no longer has is treated as holding no text, so every approval in it goes stale. An imported, read-only project is a record of what was imported and is never healed. A pass that moves nothing leaves the analysis identical, so opening a book neither dirties the draft nor writes storage.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ One ~60-word sentence. Make it a short list of triggers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

);

/**
* Re-anchors the draft to the absence of every book it has anything in that the text lacks,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Hard to parse. Suggest: "Marks links into books the project no longer has as stale."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded, though not as suggested: only approvals go stale, not every link, and this handler also records the draft as fully re-anchored. It now documents onTextRead in useWholeTextReanchor.

const [textReanchoredFor, setTextReanchoredFor] = useState<number>();
const staleCoversDraft = readingTarget !== undefined && textReanchoredFor === readingTarget;

/** Re-anchors the draft to a book the whole-text read hands on, where it has anything there. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Suggest: "Re-anchors the draft to each book the whole-text read delivers, if the draft links into it. The loaded book is skipped; it re-anchors to its live text."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, in one sentence; it now documents onBookRead in useWholeTextReanchor.

loadedBookCodeRef.current = verseBook?.bookRef;

/**
* The version of the draft a reading of the whole text re-anchors, `undefined` while it is

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Suggest: "Draft version the whole-text read re-anchors; undefined while loading or showing an import."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, keeping the clause that each change reads the text again, since this value is the reader's readKey. It now documents WholeTextReanchor.readKey.

Comment thread src/utils/stale-free-translations.ts Outdated
placesByVerse: ReadonlyMap<string, Place[]>,
): Place | undefined {
const [chapter, verseNumber] = versePosition(verse);
const inOrder = [...placesByVerse]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ This re-sorts every place for each vanished translation. Sort once per book.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: the document-ordered list is built once per call, and only when a translation's verse has vanished.

Comment thread src/components/TextReadingStatus.tsx Outdated

/** Localized string keys {@link TextReadingStatus} renders. */
export const TEXT_READING_STRING_KEYS = [
'%interlinearizer_concordance_loading%',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ These keys are shared beyond the concordance now. Rename them to %interlinearizer_textReading_*%?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done; _partial% stays a concordance key, since only the concordance shows it.

@@ -88,16 +109,23 @@ export default function useConcordanceIndex({
liveBook,
enabled,
shown,
onBookRead,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ This now serves the catalog and re-anchoring too. Split out a useSourceTextReader, or rename the hook?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split: useSourceTextReader reads the text, useConcordanceEntries builds the entries from it, and SourceTextContext provides both, replacing ConcordanceIndexContext.

* The version of the draft a reading of the whole text re-anchors, `undefined` while it is
* loading or an import is shown in its place; each change reads the text again.
*/
const readingTarget = isImportView || isDraftLoading ? undefined : draftVersion;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ These ~45 lines (2 refs + textReanchoredFor) could be a useWholeTextReanchor hook returning readKey / onBookRead / onTextRead / staleCoversDraft, testable with renderHook.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, with its own renderHook suite; the loader keeps a few wiring tests.

</ul>
)}

{!isAwaitingText &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Pulling out a CatalogRowList would avoid this re-indent and most of the conflict with #400.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this: #400 already extracts the <ul> and its sentinel as CatalogList, so a second extraction here would compete with it. Whichever PR lands second wraps the rows in CatalogList when it rebases.

const rows = useMemo(() => applyCatalogQuery(catalogRows, query), [catalogRows, query]);

/** Whether the listing waits on the text, a filter needing it before it is read. */
const isAwaitingText = filters.notInText && !textForms;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ filterTo (:550) sets filters without requestText(). It's safe today, since the stale notice only sets stale for the loaded book. But any filterTo({ notInText: true }), or filters restored later, would hang here on "Reading books… 0 of 0". Key the request off the wait condition instead:

Suggested change
const isAwaitingText = filters.notInText && !textForms;
const isAwaitingText = filters.notInText && !textForms;
// Covers every way filters get set, not just the controls.
useEffect(() => {
if (filters.notInText) requestText();
}, [filters.notInText, requestText]);

Then the handler only needs if (next.stale) requestText();.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as suggested.

@alex-rawlings-yyc alex-rawlings-yyc left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alex-rawlings-yyc made 21 comments.
Reviewable status: 0 of 37 files reviewed, 21 unresolved discussions (waiting on alex-rawlings-yyc and imnasnainaec).

Comment thread docs/reanchoring.md Outdated
## Algorithm

When it runs: on every book load, in `useReanchorToBook` (`src/components/AnalysisStore.tsx`), for an editable project only. An imported, read-only project is a record of what was imported and is never healed. A pass that moves nothing leaves the analysis identical, so opening a book neither dirties the draft nor writes storage.
When it runs: whenever the loaded book's text, its boundaries, or the draft changes, and for every other book the draft has records in each time the whole source text is read (for the concordance, or a catalog filter that needs the text), for an editable project only. A book the project no longer has is treated as holding no text, so every approval in it goes stale. An imported, read-only project is a record of what was imported and is never healed. A pass that moves nothing leaves the analysis identical, so opening a book neither dirties the draft nor writes storage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread src/components/AnalysisCatalogPanel.tsx Outdated

const rows = useMemo(() => applyCatalogQuery(catalogRows, query), [catalogRows, query]);

/** Whether the listing waits on the text, a filter needing it before it is read. */

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, worded to cover a failed read as well, which also leaves textForms undefined.

const rows = useMemo(() => applyCatalogQuery(catalogRows, query), [catalogRows, query]);

/** Whether the listing waits on the text, a filter needing it before it is read. */
const isAwaitingText = filters.notInText && !textForms;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as suggested.

</ul>
)}

{!isAwaitingText &&

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this: #400 already extracts the <ul> and its sentinel as CatalogList, so a second extraction here would compete with it. Whichever PR lands second wraps the rows in CatalogList when it rebases.

Comment thread src/components/CatalogFilterPopover.tsx Outdated
filters.morphemes,
filters.zeroUsages,
filters.stale,
filters.notInText,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as TOGGLE_FILTER_KEYS in analysis-query.ts, used by the popover's count and by reconcileFilters. It's named for toggles rather than booleans, since morphemes is 'has' | 'lacks'.

Comment thread src/hooks/useConcordanceIndex.ts Outdated
shown: boolean;
/**
* Reads every book again, as a refresh does, whenever this changes; a reading begun under an
* earlier value hands nothing on.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done; the hook is now useSourceTextReader.

@@ -88,16 +109,23 @@ export default function useConcordanceIndex({
liveBook,
enabled,
shown,
onBookRead,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split: useSourceTextReader reads the text, useConcordanceEntries builds the entries from it, and SourceTextContext provides both, replacing ConcordanceIndexContext.

Comment thread src/hooks/useConcordanceIndex.ts Outdated
writingSystemRef.current = writingSystem;

// Read through refs so a read in flight calls whichever callbacks are current when it lands.
const onBookReadRef = useRef(onBookRead);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, along with the three older hand-mirrored refs in the same file, which is now useSourceTextReader.

Comment thread src/hooks/useConcordanceIndex.ts Outdated
if (!wanted) return undefined;
let isAbandoned = false;
const key = readKeyRef.current;
// The key moves on as soon as it renders, ahead of the cleanup that abandons this read.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -201,8 +253,19 @@ export default function useConcordanceIndex({
return buildConcordanceEntries(books.values(), collator);
}, [status, readings, mergedLiveVersions, collator]);

const textForms = useMemo(() => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No comment added: with the reader split out, the held-back copy lives only in useConcordanceEntries, so textForms in useSourceTextReader has nothing to differ from.

@alex-rawlings-yyc alex-rawlings-yyc left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alex-rawlings-yyc made 21 comments.
Reviewable status: 0 of 37 files reviewed, 21 unresolved discussions (waiting on alex-rawlings-yyc and imnasnainaec).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handle orphaned analyses

2 participants