@verbatra/sdk 0.2.1 → 0.3.0

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/dist/index.d.cts CHANGED
@@ -1,13 +1,43 @@
1
+ import { AnthropicModel, OpenAiModel, GeminiModel, ProviderNotice, TranslationProvider } from '@verbatra/ai-providers';
1
2
  import { z } from 'zod';
2
- import { ProviderNotice, TranslationProvider } from '@verbatra/ai-providers';
3
3
  import { AdapterRegistry } from '@verbatra/format-adapters';
4
4
 
5
5
  /**
6
- * The verbatra project configuration. Non-secret only: it carries no API key (the
7
- * provider reads its key from the environment), and unknown top-level keys are rejected
8
- * so a stray secret cannot hide in the config. Validated with zod at the boundary
9
- * regardless of where it was loaded from.
6
+ * The provider section of the config: a discriminated union over the provider id,
7
+ * reusing each provider's own exported config schema. There is no key field anywhere
8
+ * in this union. The provider reads its API key from the environment at construction.
9
+ *
10
+ * This union and the factory table below are co-located on purpose: adding a provider
11
+ * is a single edit here (one union variant plus one table entry), and the mapped-type
12
+ * table makes the two sets provably identical at compile time.
10
13
  */
14
+ declare const providerConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
15
+ id: z.ZodLiteral<"anthropic">;
16
+ options: z.ZodObject<{
17
+ model: z.ZodString;
18
+ maxTokens: z.ZodNumber;
19
+ }, z.core.$strict>;
20
+ }, z.core.$strip>, z.ZodObject<{
21
+ id: z.ZodLiteral<"openai">;
22
+ options: z.ZodObject<{
23
+ model: z.ZodString;
24
+ maxOutputTokens: z.ZodNumber;
25
+ }, z.core.$strict>;
26
+ }, z.core.$strip>, z.ZodObject<{
27
+ id: z.ZodLiteral<"gemini">;
28
+ options: z.ZodObject<{
29
+ model: z.ZodString;
30
+ maxOutputTokens: z.ZodNumber;
31
+ }, z.core.$strict>;
32
+ }, z.core.$strip>, z.ZodObject<{
33
+ id: z.ZodLiteral<"deepl">;
34
+ options: z.ZodObject<{
35
+ glossaryId: z.ZodOptional<z.ZodString>;
36
+ }, z.core.$strict>;
37
+ }, z.core.$strip>], "id">;
38
+ type ProviderConfig = z.infer<typeof providerConfigSchema>;
39
+ type ProviderId = ProviderConfig["id"];
40
+
11
41
  declare const verbatraConfigSchema: z.ZodObject<{
12
42
  sourceLocale: z.ZodString;
13
43
  targetLocales: z.ZodArray<z.ZodString>;
@@ -50,15 +80,106 @@ declare const verbatraConfigSchema: z.ZodObject<{
50
80
  informal: "informal";
51
81
  neutral: "neutral";
52
82
  }>>;
83
+ prune: z.ZodOptional<z.ZodBoolean>;
84
+ generatePlurals: z.ZodOptional<z.ZodBoolean>;
85
+ maxBatchSize: z.ZodOptional<z.ZodNumber>;
53
86
  }, z.core.$strict>;
54
87
  type VerbatraConfig = z.infer<typeof verbatraConfigSchema>;
55
88
 
