Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ The core types are:

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'`. Token and phrase payloads are shared by every occurrence analyzed identically, so a phrase occurrence is identified by `PhraseAnalysisLink.id`, never by its `analysisId`. `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.
Key invariants: `Segment.baselineText.slice(charStart, charEnd) === Token.surfaceText`; at most one linked analysis per token/segment may have `status: 'approved'`. Token and phrase payloads are shared by every occurrence analyzed identically, so a link is identified by its own `id`, never by its `analysisId`. Picking one of a token's or segment's links, or one of several identical payloads, goes through [pick-rules.ts](src/utils/pick-rules.ts), never list order. `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

Expand Down
5 changes: 5 additions & 0 deletions perf/dataset/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ export function generateAnalysis(
const segmentAnalyses: SegmentAnalysis[] = [];
const segmentAnalysisLinks: SegmentAnalysisLink[] = [];

// Its own stream, so link ids never shift the draws a tier's records come from.
const linkIds = createRng(seed, 'link-ids');

let clock = 0;
const nextStamp = (rng: Rng): string => {
clock += intBetween(rng, 1, 90_000);
Expand Down Expand Up @@ -119,6 +122,7 @@ export function generateAnalysis(
const payload = tokenPayload(surfaceText, token.writingSystem, sense, rng);
const stamp = nextStamp(rng);
tokenAnalysisLinks.push({
id: uuid(linkIds),
analysisId: payload.id,
createdAt: stamp,
updatedAt: stamp,
Expand Down Expand Up @@ -168,6 +172,7 @@ export function generateAnalysis(
},
});
segmentAnalysisLinks.push({
id: uuid(linkIds),
analysisId: id,
segmentId: segment.id,
createdAt: stamp,
Expand Down
8 changes: 7 additions & 1 deletion src/__tests__/components/AnalysisCatalogPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@ function link(
status: AssignmentStatus = 'approved',
surfaceText = 'ἀρχῇ',
): TokenAnalysisLink {
return { ...FIXTURE_STAMPS, analysisId, status, token: { tokenRef, surfaceText } };
return {
id: `${tokenRef}:${analysisId}:${status}`,
...FIXTURE_STAMPS,
analysisId,
status,
token: { tokenRef, surfaceText },
};
}

/** The token ref {@link FocusRequestProbe} last claimed, or `undefined` when it claimed nothing. */
Expand Down
19 changes: 17 additions & 2 deletions src/__tests__/components/AnalysisStore.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ function makeAnalysisWithGloss(
gloss: { und: gloss },
};
const link: TokenAnalysisLink = {
id: 'tl-1',
...FIXTURE_STAMPS,
analysisId: ta.id,
status: 'approved',
Expand Down Expand Up @@ -106,12 +107,14 @@ function makeSharedAnalysis(gloss = 'a', surfaceText = 'word'): TextAnalysis {
tokenAnalyses: [ta],
tokenAnalysisLinks: [
{
id: 'tl-2',
...FIXTURE_STAMPS,
analysisId: ta.id,
status: 'approved',
token: { tokenRef: 'tok-1', surfaceText },
},
{
id: 'tl-3',
...FIXTURE_STAMPS,
analysisId: ta.id,
status: 'approved',
Expand Down Expand Up @@ -207,6 +210,7 @@ describe('useGloss', () => {
gloss: { en: 'hi' },
};
const link: TokenAnalysisLink = {
id: 'tl-4',
...FIXTURE_STAMPS,
analysisId: 'ta-1',
status: 'suggested',
Expand Down Expand Up @@ -272,6 +276,7 @@ describe('useGloss', () => {
gloss: { fr: 'bonjour' },
};
const link: TokenAnalysisLink = {
id: 'tl-5',
...FIXTURE_STAMPS,
analysisId: 'ta-1',
status: 'approved',
Expand Down Expand Up @@ -563,6 +568,7 @@ describe('useGlossDispatch', () => {
gloss: { en: 'old' },
};
const suggestedLink: TokenAnalysisLink = {
id: 'tl-6',
...FIXTURE_STAMPS,
analysisId: 'suggested-1',
status: 'suggested',
Expand Down Expand Up @@ -1010,7 +1016,7 @@ const SEGMENT_ANALYSIS_WITH_TRANSLATION: TextAnalysis = {
},
],
segmentAnalysisLinks: [
{ ...FIXTURE_STAMPS, analysisId: 'sa-1', status: 'approved', segmentId: 'seg-1' },
{ id: 'sl-1', ...FIXTURE_STAMPS, analysisId: 'sa-1', status: 'approved', segmentId: 'seg-1' },
],
tokenAnalyses: [],
tokenAnalysisLinks: [],
Expand Down Expand Up @@ -1119,7 +1125,9 @@ function withSegmentTranslation(segmentId: string, status: AssignmentStatus): Te
segmentAnalyses: [
{ ...FIXTURE_STAMPS, id: 'sa-1', surfaceText: 'old text', freeTranslation: { und: 'old' } },
],
segmentAnalysisLinks: [{ ...FIXTURE_STAMPS, analysisId: 'sa-1', status, segmentId }],
segmentAnalysisLinks: [
{ id: 'sl-2', ...FIXTURE_STAMPS, analysisId: 'sa-1', status, segmentId },
],
};
}

Expand Down Expand Up @@ -1257,6 +1265,7 @@ describe('useMorphemes', () => {
],
};
const link: TokenAnalysisLink = {
id: 'tl-7',
...FIXTURE_STAMPS,
analysisId: 'ta-1',
status: 'approved',
Expand Down Expand Up @@ -1385,6 +1394,7 @@ describe('useConfirmMorphemesDispatch', () => {
],
tokenAnalysisLinks: [
{
id: 'tl-8',
...FIXTURE_STAMPS,
analysisId: 'ta-1',
status: 'approved',
Expand Down Expand Up @@ -1435,6 +1445,7 @@ describe('useMorphemeDeleteDispatch', () => {
],
};
const link: TokenAnalysisLink = {
id: 'tl-9',
...FIXTURE_STAMPS,
analysisId: 'ta-1',
status: 'approved',
Expand Down Expand Up @@ -1493,6 +1504,7 @@ describe('useMorphemeGlossDispatch', () => {
],
};
const link: TokenAnalysisLink = {
id: 'tl-10',
...FIXTURE_STAMPS,
analysisId: 'ta-1',
status: 'approved',
Expand Down Expand Up @@ -1533,12 +1545,14 @@ describe('useMorphemeGlossDispatch', () => {
],
tokenAnalysisLinks: [
{
id: 'tl-11',
...FIXTURE_STAMPS,
analysisId: 'shared-analysis',
status: 'approved',
token: { tokenRef: 'tok-1', surfaceText: 'word' },
},
{
id: 'tl-12',
...FIXTURE_STAMPS,
analysisId: 'shared-analysis',
status: 'approved',
Expand Down Expand Up @@ -1891,6 +1905,7 @@ describe('useApproveAnalysisDispatch', () => {

function approvedLink(analysisId: string, tokenRef: string): TokenAnalysisLink {
return {
id: `${tokenRef}:${analysisId}`,
...FIXTURE_STAMPS,
analysisId,
token: { tokenRef, surfaceText: 'ἀρχῇ' },
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/components/ConcordancePanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ function makeText(overrides: Partial<SourceText> = {}): SourceText {
/** Builds an approved link from `tokenRef` to the analysis. */
function link(analysisId: string, tokenRef: string): TokenAnalysisLink {
return {
id: `${tokenRef}:${analysisId}`,
...FIXTURE_STAMPS,
analysisId,
status: 'approved',
Expand Down
12 changes: 9 additions & 3 deletions src/__tests__/components/ConcordanceRowView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const localizedStrings = Object.fromEntries(CONCORDANCE_ROW_STRING_KEYS.map((k)
/** Builds an approved link from `tokenRef` to the analysis. */
function link(analysisId: string, tokenRef: string): TokenAnalysisLink {
return {
id: `${tokenRef}:${analysisId}`,
...FIXTURE_STAMPS,
analysisId,
status: 'approved',
Expand All @@ -49,7 +50,8 @@ type RowOptions = Partial<{
function renderRow(options: RowOptions = {}) {
const verses = options.verses ?? [{ sid: 'GEN 1:3', text: 'Let there be light' }];
const entries = buildConcordanceEntries([indexBook(makeVerseBook(verses))], new Collator('en'));
const approvedByToken = approvedAnalysisByToken(options.links ?? []);
const analysesById = new Map((options.analyses ?? []).map((a) => [a.id, a]));
const approvedByToken = approvedAnalysisByToken(options.links ?? [], analysesById);
const row = deriveConcordanceRows(entries, approvedByToken, options.currentBook ?? 'GEN').find(
(r) => r.entry.form === 'light',
);
Expand All @@ -58,7 +60,7 @@ function renderRow(options: RowOptions = {}) {
withTooltipProvider(
<ul>
<ConcordanceRowView
analysesById={new Map((options.analyses ?? []).map((a) => [a.id, a]))}
analysesById={analysesById}
analysisLanguage="en"
approvedByToken={approvedByToken}
isSelected={options.isSelected ?? false}
Expand Down Expand Up @@ -132,7 +134,11 @@ describe('ConcordanceRowView', () => {
['partlyAnalyzed', ['GEN 1:3:0'], 'circle-dashed-icon'],
['unanalyzed', [], 'circle-icon'],
])('marks a %s form with its own icon and label', (status, refs, icon) => {
renderRow({ verses: LIGHT_VERSES, links: refs.map((ref) => link('a1', ref)) });
renderRow({
verses: LIGHT_VERSES,
links: refs.map((ref) => link('a1', ref)),
analyses: [analysis('a1')],
});

const marker = screen.getByTestId('concordance-row-status');
expect(marker).toHaveAttribute('data-status', status);
Expand Down
3 changes: 3 additions & 0 deletions src/__tests__/components/InterlinearizerLoader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ function analysisApprovingAt(tokenRef: string, surfaceText: string): TextAnalysi
tokenAnalyses: [{ ...FIXTURE_STAMPS, id: 'ta-1', surfaceText }],
tokenAnalysisLinks: [
{
id: 'tl-1',
...FIXTURE_STAMPS,
analysisId: 'ta-1',
status: 'approved',
Expand Down Expand Up @@ -3440,6 +3441,7 @@ describe('InterlinearizerLoader', () => {
/** A stale place of `analysisId` at `tokenRef`, whose word read differently when glossed. */
function staleAt(analysisId: string, tokenRef: string) {
return {
id: `${tokenRef}:${analysisId}`,
...FIXTURE_STAMPS,
analysisId,
status: 'stale' as const,
Expand All @@ -3459,6 +3461,7 @@ describe('InterlinearizerLoader', () => {
staleAt('ta-gen', 'GEN 1:1:0'),
staleAt('ta-exo', 'EXO 1:1:0'),
{
id: 'tl-2',
...FIXTURE_STAMPS,
analysisId: 'ta-live',
status: 'approved',
Expand Down
8 changes: 7 additions & 1 deletion src/__tests__/components/StaleAnalysesReporter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ jest.mock('../../components/AnalysisStore', () => ({
}));

function staleLink(analysisId: string, tokenRef: string): TokenAnalysisLink {
return { ...FIXTURE_STAMPS, analysisId, token: { tokenRef, surfaceText: 'In' }, status: 'stale' };
return {
id: `${tokenRef}:${analysisId}`,
...FIXTURE_STAMPS,
analysisId,
token: { tokenRef, surfaceText: 'In' },
status: 'stale',
};
}

function staleTranslation(analysisId: string, segmentId: string): StaleFreeTranslation {
Expand Down
14 changes: 14 additions & 0 deletions src/__tests__/components/TokenChip.suggestions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function poolWithOneApproved(gloss: string | undefined, surfaceText = 'logos'):
...(gloss === undefined ? {} : { gloss: { en: gloss } }),
};
const link: TokenAnalysisLink = {
id: 'tl-1',
...FIXTURE_STAMPS,
analysisId: 'ta-1',
status: 'approved',
Expand Down Expand Up @@ -64,18 +65,21 @@ function homographBankPool(financeGloss: string | undefined): TextAnalysis {
};
const links: TokenAnalysisLink[] = [
{
id: 'tl-2',
...FIXTURE_STAMPS,
analysisId: 'ta-river',
status: 'approved',
token: { tokenRef: 'r1', surfaceText: 'bank' },
},
{
id: 'tl-3',
...FIXTURE_STAMPS,
analysisId: 'ta-river',
status: 'approved',
token: { tokenRef: 'r2', surfaceText: 'bank' },
},
{
id: 'tl-4',
...FIXTURE_STAMPS,
analysisId: 'ta-fin',
status: 'approved',
Expand Down Expand Up @@ -113,18 +117,21 @@ function sameGlossParsePool(rivalForms: readonly string[]): TextAnalysis {
};
const links: TokenAnalysisLink[] = [
{
id: 'tl-5',
...FIXTURE_STAMPS,
analysisId: 'ta-past',
status: 'approved',
token: { tokenRef: 'p1', surfaceText: 'ran' },
},
{
id: 'tl-6',
...FIXTURE_STAMPS,
analysisId: 'ta-past',
status: 'approved',
token: { tokenRef: 'p2', surfaceText: 'ran' },
},
{
id: 'tl-7',
...FIXTURE_STAMPS,
analysisId: 'ta-rival',
status: 'approved',
Expand Down Expand Up @@ -154,18 +161,21 @@ function homographTopBlankPool(): TextAnalysis {
};
const links: TokenAnalysisLink[] = [
{
id: 'tl-8',
...FIXTURE_STAMPS,
analysisId: 'ta-blank',
status: 'approved',
token: { tokenRef: 'b1', surfaceText: 'bank' },
},
{
id: 'tl-9',
...FIXTURE_STAMPS,
analysisId: 'ta-blank',
status: 'approved',
token: { tokenRef: 'b2', surfaceText: 'bank' },
},
{
id: 'tl-10',
...FIXTURE_STAMPS,
analysisId: 'ta-fin',
status: 'approved',
Expand Down Expand Up @@ -545,24 +555,28 @@ function homographMajorityPool(): TextAnalysis {
};
const links: TokenAnalysisLink[] = [
{
id: 'tl-11',
...FIXTURE_STAMPS,
analysisId: 'ta-river',
status: 'approved',
token: { tokenRef: 'r1', surfaceText: 'bank' },
},
{
id: 'tl-12',
...FIXTURE_STAMPS,
analysisId: 'ta-river',
status: 'approved',
token: { tokenRef: 'r2', surfaceText: 'bank' },
},
{
id: 'tl-13',
...FIXTURE_STAMPS,
analysisId: 'ta-river',
status: 'approved',
token: { tokenRef: 'r3', surfaceText: 'bank' },
},
{
id: 'tl-14',
...FIXTURE_STAMPS,
analysisId: 'ta-fin',
status: 'approved',
Expand Down
4 changes: 4 additions & 0 deletions src/__tests__/components/TokenChip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,13 @@ describe('TokenChip', () => {
],
tokenAnalysisLinks: [
{
id: 'tl-1',
...FIXTURE_STAMPS,
analysisId: 'ta-1',
status: 'approved',
token: { tokenRef: 'GEN 1:1:0', surfaceText: 'hello' },
} satisfies {
id: string;
analysisId: string;
createdAt: string;
updatedAt: string;
Expand Down Expand Up @@ -966,11 +968,13 @@ describe('TokenChip read-only', () => {
tokenAnalyses: [{ ...FIXTURE_STAMPS, id: 'ta-1', surfaceText: 'hello', gloss: { und: 'in' } }],
tokenAnalysisLinks: [
{
id: 'tl-1',
...FIXTURE_STAMPS,
analysisId: 'ta-1',
status: 'approved',
token: { tokenRef: 'GEN 1:1:0', surfaceText: 'hello' },
} satisfies {
id: string;
analysisId: string;
createdAt: string;
updatedAt: string;
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/converters/pt9/analysisMerger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ describe('mergeLanguageAnalyses - token records', () => {
]);
expect(result.tokenAnalysisLinks).toStrictEqual([
{
id: 'pt9:tl:GEN 1:1:0:0',
analysisId: 'pt9:ta:GEN 1:1:0:0',
createdAt: STAMP,
updatedAt: STAMP,
Expand Down
Loading
Loading