@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.
@@ -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
+ }