89
+ /**
90
+ * The closed set of a provider's known model IDs: exactly the string literals its SDK
91
+ * model type ships, with the SDK's own open `string & {}` arm stripped out. Distributing
92
+ * over the union and dropping any arm the wide `string` extends leaves only the literals,
93
+ * so the authoring field offers and ACCEPTS only the selected provider's known models. A
94
+ * foreign or unknown model (for example a Claude model under `id: "gemini"`) is a type
95
+ * error at authoring time. This is a static authoring constraint only; the runtime schema
96
+ * stays `z.string().min(1)` and still accepts any non-empty string, so a brand-new model
97
+ * the installed SDK does not yet list is flagged in the editor but still runs.
98
+ */
99
+ type KnownModels<M extends string> = M extends string ? (string extends M ? never : M) : never;
100
+ type AuthoringVariant<Id extends ProviderId, M extends string> = Extract<ProviderConfig, {
101
+ id: Id;
102
+ }> extends infer Variant ? Variant extends {
103
+ options: {
104
+ model: string;
105
+ };
106
+ } ? Omit<Variant, "options"> & {
107
+ options: Omit<Variant["options"], "model"> & {
108
+ model: KnownModels<M>;
109
+ };
110
+ } : never : never;
111
+ /**
112
+ * The authoring view of one provider variant, keyed by id. LLM providers (anthropic,
113
+ * openai, gemini) restrict `options.model` to that provider's known model literals; DeepL
114
+ * has no model field and is carried through unchanged. Keying by id (rather than a flat
115
+ * union) lets {@link AuthoringConfigFor} select exactly one variant for a literal id, so
116
+ * the `options.model` site is one provider's literal set and never a multi-provider union.
117
+ */
118
+ type AuthoringProviderVariant = {
119
+ anthropic: AuthoringVariant<"anthropic", AnthropicModel>;
120
+ openai: AuthoringVariant<"openai", OpenAiModel>;
121
+ gemini: AuthoringVariant<"gemini", GeminiModel>;
122
+ deepl: Extract<ProviderConfig, {
123
+ id: "deepl";
124
+ }>;
125
+ };
126
+ /**
127
+ * The authoring view of the whole config for a given provider id. It is structurally a
128
+ * {@link VerbatraConfig} whose `provider` is the single authoring variant for `TId`.
129
+ *
130
+ * When `TId` is a single provider literal, `provider` is one concrete variant, so
131
+ * `options.model` is that provider's known model literals alone and not a union across
132
+ * providers. `defineConfig` declares one overload per provider parameterized on this type
133
+ * (`AuthoringConfigFor<"openai">` and so on), so overload resolution picks the variant from
134
+ * the `provider.id` literal. That, together with the closed {@link KnownModels} set, makes a
135
+ * foreign model (for example a Claude model under `id: "gemini"`) a type error and aims to
136
+ * keep editors with weaker discriminated-union narrowing (for example the JetBrains/WebStorm
137
+ * completion engine) offering only the selected provider's models, since each overload's
138
+ * parameter is already a single variant with no nested union to narrow. When `TId` defaults
139
+ * to `ProviderId`, `provider` is the full authoring union. Every value assignable here is
140
+ * assignable to {@link VerbatraConfig}, because a model literal is a subtype of `string`.
141
+ */
142
+ type AuthoringConfigFor<TId extends ProviderId = ProviderId> = Omit<VerbatraConfig, "provider"> & {
143
+ provider: AuthoringProviderVariant[TId];
144
+ };
145
+ /**
146
+ * The authoring view of the whole config across every provider (the `TId = ProviderId`
147
+ * case of {@link AuthoringConfigFor}): identical to {@link VerbatraConfig} except that
148
+ * `provider` offers per-provider model completions.
149
+ */
150
+ type AuthoringConfig = AuthoringConfigFor;
151
+
56
152
  /**
57
153
  * Identity helper for authoring a code-defined verbatra.config.ts. It returns its
58
154
  * argument unchanged; its only purpose is to give the author full type inference and
59
155
  * editor autocomplete on the config object.
156
+ *
157
+ * It is declared as one overload per provider id rather than a single generic. Each
158
+ * overload's parameter is the concrete single-provider authoring config
159
+ * ({@link AuthoringConfigFor}), so `provider.options.model` is already that one provider's
160
+ * known model literals, with no union across providers and no generic for an editor to
161
+ * infer. Overload resolution selects the matching overload from the `provider.id` literal,
162
+ * so the editor offers only the selected provider's models and a foreign or unknown model
163
+ * (for example a Claude model under `id: "gemini"`) is a type error. This is deliberately
164
+ * overload-based: a generic whose type parameter is inferred from a nested `provider.id`
165
+ * collapses correctly in tsserver but not in editors with weaker inference (notably the
166
+ * JetBrains/WebStorm completion engine), which then fall back to the full union and offer
167
+ * every provider's models. Concrete per-provider signatures avoid that inference step.
168
+ *
169
+ * The final overload accepts the full {@link AuthoringConfig} union, so a config whose
170
+ * provider id is not a single literal (for example a value typed as the union) still
171
+ * type-checks.
172
+ *
173
+ * The return type is the runtime {@link VerbatraConfig}. The model restriction is a static
174
+ * authoring constraint, not a runtime one: `loadConfig` still validates `model` as
175
+ * `z.string().min(1)`, so a model the installed provider SDK does not yet list is flagged
176
+ * in the editor but still runs.
60
177
  */
