@startsimpli/integrations 0.2.19 → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/connections-me-client.test.ts +122 -0
- package/src/__tests__/connections.test.tsx +206 -1
- package/src/__tests__/translation-corpus.test.ts +428 -0
- package/src/api/connections.ts +129 -0
- package/src/api/index.ts +1 -0
- package/src/components/pages/ConnectionsPage.tsx +214 -64
- package/src/components/pages/types.ts +58 -0
- package/src/corpus/translation.ts +321 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CLIENT CORPUS FOR TRANSLATION — glossary and translation memory
|
|
3
|
+
* (bd startsim-ppfl RED / bd startsim-364c GREEN).
|
|
4
|
+
*
|
|
5
|
+
* CONTRACT AND STUBS ONLY. Every function throws `NotImplementedError`, so the
|
|
6
|
+
* tests in `../__tests__/translation-corpus.test.ts` fail on missing BEHAVIOUR
|
|
7
|
+
* rather than on a missing module.
|
|
8
|
+
*
|
|
9
|
+
* ── WHY THIS LIVES IN THE CORPUS PACKAGE ───────────────────────────────────
|
|
10
|
+
* `startsim-3o4e` decides the whole mechanism is glossary + translation-memory
|
|
11
|
+
* injection — there is no fine-tune. So "the client's own stuff" is a CORPUS
|
|
12
|
+
* question, and this package already owns the three hard parts of that:
|
|
13
|
+
*
|
|
14
|
+
* - the generic `TenantEntity` envelope (`./entities`), so a glossary term
|
|
15
|
+
* and a memory unit are rows of a new `entity_type` and NOT new models
|
|
16
|
+
* (feedback_brutally_generic);
|
|
17
|
+
* - the scope guard (`./retrieval`), which already partitions a candidate set
|
|
18
|
+
* and SURFACES the drop count rather than swallowing it — which is exactly
|
|
19
|
+
* the shape isolation needs;
|
|
20
|
+
* - a Porter stemmer and a Jaccard (`./text`), which term extraction and
|
|
21
|
+
* fuzzy memory both want and neither should reimplement.
|
|
22
|
+
*
|
|
23
|
+
* ── THE ONE RULE THE BEAD PUTS IN CAPITALS ─────────────────────────────────
|
|
24
|
+
* AN UNCONFIRMED AUTO-EXTRACTED TERM MUST NOT SILENTLY BECOME AUTHORITATIVE.
|
|
25
|
+
* Extraction proposes; a human disposes. `status: 'candidate'` is never
|
|
26
|
+
* binding, never injected into a prompt, and never counted in an adherence
|
|
27
|
+
* score — because a glossary that fills itself is a glossary the client did not
|
|
28
|
+
* agree to, and `startsim-r9q8` measures adherence AGAINST it.
|
|
29
|
+
*
|
|
30
|
+
* ── ISOLATION IS THE POINT, NOT A FEATURE ──────────────────────────────────
|
|
31
|
+
* `startsim-hopl` sells "one client's material never appears against
|
|
32
|
+
* another's". A glossary term is the sharpest possible violation of that: it is
|
|
33
|
+
* short, memorable, and it would appear IN ANOTHER CLIENT'S TRANSLATED
|
|
34
|
+
* DOCUMENT. So every read here is scope-partitioned and every drop is counted.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type { CorpusPage, CorpusScope, SourceDocument } from './types';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The `entity_type` keys translation adds. Defaults, not a closed set — the
|
|
41
|
+
* same convention as `DEFAULT_TYPE_KEYS` in `./entities`.
|
|
42
|
+
*/
|
|
43
|
+
export interface TranslationTypeKeys {
|
|
44
|
+
glossaryTerm: string;
|
|
45
|
+
memoryUnit: string;
|
|
46
|
+
/** The client's ingested source document, reusing the corpus document type. */
|
|
47
|
+
sourceDocument: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const TRANSLATION_TYPE_KEYS: TranslationTypeKeys = {
|
|
51
|
+
glossaryTerm: 'glossary_term',
|
|
52
|
+
memoryUnit: 'translation_memory_unit',
|
|
53
|
+
sourceDocument: 'source_document',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Where a term is in its life.
|
|
58
|
+
*
|
|
59
|
+
* `candidate` — proposed by extraction, NOT authoritative, NOT binding.
|
|
60
|
+
* `approved` — a human ruled on it. Only these bind.
|
|
61
|
+
* `rejected` — a human ruled against it. Kept so extraction stops re-proposing.
|
|
62
|
+
*/
|
|
63
|
+
export type TermStatus = 'candidate' | 'approved' | 'rejected';
|
|
64
|
+
|
|
65
|
+
export interface GlossaryTermRecord {
|
|
66
|
+
id: string;
|
|
67
|
+
sourceLocale: string;
|
|
68
|
+
targetLocale: string;
|
|
69
|
+
sourceTerm: string;
|
|
70
|
+
/** Empty while a candidate — nobody has said what it should be yet. */
|
|
71
|
+
targetTerm: string;
|
|
72
|
+
status: TermStatus;
|
|
73
|
+
/** Brand and entity names: `sourceTerm` must survive verbatim. */
|
|
74
|
+
doNotTranslate?: boolean;
|
|
75
|
+
/** How often extraction saw it. Evidence for a human, never a promotion rule. */
|
|
76
|
+
occurrences: number;
|
|
77
|
+
/** Which document(s) it came from, so a reviewer can check the claim. */
|
|
78
|
+
documentIds: string[];
|
|
79
|
+
scope: CorpusScope;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Where a memory unit came from. A reviewer edit is the one that matters. */
|
|
83
|
+
export type MemoryOrigin = 'machine' | 'reviewer_edit' | 'imported';
|
|
84
|
+
|
|
85
|
+
export interface MemoryUnitRecord {
|
|
86
|
+
id: string;
|
|
87
|
+
sourceLocale: string;
|
|
88
|
+
targetLocale: string;
|
|
89
|
+
sourceText: string;
|
|
90
|
+
targetText: string;
|
|
91
|
+
/**
|
|
92
|
+
* The NORMALISED source, which is what an exact match compares. Stored rather
|
|
93
|
+
* than recomputed so a normalisation change is visible as a migration instead
|
|
94
|
+
* of silently repartitioning the memory.
|
|
95
|
+
*/
|
|
96
|
+
sourceHash: string;
|
|
97
|
+
origin: MemoryOrigin;
|
|
98
|
+
/** Only an approved unit is served. A machine draft is not memory. */
|
|
99
|
+
approved: boolean;
|
|
100
|
+
scope: CorpusScope;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A row as the tenant's generic upsert takes it. */
|
|
104
|
+
export interface TenantEntityWrite {
|
|
105
|
+
entity_type: string;
|
|
106
|
+
external_id: string;
|
|
107
|
+
name: string;
|
|
108
|
+
data: Record<string, unknown>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class NotImplementedError extends Error {
|
|
112
|
+
constructor(what: string) {
|
|
113
|
+
super(`${what} is not implemented (bd startsim-ppfl is RED; bd startsim-364c implements it)`);
|
|
114
|
+
this.name = 'NotImplementedError';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// ingestion
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
export interface IngestOptions {
|
|
123
|
+
scope: CorpusScope;
|
|
124
|
+
typeKeys?: TranslationTypeKeys;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A client document, as rows.
|
|
129
|
+
*
|
|
130
|
+
* Returns WRITES rather than performing them, so the test can assert what would
|
|
131
|
+
* be written without a tenant — and so ingestion stays pure, like every other
|
|
132
|
+
* mapper in this package.
|
|
133
|
+
*/
|
|
134
|
+
export function ingestForTranslation(
|
|
135
|
+
_document: SourceDocument,
|
|
136
|
+
_options: IngestOptions,
|
|
137
|
+
): TenantEntityWrite[] {
|
|
138
|
+
throw new NotImplementedError('ingestForTranslation');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// term extraction — proposes, never decides
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
export interface ExtractTermsOptions {
|
|
146
|
+
scope: CorpusScope;
|
|
147
|
+
sourceLocale: string;
|
|
148
|
+
targetLocale: string;
|
|
149
|
+
/** Terms already ruled on, so a rejected term is not proposed again. */
|
|
150
|
+
known?: readonly GlossaryTermRecord[];
|
|
151
|
+
/** How many times a phrase must occur before it is worth a human's attention. */
|
|
152
|
+
minOccurrences?: number;
|
|
153
|
+
maxCandidates?: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Propose glossary candidates from the client's own documents.
|
|
158
|
+
*
|
|
159
|
+
* EVERY RETURNED TERM IS `status: 'candidate'`. There is no code path in this
|
|
160
|
+
* module that produces an `approved` term without a human verdict.
|
|
161
|
+
*/
|
|
162
|
+
export function extractTermCandidates(
|
|
163
|
+
_documents: readonly SourceDocument[],
|
|
164
|
+
_options: ExtractTermsOptions,
|
|
165
|
+
): GlossaryTermRecord[] {
|
|
166
|
+
throw new NotImplementedError('extractTermCandidates');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Record a human's verdict. The ONLY route from `candidate` to `approved`. */
|
|
170
|
+
export function applyTermVerdict(
|
|
171
|
+
_term: GlossaryTermRecord,
|
|
172
|
+
_verdict: { status: 'approved' | 'rejected'; targetTerm?: string; doNotTranslate?: boolean },
|
|
173
|
+
): GlossaryTermRecord {
|
|
174
|
+
throw new NotImplementedError('applyTermVerdict');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// glossary lookup — scales with the text, not with the glossary
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The BINDING terms whose source form occurs in `segmentText`.
|
|
183
|
+
*
|
|
184
|
+
* Two filters, both load-bearing: occurrence (so the prompt scales with the
|
|
185
|
+
* page, not the glossary) and status (so a candidate never reaches a model).
|
|
186
|
+
*/
|
|
187
|
+
export function glossaryForSegment(
|
|
188
|
+
_segmentText: string,
|
|
189
|
+
_terms: readonly GlossaryTermRecord[],
|
|
190
|
+
): GlossaryTermRecord[] {
|
|
191
|
+
throw new NotImplementedError('glossaryForSegment');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// translation memory
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The comparable form of a source string: whitespace collapsed, case folded,
|
|
200
|
+
* bidi isolates stripped. A MATERIAL change — a different word, a different
|
|
201
|
+
* number — must survive normalisation and defeat the match.
|
|
202
|
+
*/
|
|
203
|
+
export function normalizeForMemory(_text: string): string {
|
|
204
|
+
throw new NotImplementedError('normalizeForMemory');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface MemoryLookupOptions {
|
|
208
|
+
scope: CorpusScope;
|
|
209
|
+
sourceLocale: string;
|
|
210
|
+
targetLocale: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** An exact hit, or null. Costs nothing and makes no provider call. */
|
|
214
|
+
export function exactMemoryHit(
|
|
215
|
+
_segmentText: string,
|
|
216
|
+
_units: readonly MemoryUnitRecord[],
|
|
217
|
+
_options: MemoryLookupOptions,
|
|
218
|
+
): MemoryUnitRecord | null {
|
|
219
|
+
throw new NotImplementedError('exactMemoryHit');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface FuzzyMatch {
|
|
223
|
+
unit: MemoryUnitRecord;
|
|
224
|
+
/** 0..1. Higher is closer. */
|
|
225
|
+
score: number;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface FuzzyOptions extends MemoryLookupOptions {
|
|
229
|
+
limit?: number;
|
|
230
|
+
minScore?: number;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Neighbours, best first, capped. Offered to the model as EXAMPLES, never as answers. */
|
|
234
|
+
export function fuzzyMemoryMatches(
|
|
235
|
+
_segmentText: string,
|
|
236
|
+
_units: readonly MemoryUnitRecord[],
|
|
237
|
+
_options: FuzzyOptions,
|
|
238
|
+
): FuzzyMatch[] {
|
|
239
|
+
throw new NotImplementedError('fuzzyMemoryMatches');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* A reviewer's accepted edit, as a memory unit.
|
|
244
|
+
*
|
|
245
|
+
* `startsim-witd`: without this "the reviewer corrects the same term forever,
|
|
246
|
+
* which is exactly the upkeep trap that makes people abandon tools."
|
|
247
|
+
*/
|
|
248
|
+
export function memoryUnitFromReviewerEdit(
|
|
249
|
+
_edit: {
|
|
250
|
+
sourceText: string;
|
|
251
|
+
editedTargetText: string;
|
|
252
|
+
sourceLocale: string;
|
|
253
|
+
targetLocale: string;
|
|
254
|
+
},
|
|
255
|
+
_scope: CorpusScope,
|
|
256
|
+
): MemoryUnitRecord {
|
|
257
|
+
throw new NotImplementedError('memoryUnitFromReviewerEdit');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// isolation
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
export interface ScopedRead<T> {
|
|
265
|
+
kept: T[];
|
|
266
|
+
/** NON-ZERO IS A BUG UPSTREAM AND SOMEBODY NEEDS TO SEE IT. Never swallowed. */
|
|
267
|
+
droppedOutOfScope: number;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Partition glossary terms by the asking principal's scope.
|
|
272
|
+
*
|
|
273
|
+
* A THIRD check, after the tenant API's `org_id` filter and Postgres RLS, for
|
|
274
|
+
* the reason `./retrieval` already states: if any layer above hands back a row
|
|
275
|
+
* it should not have, the OUTPUT must still not contain it.
|
|
276
|
+
*/
|
|
277
|
+
export function scopedTerms(
|
|
278
|
+
_terms: readonly GlossaryTermRecord[],
|
|
279
|
+
_scope: CorpusScope,
|
|
280
|
+
): ScopedRead<GlossaryTermRecord> {
|
|
281
|
+
throw new NotImplementedError('scopedTerms');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function scopedMemory(
|
|
285
|
+
_units: readonly MemoryUnitRecord[],
|
|
286
|
+
_scope: CorpusScope,
|
|
287
|
+
): ScopedRead<MemoryUnitRecord> {
|
|
288
|
+
throw new NotImplementedError('scopedMemory');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// paging
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The read side of the corpus, paged.
|
|
297
|
+
*
|
|
298
|
+
* PAST THE FIRST PAGE, for the reason `./extract` documents from the wire: the
|
|
299
|
+
* tenant's `EntityViewSet` pages with DRF's default and ignores `page_size`, so
|
|
300
|
+
* a caller that reads one page and calls it the glossary silently stops seeing
|
|
301
|
+
* terms at whatever `PAGE_SIZE` happens to be. A glossary that quietly truncates
|
|
302
|
+
* is an adherence score that is quietly wrong.
|
|
303
|
+
*/
|
|
304
|
+
export interface TranslationCorpusApi {
|
|
305
|
+
listGlossaryTerms(query: {
|
|
306
|
+
page?: number;
|
|
307
|
+
pageSize?: number;
|
|
308
|
+
}): Promise<CorpusPage<GlossaryTermRecord>>;
|
|
309
|
+
listMemoryUnits(query: {
|
|
310
|
+
page?: number;
|
|
311
|
+
pageSize?: number;
|
|
312
|
+
}): Promise<CorpusPage<MemoryUnitRecord>>;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function readAllGlossaryTerms(
|
|
316
|
+
_api: TranslationCorpusApi,
|
|
317
|
+
_scope: CorpusScope,
|
|
318
|
+
_options?: { maxPages?: number },
|
|
319
|
+
): Promise<ScopedRead<GlossaryTermRecord>> {
|
|
320
|
+
throw new NotImplementedError('readAllGlossaryTerms');
|
|
321
|
+
}
|