@startsimpli/llm 0.4.15 → 0.4.17

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,300 @@
1
+ /**
2
+ * The translation engine's CONTRACT (bd startsim-ob4d, RED).
3
+ *
4
+ * Types only. The behaviour is stubbed in `./index` and unimplemented on
5
+ * purpose — the tests in `./__tests__` are written against this surface first
6
+ * and are expected to fail until bd startsim-h1q8 (GREEN) implements it.
7
+ *
8
+ * THE THREE DESIGN DECISIONS THIS FILE ENCODES, so a reader does not have to
9
+ * rediscover them from the tests:
10
+ *
11
+ * `docs/design/translation/startsim-yfot-…` — the batch is N segments in ONE
12
+ * prompt with echoed ids, cut by CHARACTER budget, and the id-echo contract
13
+ * is exact: the returned id set equals the requested id set.
14
+ *
15
+ * `docs/design/translation/startsim-r9q8-…` — quality is proven by comparing
16
+ * SOURCE TO OUTPUT, never by judging the output's language. Hence
17
+ * `MechanicalChecks`: glossary adherence, do-not-translate, numeric fidelity.
18
+ *
19
+ * `docs/design/translation/startsim-bfem-…` — assert invariants, never
20
+ * wording. `temperature: 0` is not available on the default model, so nothing
21
+ * here may assume a reproducible string.
22
+ *
23
+ * NO LOCALE IS NAMED ANYWHERE IN THIS PACKAGE. `sourceLocale`/`targetLocale`
24
+ * are opaque BCP-47 strings supplied per request. A branch on a locale code is
25
+ * the leak `startsim-jb1z` bans, and the two-locale fixture set exists to catch
26
+ * one.
27
+ */
28
+
29
+ import type { ILLMProvider } from '../providers/interface';
30
+
31
+ /** A BCP-47 tag. Data, never a branch. */
32
+ export type Locale = string;
33
+
34
+ /**
35
+ * One unit of text to translate.
36
+ *
37
+ * `id` IS OPAQUE. The engine assigns nothing, parses nothing out of it, and
38
+ * round-trips it byte-for-byte. The caller owns segmentation, so the caller
39
+ * owns the naming.
40
+ */
41
+ export interface TranslationSegment {
42
+ id: string;
43
+ text: string;
44
+ }
45
+
46
+ /**
47
+ * A term the client has ruled on.
48
+ *
49
+ * `binding` means the approved `target` MUST appear when `source` occurred.
50
+ * `doNotTranslate` means `source` must survive VERBATIM — brand names, product
51
+ * names, legal entity names — and `target` is ignored for those.
52
+ */
53
+ export interface GlossaryTerm {
54
+ source: string;
55
+ target: string;
56
+ binding: boolean;
57
+ doNotTranslate?: boolean;
58
+ }
59
+
60
+ /** A previously approved pair, reusable by exact match. */
61
+ export interface MemoryUnit {
62
+ sourceText: string;
63
+ targetText: string;
64
+ sourceLocale: Locale;
65
+ targetLocale: Locale;
66
+ }
67
+
68
+ export interface TranslationRequest {
69
+ segments: TranslationSegment[];
70
+ sourceLocale: Locale;
71
+ targetLocale: Locale;
72
+ /** Only the terms occurring in THIS batch are injected. See `renderTranslatePrompt`. */
73
+ glossary?: readonly GlossaryTerm[];
74
+ /**
75
+ * Approved pairs, used two ways: an EXACT source match is served without a
76
+ * provider call at all, and the remainder ride along as labelled examples.
77
+ *
78
+ * THE CALLER BOUNDS THIS. The engine injects what it is given, so handing it a
79
+ * whole translation memory puts a whole translation memory in every prompt —
80
+ * the failure the glossary is filtered to avoid. `startsim-jb1z` specs the
81
+ * store side as `nearest(segmentText, targetLocale, limit)` for exactly this
82
+ * reason; the engine does not re-rank, because it has no retriever.
83
+ */
84
+ memory?: readonly MemoryUnit[];
85
+ }
86
+
87
+ /**
88
+ * What produced a unit. Without it, a quality regression after a route change
89
+ * is unattributable (`startsim-jb1z`'s artifact model).
90
+ */
91
+ export interface TranslationProvenance {
92
+ provider: string;
93
+ model: string;
94
+ promptVersion: string;
95
+ }
96
+
97
+ /** One term's verdict. `present` is a substring question, not a judgement. */
98
+ export interface TermCheck {
99
+ term: string;
100
+ present: boolean;
101
+ }
102
+
103
+ /**
104
+ * The gates that run on EVERY batch in production, not only in tests
105
+ * (`startsim-r9q8`). All are computed source-to-output and need no reader of
106
+ * the target language.
107
+ */
108
+ export interface MechanicalChecks {
109
+ glossaryAdherence: TermCheck[];
110
+ doNotTranslate: TermCheck[];
111
+ /** Numbers, dates and percentages in the source that are absent from the target. */
112
+ missingNumerics: string[];
113
+ /** False when any binding term, do-not-translate string or numeric is missing. */
114
+ ok: boolean;
115
+ }
116
+
117
+ export interface TranslatedSegment {
118
+ id: string;
119
+ /**
120
+ * Storage form. Carries NO bidi isolate characters — isolation happens at
121
+ * render time (`packages/i18n/src/bidi.ts`), so a stored string stays
122
+ * comparable.
123
+ */
124
+ targetText: string;
125
+ provenance: TranslationProvenance;
126
+ /** True when served from memory, which means no provider call was made. */
127
+ fromMemory: boolean;
128
+ checks: MechanicalChecks;
129
+ }
130
+
131
+ export type FailureReason =
132
+ | 'missing_id'
133
+ | 'empty_text'
134
+ | 'mechanical_check'
135
+ | 'provider_error';
136
+
137
+ export interface TranslationFailure {
138
+ id: string;
139
+ reason: FailureReason;
140
+ }
141
+
142
+ export interface TranslationResult {
143
+ translated: TranslatedSegment[];
144
+ failed: TranslationFailure[];
145
+ /** Ids re-queued individually after the batch response omitted them. */
146
+ requeued: string[];
147
+ /** Provider calls actually made. A memory-only request must report 0. */
148
+ providerCalls: number;
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // the envelope
153
+ // ---------------------------------------------------------------------------
154
+
155
+ /**
156
+ * The parse of one provider response, per `startsim-yfot`'s id-echo contract.
157
+ *
158
+ * A MISSING id is survivable — it fails only its own segment, which is
159
+ * re-queued. An UNKNOWN or DUPLICATE id is not: the model has attributed text
160
+ * to a segment that did not ask for it, and in a product whose selling point is
161
+ * that one client's material never appears against another's, that is a
162
+ * correctness alarm. Never map an unknown id positionally.
163
+ */
164
+ export type EnvelopeRejection = 'unknown_id' | 'duplicate_id' | 'malformed';
165
+
166
+ export interface EnvelopeAccepted {
167
+ ok: true;
168
+ segments: TranslationSegment[];
169
+ /** Requested ids the response did not carry. Re-queued individually. */
170
+ missing: string[];
171
+ /** Present ids whose text was blank — a failure, never an empty translation. */
172
+ empty: string[];
173
+ }
174
+
175
+ export interface EnvelopeRejected {
176
+ ok: false;
177
+ reason: EnvelopeRejection;
178
+ /** The offending ids, for the operator. NEVER the response text. */
179
+ offendingIds: string[];
180
+ }
181
+
182
+ export type EnvelopeResult = EnvelopeAccepted | EnvelopeRejected;
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // structure
186
+ // ---------------------------------------------------------------------------
187
+
188
+ /**
189
+ * A document as the CALLER models it. Structure never enters the model
190
+ * (`startsim-jb1z`), so the engine segments and reassembles and the round trip
191
+ * is deterministic by construction.
192
+ *
193
+ * `kind` is free-form — `heading`, `paragraph`, `list_item`, `table_cell`,
194
+ * `code`. The engine does not interpret it; it carries it through.
195
+ */
196
+ export interface DocumentNode {
197
+ kind: string;
198
+ /** Leaf text. Absent on a container. */
199
+ text?: string;
200
+ children?: DocumentNode[];
201
+ /** Nodes flagged here are carried through untranslated (product codes, ids). */
202
+ translatable?: boolean;
203
+ }
204
+
205
+ export interface StructuredDocument {
206
+ nodes: DocumentNode[];
207
+ }
208
+
209
+ /**
210
+ * The extraction result: the translatable leaves, plus whatever the
211
+ * reassembler needs to put them back. The skeleton is opaque to the caller.
212
+ */
213
+ export interface SegmentedDocument {
214
+ segments: TranslationSegment[];
215
+ skeleton: StructuredDocument;
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // batching
220
+ // ---------------------------------------------------------------------------
221
+
222
+ /**
223
+ * Provisional, and explicitly so (`startsim-yfot`, Unverified). 4,000 chars sits
224
+ * below the MEASURED 6,000-char extraction budget because translation output
225
+ * scales with input and extraction's does not; 25 segments caps the id-matching
226
+ * burden on a page of one-line table cells.
227
+ *
228
+ * These are to be REPLACED by a measurement of translation itself.
229
+ */
230
+ export interface BatchLimits {
231
+ maxCharsPerBatch: number;
232
+ maxSegmentsPerBatch: number;
233
+ }
234
+
235
+ export const DEFAULT_BATCH_LIMITS: BatchLimits = {
236
+ maxCharsPerBatch: 4_000,
237
+ maxSegmentsPerBatch: 25,
238
+ };
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // routing
242
+ // ---------------------------------------------------------------------------
243
+
244
+ /**
245
+ * The retention posture, which is what picks the provider — not the model's
246
+ * quality (`startsim-hopl`, `startsim-yfot`).
247
+ *
248
+ * `walled` carries the paying client's confidential material and CANNOT fall
249
+ * back: if the configured provider is unavailable the call fails and is retried
250
+ * later against the same provider. `open` carries material that is not
251
+ * confidential in that sense and may fall back within the posture.
252
+ */
253
+ export type RetentionPosture = 'walled' | 'open';
254
+
255
+ export interface TranslationRoute {
256
+ posture: RetentionPosture;
257
+ provider: string;
258
+ model: string;
259
+ /** Boot-time allow-list. A model outside it must stop the service starting. */
260
+ allowedModels: readonly string[];
261
+ fallback: boolean;
262
+ }
263
+
264
+ /**
265
+ * Where the engine writes operational detail.
266
+ *
267
+ * IT IS AN INJECTED SEAM SO A TEST CAN CAPTURE IT. The confidentiality
268
+ * guarantee breaks on the failure path — `startsim-bfem` names the live
269
+ * precedent, a Django handler that logs the model's raw response on a
270
+ * JSON-parse failure — so "no client text reaches a log" has to be an
271
+ * assertion, not a code-review habit.
272
+ */
273
+ export interface TranslationLogger {
274
+ debug(message: string, meta?: Record<string, unknown>): void;
275
+ info(message: string, meta?: Record<string, unknown>): void;
276
+ warn(message: string, meta?: Record<string, unknown>): void;
277
+ error(message: string, meta?: Record<string, unknown>): void;
278
+ }
279
+
280
+ export interface TranslationDeps {
281
+ provider: ILLMProvider;
282
+ logger?: TranslationLogger;
283
+ limits?: BatchLimits;
284
+ /** Attempts at a malformed envelope before the batch fails. */
285
+ maxEnvelopeRetries?: number;
286
+ }
287
+
288
+ export interface TranslationService {
289
+ translate(request: TranslationRequest): Promise<TranslationResult>;
290
+ readonly route: TranslationRoute;
291
+ }
292
+
293
+ /** Context the batch prompt is rendered from. */
294
+ export interface PromptContext {
295
+ sourceLocale: Locale;
296
+ targetLocale: Locale;
297
+ glossary?: readonly GlossaryTerm[];
298
+ /** Labelled separately so the model can tell an EXAMPLE from a thing to translate. */
299
+ memoryExamples?: readonly MemoryUnit[];
300
+ }