61
- declare function defineConfig(config: VerbatraConfig): VerbatraConfig;
178
+ declare function defineConfig(config: AuthoringConfigFor<"anthropic">): VerbatraConfig;
179
+ declare function defineConfig(config: AuthoringConfigFor<"openai">): VerbatraConfig;
180
+ declare function defineConfig(config: AuthoringConfigFor<"gemini">): VerbatraConfig;
181
+ declare function defineConfig(config: AuthoringConfigFor<"deepl">): VerbatraConfig;
182
+ declare function defineConfig(config: AuthoringConfig): VerbatraConfig;
62
183
 
63
184
  interface LoadConfigOptions {
64
185
  /** Directory to start the search from. Defaults to the current working directory. */
@@ -107,57 +228,21 @@ interface LoadConfigOptions {
107
228
  */
108
229
  declare function loadConfig(options?: LoadConfigOptions): Promise<VerbatraConfig>;
109
230
 
110
- /**
111
- * The provider section of the config: a discriminated union over the provider id,
112
- * reusing each provider's own exported config schema. There is no key field anywhere
113
- * in this union. The provider reads its API key from the environment at construction.
114
- *
115
- * This union and the factory table below are co-located on purpose: adding a provider
116
- * is a single edit here (one union variant plus one table entry), and the mapped-type
117
- * table makes the two sets provably identical at compile time.
118
- */
119
- declare const providerConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
120
- id: z.ZodLiteral<"anthropic">;
121
- options: z.ZodObject<{
122
- model: z.ZodString;
123
- maxTokens: z.ZodNumber;
124
- }, z.core.$strict>;
125
- }, z.core.$strip>, z.ZodObject<{
126
- id: z.ZodLiteral<"openai">;
127
- options: z.ZodObject<{
128
- model: z.ZodString;
129
- maxOutputTokens: z.ZodNumber;
130
- }, z.core.$strict>;
131
- }, z.core.$strip>, z.ZodObject<{
132
- id: z.ZodLiteral<"gemini">;
133
- options: z.ZodObject<{
134
- model: z.ZodString;
135
- maxOutputTokens: z.ZodNumber;
136
- }, z.core.$strict>;
137
- }, z.core.$strip>, z.ZodObject<{
138
- id: z.ZodLiteral<"deepl">;
139
- options: z.ZodObject<{
140
- glossaryId: z.ZodOptional<z.ZodString>;
141
- }, z.core.$strict>;
142
- }, z.core.$strip>], "id">;
143
- type ProviderConfig = z.infer<typeof providerConfigSchema>;
144
- type ProviderId = ProviderConfig["id"];
145
-
146
231
  /**
147
232
  * Structured, secret-free error codes for the SDK boundaries. A key never appears in
148
233
  * any message: provider/adapter/core errors are already secret-free, and the SDK never
149
234
  * reads or holds a key. Each names a distinct boundary:
150
235
  *
151
- * - `CONFIG_NOT_FOUND`:no config was found by search, or an explicit `configPath` does not exist
236
+ * - `CONFIG_NOT_FOUND`: no config was found by search, or an explicit `configPath` does not exist
152
237
  * (thrown by `loadConfig`).
153
- * - `CONFIG_INVALID`:a config was found but is unparseable or fails validation (thrown by `loadConfig`).
154
- * - `UNKNOWN_FORMAT`:no adapter is registered for the configured format (thrown by `translate`).
155
- * - `PROVIDER_CONSTRUCTION_FAILED`:the provider factory threw; wraps the provider's own error, including
238
+ * - `CONFIG_INVALID`: a config was found but is unparseable or fails validation (thrown by `loadConfig`).
239
+ * - `UNKNOWN_FORMAT`: no adapter is registered for the configured format (thrown by `translate`).
240
+ * - `PROVIDER_CONSTRUCTION_FAILED`: the provider factory threw; wraps the provider's own error, including
156
241
  * a missing `*_API_KEY` reported as `MISSING_API_KEY` (thrown by `translate`, non-dry-run only).
157
- * - `SOURCE_UNREADABLE`:the source locale file is absent (thrown by `translate`, and by `watch` at startup).
158
- * - `SOURCE_INVALID`:the source locale file could not be read or parsed; wraps the adapter read error
242
+ * - `SOURCE_UNREADABLE`: the source locale file is absent (thrown by `translate`, and by `watch` at startup).
243
+ * - `SOURCE_INVALID`: the source locale file could not be read or parsed; wraps the adapter read error
159
244
  * (thrown by `translate`).
160
- * - `LOCK_FILE_INVALID`:the lock-file is present but corrupt or oversized (thrown by `translate`).
245
+ * - `LOCK_FILE_INVALID`: the lock-file is present but corrupt or oversized (thrown by `translate`).
161
246
  * - `LOCALE_FAILED` (NOT thrown): the fallback `code` recorded on a failed `LocaleSummary` when a
162
247
  * per-locale failure carries no string code of its own. See the surfaced-not-thrown distinction on
163
248
  * `translate`.
@@ -174,6 +259,23 @@ declare class SdkError extends Error {
174
259
  constructor(code: SdkErrorCode, message: string);
175
260
  }
176
261
 
262
+ /** A stable code for an SDK-originated notice (not a provider notice). */
263
+ type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED";
264
+ /**
265
+ * A notice raised by the SDK itself (not a provider), structurally identical to a {@link ProviderNotice}
266
+ * so both share the {@link LocaleSummary.notices} channel. Carries only a stable code and a static,
267
+ * secret-free message; never a key value or translatable content. The SDK keeps its own codes here so
268
+ * the ai-providers `ProviderNoticeCode` union stays free of SDK concerns (the dependency arrow points
269
+ * sdk -> ai-providers, never the reverse).
270
+ */
271
+ interface SdkNotice {
272
+ /** The stable {@link SdkNoticeCode} for what the SDK is reporting. */
273
+ readonly code: SdkNoticeCode;
274
+ /** A static, safe description; never a key or translatable content. */
275
+ readonly message: string;
276
+ }
277
+ /** A notice on a locale summary: either a provider-emitted notice or an SDK-emitted one. */
278
+ type LocaleNotice = ProviderNotice | SdkNotice;
177
279
  /** Structured outcome for one target locale; surfaced as data on the run, never thrown. */
178
280
  interface LocaleSummary {
179
281
  /** The target locale this summary is for. */
@@ -187,14 +289,31 @@ interface LocaleSummary {
187
289
  readonly translated: readonly string[];
188
290
  /** Keys already up to date, left unchanged this run. */
189
291
  readonly unchanged: readonly string[];
190
- /** Target keys with no corresponding source key (candidates for removal). */
292
+ /** Target keys with no corresponding source key (candidates for removal). Reported regardless of pruning. */
191
293
  readonly orphaned: readonly string[];
294
+ /**
295
+ * Orphaned keys actually removed this run because pruning was on. In a dry-run with pruning on, the keys
296
+ * that WOULD be removed. Empty when pruning is off (the orphans then survive and are reported in
297
+ * `orphaned` only). A subset of `orphaned`; never includes a source-present key.
298
+ */
299
+ readonly pruned: readonly string[];
192
300
  /** Source keys flagged invalid-ICU that were skipped for translation this run. */
193
301
  readonly invalidIcuSource: readonly string[];
194
302
  /** Translated keys that failed the placeholder-integrity check and were withheld. */
195
303
  readonly integrityMismatches: readonly string[];
196
- /** Provider notices for this locale (e.g. DeepL graceful-degradation); empty for LLM providers. */
197
- readonly notices: readonly ProviderNotice[];
304
+ /**
305
+ * Plural-category keys verbatra SYNTHESIZED this run (for example a Polish `items_few` the source
306
+ * never supplied), kept distinct from {@link translated} because they were generated from the meaning
307
+ * of the source plural forms, not translated 1:1 from a source string. Empty unless plural generation
308
+ * was enabled and acted. In a dry-run this is empty (the provider is not called).
309
+ */
310
+ readonly generated: readonly string[];
311
+ /**
312
+ * Notices for this locale: provider notices (e.g. DeepL graceful-degradation) and SDK notices
313
+ * (e.g. a target language needing more CLDR plural categories than the source supplies). Empty when
314
+ * nothing was degraded or flagged.
315
+ */
316
+ readonly notices: readonly LocaleNotice[];
198
317
  /**
199
318
  * Present only when status is "failed": a structured, secret-free error. `code` is a PRESERVED string
200
319
  * (the underlying provider/adapter error's `code`, or `"LOCALE_FAILED"` as a fallback), intentionally
@@ -236,9 +355,11 @@ type BoundedBytesRead = {
236
355
  readonly kind: "too-large";
237
356
  };
238
357
  /**
239
- * The minimal file-system surface the SDK needs for the lock-file and for existence
240
- * checks. Injectable so tests stay deterministic; the format adapters do their own
241
- * file IO and are not routed through this seam.
358
+ * The minimal file-system surface the SDK needs: existence checks, the lock-file read/write,
359
+ * and the untrusted workbook bytes read/write for the export/import flow. Reads are bounded
360
+ * (see {@link readFileBounded} / {@link readBytesBounded}) and writes are atomic. Injectable so
361
+ * tests stay deterministic; the format adapters do their own file IO and are not routed through
362
+ * this seam.
242
363
  */
243
364
  interface SdkFs {
244
365
  /** Whether a readable file exists at the path. */
@@ -274,6 +395,19 @@ interface TranslateInput {
274
395
  readonly cwd?: string;
275
396
  /** When true, read + diff + report only: the provider is never constructed or called and nothing is written. */
276
397
  readonly dryRun?: boolean;
398
+ /**
399
+ * When true, remove orphaned keys (target keys absent from source) from the written file and the lock.
400
+ * Off by default. When set, takes precedence over the config's `prune` option for this run; when unset,
401
+ * the config's `prune` applies. Only `diff.orphaned` keys are ever removed; no other key is touched.
402
+ */
403
+ readonly prune?: boolean;
404
+ /**
405
+ * When true, synthesize the CLDR plural forms a richer target language requires but the source lacks
406
+ * (i18next-JSON + LLM providers only; every other case falls back to the existing warning). Off by
407
+ * default. When set, takes precedence over the config's `generatePlurals` option for this run; when
408
+ * unset, the config's `generatePlurals` applies.
409
+ */
410
+ readonly generatePlurals?: boolean;
277
411
  }
278
412
  /** Composition seam: inject a registry, a provider builder, and a file system for tests. */
279
413
  interface TranslateDeps {
@@ -298,7 +432,7 @@ interface TranslateDeps {
298
432
  * {@link SdkErrorCode}. DeepL notices, integrity mismatches, and invalid-ICU source keys likewise
299
433
  * surface on each `LocaleSummary`, never as throws.
300
434
  *
301
- * @param input - The validated config and run options (cwd, dryRun).
435
+ * @param input - The validated config and run options (cwd, dryRun, prune, generatePlurals).
302
436
  * @param deps - Optional composition seams (registry, provider builder, file system) for tests.
303
437
  * @returns A {@link RunSummary}: the per-locale {@link LocaleSummary}s and the succeeded/failed locale lists.
304
438
  * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
@@ -424,7 +558,7 @@ interface Watcher {
424
558
  }
425
559
  /** Builds a {@link Watcher} for the given paths; the seam production fills with chokidar. */
426
560
  type CreateWatcher = (paths: readonly string[]) => Watcher;
427
- /** The run a watch trigger performs: the slice-1 one-shot translate, unchanged. */
561
+ /** The run a watch trigger performs: the one-shot translate, unchanged. */
428
562
  type RunTranslate = (input: TranslateInput) => Promise<RunSummary>;
429
563
  /** The outcome of one run, surfaced to the caller; never carries a secret. */
430
564
  type WatchRunResult = {
@@ -512,4 +646,4 @@ interface WatchController {
512
646
  */
513
647
  declare function watch(input: WatchInput, deps?: WatchDeps): Promise<WatchController>;
514
648
 
515
- export { type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type ImportWorkbookDeps, type ImportWorkbookInput, type LoadConfigOptions, type LocaleSummary, type ProviderConfig, type ProviderId, type RunSummary, type RunTranslate, SdkError, type SdkErrorCode, type SdkFs, type TranslateDeps, type TranslateInput, type VerbatraConfig, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, defineConfig, exportWorkbook, importWorkbook, loadConfig, translate, verbatraConfigSchema, watch };
649
+ export { type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type ImportWorkbookDeps, type ImportWorkbookInput, type LoadConfigOptions, type LocaleNotice, type LocaleSummary, type ProviderConfig, type ProviderId, type RunSummary, type RunTranslate, SdkError, type SdkErrorCode, type SdkFs, type SdkNotice, type SdkNoticeCode, type TranslateDeps, type TranslateInput, type VerbatraConfig, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, defineConfig, exportWorkbook, importWorkbook, loadConfig, translate, verbatraConfigSchema, watch };