@verbatra/sdk 0.4.4 → 0.5.0-next.1

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.ts CHANGED
@@ -36,7 +36,7 @@ type TranslationEntry$1 = Readonly<z.infer<typeof translationEntrySchema$1>>;
36
36
  * Result of comparing source placeholders against translated-output placeholders as MULTISETS
37
37
  * (counts matter). Always returned (never thrown) for an ordinary mismatch.
38
38
  */
39
- interface PlaceholderIntegrityResult {
39
+ interface PlaceholderIntegrityResult$1 {
40
40
  /** True when nothing is missing or extra: the multisets match, regardless of order. */
41
41
  readonly matches: boolean;
42
42
  /** Occurrences present in source but absent from the translation, one per dropped occurrence. Sorted. */
@@ -57,6 +57,13 @@ type Tone = "formal" | "informal" | "neutral";
57
57
  * derives placeholders itself.
58
58
  */
59
59
  type PlaceholderExtractor = (value: string) => readonly string[];
60
+ /**
61
+ * Compares a source and translated value's placeholders directly, branch-aware, instead of
62
+ * independently extracting each side's flat placeholder list first. Supplied by the caller (the SDK, for
63
+ * a format whose adapter defines one, for example ICU plural/select) so it matches the entries' format;
64
+ * ai-providers never derives it itself and never parses a specific format's message syntax.
65
+ */
66
+ type PlaceholderComparator = (source: string, translated: string) => PlaceholderIntegrityResult$1;
60
67
  /**
61
68
  * A batch translation request. Format- and provider-neutral: it carries no prompt,
62
69
  * model, key, or other provider-specific field. The placeholder extractor is
@@ -75,6 +82,18 @@ interface TranslateRequest {
75
82
  readonly tone?: Tone;
76
83
  /** Mandatory placeholder extractor; the output integrity check runs against it. */
77
84
  readonly extractPlaceholders: PlaceholderExtractor;
85
+ /**
86
+ * Optional branch-aware placeholder comparator. When present, the output integrity check uses it
87
+ * instead of independently extracting each side's placeholders with {@link extractPlaceholders} and
88
+ * diffing the flat lists. Absent for a format with no plural/select sub-message structure.
89
+ */
90
+ readonly comparePlaceholders?: PlaceholderComparator;
91
+ /**
92
+ * Optional cancellation signal for this batch. When aborted, an in-flight provider call rejects
93
+ * with the abort, unwrapped, instead of a {@link ProviderError} (see `guardProviderCall`). Not a
94
+ * plain-data field: it is never validated by `requestDataSchema` and never sent to a provider.
95
+ */
96
+ readonly signal?: AbortSignal;
78
97
  }
79
98
  /** Token usage, when the provider reports it. Absent for providers without tokens (DeepL). */
80
99
  interface Usage {
@@ -86,7 +105,7 @@ interface TranslateResult {
86
105
  /** The translated value for each requested key. */
87
106
  readonly values: ReadonlyMap<string, string>;
88
107
  /** The placeholder-integrity outcome for each key (source vs translated placeholder sets). */
89
- readonly integrity: ReadonlyMap<string, PlaceholderIntegrityResult>;
108
+ readonly integrity: ReadonlyMap<string, PlaceholderIntegrityResult$1>;
90
109
  /** Token usage when the provider reports it; absent for token-less providers. */
91
110
  readonly usage?: Usage;
92
111
  }
@@ -187,8 +206,9 @@ type OpenAiModel = OpenAI.ChatModel;
187
206
 
188
207
  /**
189
208
  * The provider section of the config: a discriminated union over the provider id, reusing each
190
- * provider's own config schema. There is no key field anywhere in this union; the provider reads its
191
- * API key from the environment at construction.
209
+ * provider's own config schema. There is no key field anywhere in this union except
210
+ * openai-compatible's optional `apiKeyEnvVar`, which names an environment variable rather than carrying
211
+ * a key value; every provider still reads its actual key from the environment at construction.
192
212
  */
193
213
  declare const providerConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
194
214
  id: z.ZodLiteral<"anthropic">;
@@ -213,6 +233,14 @@ declare const providerConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
213
233
  options: z.ZodObject<{
214
234
  glossaryId: z.ZodOptional<z.ZodString>;
215
235
  }, z.core.$strict>;
236
+ }, z.core.$strip>, z.ZodObject<{
237
+ id: z.ZodLiteral<"openai-compatible">;
238
+ options: z.ZodObject<{
239
+ baseUrl: z.ZodURL;
240
+ model: z.ZodString;
241
+ maxOutputTokens: z.ZodNumber;
242
+ apiKeyEnvVar: z.ZodOptional<z.ZodString>;
243
+ }, z.core.$strict>;
216
244
  }, z.core.$strip>], "id">;
217
245
  type ProviderConfig = z.infer<typeof providerConfigSchema>;
218
246
  type ProviderId = ProviderConfig["id"];
@@ -260,8 +288,16 @@ declare const verbatraConfigSchema: z.ZodObject<{
260
288
  options: z.ZodObject<{
261
289
  glossaryId: z.ZodOptional<z.ZodString>;
262
290
  }, z.core.$strict>;
291
+ }, z.core.$strip>, z.ZodObject<{
292
+ id: z.ZodLiteral<"openai-compatible">;
293
+ options: z.ZodObject<{
294
+ baseUrl: z.ZodURL;
295
+ model: z.ZodString;
296
+ maxOutputTokens: z.ZodNumber;
297
+ apiKeyEnvVar: z.ZodOptional<z.ZodString>;
298
+ }, z.core.$strict>;
263
299
  }, z.core.$strip>], "id">;
264
- glossary: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
300
+ glossary: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodString>, z.ZodString]>>;
265
301
  tone: z.ZodOptional<z.ZodEnum<{
266
302
  formal: "formal";
267
303
  informal: "informal";
@@ -271,7 +307,22 @@ declare const verbatraConfigSchema: z.ZodObject<{
271
307
  generatePlurals: z.ZodOptional<z.ZodBoolean>;
272
308
  maxBatchSize: z.ZodOptional<z.ZodNumber>;
273
309
  }, z.core.$strict>;
274
- type VerbatraConfig = z.infer<typeof verbatraConfigSchema>;
310
+ /**
311
+ * The as-authored (or as-parsed) shape of the verbatra configuration, straight from the schema: `glossary`
312
+ * is still the union of an inline record or a file path. {@link defineConfig} and the authoring types are
313
+ * built on this; `loadConfig` accepts it as input and produces the resolved {@link VerbatraConfig}.
314
+ */
315
+ type VerbatraConfigInput = z.infer<typeof verbatraConfigSchema>;
316
+ /**
317
+ * The verbatra configuration after `loadConfig` has resolved it: identical to {@link VerbatraConfigInput}
318
+ * except `glossary`, which is always a plain record here. A glossary given as a file path is read,
319
+ * parsed, and validated by the loader before a `VerbatraConfig` is produced, so every downstream
320
+ * consumer (the translation flow, `watch`, the CLI) keeps receiving the same resolved shape it always
321
+ * did.
322
+ */
323
+ type VerbatraConfig = Omit<VerbatraConfigInput, "glossary"> & {
324
+ glossary?: Readonly<Record<string, string>>;
325
+ };
275
326
 
276
327
  type KnownModels<M extends string> = M extends string ? (string extends M ? never : M) : never;
277
328
  type AuthoringVariant<Id extends ProviderId, M extends string> = Extract<ProviderConfig, {
@@ -292,19 +343,24 @@ type AuthoringProviderVariant = {
292
343
  deepl: Extract<ProviderConfig, {
293
344
  id: "deepl";
294
345
  }>;
346
+ "openai-compatible": Extract<ProviderConfig, {
347
+ id: "openai-compatible";
348
+ }>;
295
349
  };
296
350
  /**
297
- * The authoring view of the whole config for a provider id: a {@link VerbatraConfig} whose `provider`
298
- * is that id's single authoring variant, so `options.model` offers only that provider's models. When
299
- * `TId` defaults to `ProviderId`, `provider` is the full authoring union. Every value here is assignable
300
- * to {@link VerbatraConfig}, since a model literal is a subtype of `string`.
351
+ * The authoring view of the whole config for a provider id: a {@link VerbatraConfigInput} whose
352
+ * `provider` is that id's single authoring variant, so `options.model` offers only that provider's
353
+ * models. `glossary` stays the as-authored union (inline record or file path); resolution happens only
354
+ * in `loadConfig`. When `TId` defaults to `ProviderId`, `provider` is the full authoring union. Every
355
+ * value here is assignable to {@link VerbatraConfigInput}, since a model literal is a subtype of
356
+ * `string`.
301
357
  */
302
- type AuthoringConfigFor<TId extends ProviderId = ProviderId> = Omit<VerbatraConfig, "provider"> & {
358
+ type AuthoringConfigFor<TId extends ProviderId = ProviderId> = Omit<VerbatraConfigInput, "provider"> & {
303
359
  provider: AuthoringProviderVariant[TId];
304
360
  };
305
361
  /**
306
362
  * The authoring view of the whole config across every provider (the `TId = ProviderId`
307
- * case of {@link AuthoringConfigFor}): identical to {@link VerbatraConfig} except that
363
+ * case of {@link AuthoringConfigFor}): identical to {@link VerbatraConfigInput} except that
308
364
  * `provider` offers per-provider model completions.
309
365
  */
310
366
  type AuthoringConfig = AuthoringConfigFor;
@@ -316,16 +372,66 @@ type AuthoringConfig = AuthoringConfigFor;
316
372
  *
317
373
  * The model restriction is a static authoring constraint only: `loadConfig` validates `model` as a
318
374
  * non-empty string, so a model the installed provider SDK does not yet list is flagged in the editor
319
- * but still runs.
375
+ * but still runs. Likewise, `glossary` accepts either an inline record or a file path here; `loadConfig`
376
+ * resolves a path to a record before the translation flow ever sees it.
320
377
  *
321
378
  * @param config - The verbatra configuration object.
322
- * @returns The same config, typed as {@link VerbatraConfig}.
379
+ * @returns The same config, typed as {@link VerbatraConfigInput}.
380
+ */
381
+ declare function defineConfig(config: AuthoringConfigFor<"anthropic">): VerbatraConfigInput;
382
+ declare function defineConfig(config: AuthoringConfigFor<"openai">): VerbatraConfigInput;
383
+ declare function defineConfig(config: AuthoringConfigFor<"gemini">): VerbatraConfigInput;
384
+ declare function defineConfig(config: AuthoringConfigFor<"deepl">): VerbatraConfigInput;
385
+ declare function defineConfig(config: AuthoringConfig): VerbatraConfigInput;
386
+
387
+ /** Outcome of a bounded read: the content, or why it could not be read in bounds. */
388
+ type BoundedFileRead = {
389
+ readonly kind: "ok";
390
+ readonly content: string;
391
+ } | {
392
+ readonly kind: "missing";
393
+ } | {
394
+ readonly kind: "too-large";
395
+ };
396
+ /** Outcome of a bounded binary read: the bytes, or why they could not be read in bounds. */
397
+ type BoundedBytesRead = {
398
+ readonly kind: "ok";
399
+ readonly bytes: Uint8Array;
400
+ } | {
401
+ readonly kind: "missing";
402
+ } | {
403
+ readonly kind: "too-large";
404
+ };
405
+ /**
406
+ * The minimal file-system surface the SDK needs. Reads are bounded and writes are atomic. Injectable so
407
+ * tests stay deterministic; the format adapters do their own file IO and bypass this seam.
323
408
  */
324
- declare function defineConfig(config: AuthoringConfigFor<"anthropic">): VerbatraConfig;
325
- declare function defineConfig(config: AuthoringConfigFor<"openai">): VerbatraConfig;
326
- declare function defineConfig(config: AuthoringConfigFor<"gemini">): VerbatraConfig;
327
- declare function defineConfig(config: AuthoringConfigFor<"deepl">): VerbatraConfig;
328
- declare function defineConfig(config: AuthoringConfig): VerbatraConfig;
409
+ interface SdkFs {
410
+ /** Whether a readable file exists at the path. */
411
+ fileExists(path: string): Promise<boolean>;
412
+ /**
413
+ * Read a file as UTF-8 through a single handle, bounded to maxBytes. TOCTOU-safe: the handle is
414
+ * fstat'd and the read never advances past the sized length, so swapping in a larger file cannot
415
+ * bypass the cap. A missing or unreadable path is "missing"; a file over the cap is "too-large".
416
+ */
417
+ readFileBounded(path: string, maxBytes: number): Promise<BoundedFileRead>;
418
+ /** Read a file as raw bytes with the same TOCTOU-safe, bounded discipline as {@link readFileBounded}. */
419
+ readBytesBounded(path: string, maxBytes: number): Promise<BoundedBytesRead>;
420
+ /** Write atomically: a temp file in the same directory, then rename over the target. */
421
+ writeFile(path: string, data: string): Promise<void>;
422
+ /** Write raw bytes atomically (temp file, then rename over the target). Used for the workbook. */
423
+ writeBytes(path: string, data: Uint8Array): Promise<void>;
424
+ }
425
+
426
+ /** Where a config's resolved glossary came from, recorded for later Studio consumption. */
427
+ type GlossaryProvenance = {
428
+ readonly source: "none";
429
+ } | {
430
+ readonly source: "inline";
431
+ } | {
432
+ readonly source: "file";
433
+ readonly path: string;
434
+ };
329
435
 
330
436
  interface LoadConfigOptions {
331
437
  /** Directory to start the search from. Defaults to the current working directory. */
@@ -342,7 +448,42 @@ interface LoadConfigOptions {
342
448
  * search, but `configOverride` takes precedence over it.
343
449
  */
344
450
  readonly configPath?: string;
451
+ /** The file-system seam a glossary file path is read through. Defaults to {@link defaultFs}. */
452
+ readonly fs?: SdkFs;
345
453
  }
454
+ /** Where the loaded config came from: an in-memory override, one explicit file, or a search result. */
455
+ type ConfigSource = {
456
+ readonly kind: "search" | "explicit";
457
+ readonly filepath: string;
458
+ } | {
459
+ readonly kind: "override";
460
+ };
461
+ /**
462
+ * The result of {@link loadConfigWithMeta}: the resolved config, where it was loaded from, and where its
463
+ * glossary came from. `filepath` (when present) and a `glossary` file path are always absolute.
464
+ */
465
+ interface LoadedConfig {
466
+ readonly config: VerbatraConfig;
467
+ readonly source: ConfigSource;
468
+ readonly glossary: GlossaryProvenance;
469
+ }
470
+ /**
471
+ * Load and validate the verbatra configuration, returning provenance alongside it: which source it was
472
+ * loaded from ({@link ConfigSource}), and whether its glossary is absent, inline, or resolved from a
473
+ * file ({@link GlossaryProvenance}). {@link loadConfig} is a thin wrapper that returns only the
474
+ * `config` field; use this directly when the provenance is needed (for example, to display it or to
475
+ * re-resolve a glossary file relative to the same base directory).
476
+ *
477
+ * @param options - Where/what to load: `cwd`, an in-memory `configOverride`, an explicit `configPath`,
478
+ * or a `fs` seam override.
479
+ * @returns The resolved {@link VerbatraConfig}, its {@link ConfigSource}, and its {@link GlossaryProvenance}.
480
+ * @throws {@link SdkError} `CONFIG_NOT_FOUND`: no config was found by search, or the explicit `configPath`
481
+ * does not exist.
482
+ * @throws {@link SdkError} `CONFIG_INVALID`: a config was found but is unparseable or fails validation, or
483
+ * its glossary file path could not be read as a valid, UTF-8, flat string record (a raw error never
484
+ * escapes).
485
+ */
486
+ declare function loadConfigWithMeta(options?: LoadConfigOptions): Promise<LoadedConfig>;
346
487
  /**
347
488
  * Load and validate the verbatra configuration. Supports a code-defined
348
489
  * verbatra.config.ts and file-based configs (.verbatrarc.json/.yaml, package.json
@@ -352,12 +493,17 @@ interface LoadConfigOptions {
352
493
  *
353
494
  * Precedence: `configOverride` (validate in-memory) > `configPath` (load one explicit file) > search.
354
495
  *
496
+ * A `glossary` given as a file path is read, parsed, and validated at load time (see
497
+ * {@link loadConfigWithMeta} for its provenance); a relative path resolves against the loaded config
498
+ * file's directory, or against `cwd` for a `configOverride`. Watch mode does not re-resolve a glossary
499
+ * file on later edits; restart to pick up a change.
500
+ *
355
501
  * @param options - Where/what to load: `cwd`, an in-memory `configOverride`, or an explicit `configPath`.
356
502
  * @returns The validated {@link VerbatraConfig}.
357
503
  * @throws {@link SdkError} `CONFIG_NOT_FOUND`: no config was found by search, or the explicit `configPath`
358
504
  * does not exist.
359
- * @throws {@link SdkError} `CONFIG_INVALID`: a config was found but is unparseable or fails validation
360
- * (a raw zod error never escapes).
505
+ * @throws {@link SdkError} `CONFIG_INVALID`: a config was found but is unparseable or fails validation, or
506
+ * its glossary file path could not be resolved (a raw error never escapes).
361
507
  * @example
362
508
  * ```ts
363
509
  * import { loadConfig, translate } from "@verbatra/sdk";
@@ -388,7 +534,8 @@ declare function loadConfig(options?: LoadConfigOptions): Promise<VerbatraConfig
388
534
  * - `SOURCE_UNREADABLE`: the source locale file is absent (thrown by `translate`, and by `watch` at startup).
389
535
  * - `SOURCE_INVALID`: the source locale file could not be read or parsed; wraps the adapter read error
390
536
  * (thrown by `translate`).
391
- * - `LOCK_FILE_INVALID`: the lock-file is present but corrupt or oversized (thrown by `translate`).
537
+ * - `LOCK_FILE_INVALID`: the lock-file is present but corrupt, oversized, or at an unsupported
538
+ * version (thrown by `translate`, `check`, `diff`, `exportWorkbook`, and `importWorkbook`).
392
539
  * - `LOCALE_FAILED` (NOT thrown): the fallback `code` recorded on a failed `LocaleSummary` when a
393
540
  * per-locale failure carries no string code of its own. See the surfaced-not-thrown distinction on
394
541
  * `translate`.
@@ -445,6 +592,21 @@ interface LocaleResource {
445
592
  readonly entries: ReadonlyMap<string, TranslationEntry>;
446
593
  }
447
594
 
595
+ /**
596
+ * Result of comparing source placeholders against translated-output placeholders as MULTISETS
597
+ * (counts matter). Always returned (never thrown) for an ordinary mismatch.
598
+ */
599
+ interface PlaceholderIntegrityResult {
600
+ /** True when nothing is missing or extra: the multisets match, regardless of order. */
601
+ readonly matches: boolean;
602
+ /** Occurrences present in source but absent from the translation, one per dropped occurrence. Sorted. */
603
+ readonly missing: readonly string[];
604
+ /** Occurrences present in the translation but absent from source, one per surplus occurrence. Sorted. */
605
+ readonly extra: readonly string[];
606
+ /** Same multiset present in both, but in a different order. */
607
+ readonly reordered: boolean;
608
+ }
609
+
448
610
  /**
449
611
  * The result of reading a file into core's intermediate representation.
450
612
  */
@@ -521,6 +683,18 @@ interface FormatAdapter {
521
683
  * unparseable value returns false.
522
684
  */
523
685
  validateMessage(value: string): boolean;
686
+ /**
687
+ * Optional branch-aware placeholder comparison for formats with plural/select sub-message structure.
688
+ * When present, callers use it instead of independently extracting each side's placeholders with
689
+ * {@link extractPlaceholders} and diffing the flat lists, since flattening a plural/select value loses
690
+ * which branch a placeholder came from. Absent for every non-ICU adapter, which is compared via
691
+ * `extractPlaceholders` plus `checkPlaceholders` as before.
692
+ *
693
+ * @param sourceValue - The source value.
694
+ * @param targetValue - The translated value to check against it.
695
+ * @returns The merged placeholder-integrity result across every branch. Does not throw.
696
+ */
697
+ comparePlaceholders?(sourceValue: string, targetValue: string): PlaceholderIntegrityResult;
524
698
  }
525
699
 
526
700
  /** Outcome of resolving an adapter for a file. Structured; never throws. */
@@ -570,45 +744,6 @@ declare class AdapterRegistry {
570
744
  resolve(filePath: string, options?: ResolveOptions): AdapterResolution;
571
745
  }
572
746
 
573
- /** Outcome of a bounded read: the content, or why it could not be read in bounds. */
574
- type BoundedFileRead = {
575
- readonly kind: "ok";
576
- readonly content: string;
577
- } | {
578
- readonly kind: "missing";
579
- } | {
580
- readonly kind: "too-large";
581
- };
582
- /** Outcome of a bounded binary read: the bytes, or why they could not be read in bounds. */
583
- type BoundedBytesRead = {
584
- readonly kind: "ok";
585
- readonly bytes: Uint8Array;
586
- } | {
587
- readonly kind: "missing";
588
- } | {
589
- readonly kind: "too-large";
590
- };
591
- /**
592
- * The minimal file-system surface the SDK needs. Reads are bounded and writes are atomic. Injectable so
593
- * tests stay deterministic; the format adapters do their own file IO and bypass this seam.
594
- */
595
- interface SdkFs {
596
- /** Whether a readable file exists at the path. */
597
- fileExists(path: string): Promise<boolean>;
598
- /**
599
- * Read a file as UTF-8 through a single handle, bounded to maxBytes. TOCTOU-safe: the handle is
600
- * fstat'd and the read never advances past the sized length, so swapping in a larger file cannot
601
- * bypass the cap. A missing or unreadable path is "missing"; a file over the cap is "too-large".
602
- */
603
- readFileBounded(path: string, maxBytes: number): Promise<BoundedFileRead>;
604
- /** Read a file as raw bytes with the same TOCTOU-safe, bounded discipline as {@link readFileBounded}. */
605
- readBytesBounded(path: string, maxBytes: number): Promise<BoundedBytesRead>;
606
- /** Write atomically: a temp file in the same directory, then rename over the target. */
607
- writeFile(path: string, data: string): Promise<void>;
608
- /** Write raw bytes atomically (temp file, then rename over the target). Used for the workbook. */
609
- writeBytes(path: string, data: Uint8Array): Promise<void>;
610
- }
611
-
612
747
  /** Per-locale drift status: counts only, no key lists. */
613
748
  interface LocaleCheckSummary {
614
749
  /** The target locale this entry reports on. */
@@ -708,6 +843,61 @@ interface DiffDeps {
708
843
  */
709
844
  declare function diff(input: DiffInput, deps?: DiffDeps): Promise<DiffSummary>;
710
845
 
846
+ /** Per-locale key count and drift against the source and target files, from the recorded lock baseline. */
847
+ interface LockLocaleState {
848
+ /** The target locale this entry reports on. */
849
+ readonly locale: string;
850
+ /** Number of keys the lock records a baseline for, in this locale. */
851
+ readonly keyCount: number;
852
+ /** Keys present in source but absent from the target. */
853
+ readonly missing: number;
854
+ /** Keys whose source changed since the target was last translated. */
855
+ readonly stale: number;
856
+ /** Keys whose recorded baseline still matches the source. */
857
+ readonly upToDate: number;
858
+ }
859
+ /**
860
+ * The lock-file's existence, version, and per-locale drift. `exists` is the outcome of an explicit
861
+ * probe of the lock-file path, never inferred from {@link readLockFile}'s missing-file degrade, so
862
+ * "no lock-file yet" and "an empty but present lock-file" stay distinguishable.
863
+ */
864
+ type LockStateResult = {
865
+ readonly exists: false;
866
+ } | {
867
+ readonly exists: true;
868
+ readonly version: number;
869
+ readonly locales: readonly LockLocaleState[];
870
+ };
871
+ /** Input for {@link lockState}: the validated config and which locales to report on. */
872
+ interface LockStateInput {
873
+ /** The validated configuration (typically from {@link loadConfig}). */
874
+ readonly config: VerbatraConfig;
875
+ /** Directory the file pattern and lock-file resolve against; defaults to cwd. */
876
+ readonly cwd?: string;
877
+ /** Subset of target locales to report on; defaults to all configured target locales. */
878
+ readonly locales?: readonly string[];
879
+ }
880
+ /** Composition seam for {@link lockState}: inject a registry and a file system for tests. */
881
+ interface LockStateDeps {
882
+ readonly adapterRegistry?: AdapterRegistry;
883
+ readonly fs?: SdkFs;
884
+ }
885
+ /**
886
+ * Report the lock-file's existence, version, and per-locale drift (key count from the recorded
887
+ * baseline, plus missing, stale, and up-to-date counts against the current source and target),
888
+ * without calling a provider, writing any file, or mutating the lock. `exists` comes from an
889
+ * explicit probe of the lock-file path: when it is absent, the result is `{ exists: false }` and
890
+ * no source or target file is read, since there is no recorded baseline to report drift against.
891
+ *
892
+ * @param input - The validated config and which locales to report on.
893
+ * @param deps - Optional composition seams (registry, file system) for tests.
894
+ * @returns The lock-file's existence and, when present, its version and per-locale drift.
895
+ * @throws {@link SdkError} `UNKNOWN_LOCALE` when a requested locale is not among the configured
896
+ * target locales; when the lock-file exists, also `UNKNOWN_FORMAT`, `SOURCE_UNREADABLE`,
897
+ * `SOURCE_INVALID`, or `LOCK_FILE_INVALID` with the same meanings as in `translate`.
898
+ */
899
+ declare function lockState(input: LockStateInput, deps?: LockStateDeps): Promise<LockStateResult>;
900
+
711
901
  /** A stable code for an SDK-originated notice (not a provider notice). */
712
902
  type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED";
713
903
  /**
@@ -956,6 +1146,53 @@ interface ImportWorkbookDeps {
956
1146
  */
957
1147
  declare function importWorkbook(input: ImportWorkbookInput, deps?: ImportWorkbookDeps): Promise<RunSummary>;
958
1148
 
1149
+ /**
1150
+ * The lock-file shape: for each target locale, a map of key to the source content hash from which that
1151
+ * target key was last translated. The baseline core's diff consumes to detect changed source strings.
1152
+ */
1153
+ interface LockFile {
1154
+ readonly version: number;
1155
+ readonly locales: Readonly<Record<string, Readonly<Record<string, string>>>>;
1156
+ }
1157
+
1158
+ /** Input for {@link loadLockFile}: the directory the lock-file resolves against. */
1159
+ interface LoadLockFileInput {
1160
+ /** Directory the lock-file resolves against; defaults to cwd. */
1161
+ readonly cwd?: string;
1162
+ }
1163
+ /** Composition seam for {@link loadLockFile}: inject a file system for tests. */
1164
+ interface LoadLockFileDeps {
1165
+ readonly fs?: SdkFs;
1166
+ }
1167
+ /**
1168
+ * Read the project's lock-file: a thin public wrapper around the internal lock-file reader,
1169
+ * following the same input/deps convention as {@link check}, {@link diff}, and {@link lockState}.
1170
+ * A missing lock-file degrades to an empty lock (first-run), the same behavior `translate` and
1171
+ * `watch` rely on internally. Callers that need to distinguish "no lock-file yet" from "an empty
1172
+ * but present lock-file" should use {@link lockState} instead, which probes the file's existence
1173
+ * explicitly.
1174
+ *
1175
+ * @param input - The directory the lock-file resolves against.
1176
+ * @param deps - Optional composition seam (file system) for tests.
1177
+ * @returns The parsed lock-file, or an empty lock when none exists yet.
1178
+ * @throws {@link SdkError} `LOCK_FILE_INVALID` when the lock-file is present but corrupt, oversized,
1179
+ * or at an unsupported version.
1180
+ */
1181
+ declare function loadLockFile(input?: LoadLockFileInput, deps?: LoadLockFileDeps): Promise<LockFile>;
1182
+
1183
+ /** The committed lock-file name, chosen to be obviously JSON and to not match a `*.lock` ignore rule. */
1184
+ declare const LOCK_FILE_NAME = "verbatra.lock.json";
1185
+
1186
+ /**
1187
+ * The subset of {@link ProviderId} that `init` can scaffold: every provider with a single required
1188
+ * environment variable. "openai-compatible" is deliberately excluded: unlike every other provider it
1189
+ * has no single required environment variable, only a three-tier
1190
+ * apiKeyEnvVar/OPENAI_COMPATIBLE_API_KEY/placeholder fallback (see resolveOpenAiCompatibleKey in
1191
+ * @verbatra/ai-providers), so it does not fit the `providerEnv` table below and `init` scaffolding does
1192
+ * not offer it. This is the single source of truth for the exclusion; other packages import this type
1193
+ * rather than re-deriving `Exclude<ProviderId, "openai-compatible">` themselves.
1194
+ */
1195
+ type ScaffoldableProviderId = Exclude<ProviderId, "openai-compatible">;
959
1196
  /**
960
1197
  * Read-only metadata the CLI `init` scaffold derives its tables from, so the CLI never restates the
961
1198
  * provider, env-var, model, or format truth owned by core and ai-providers.
@@ -1070,4 +1307,4 @@ interface WatchController {
1070
1307
  */
1071
1308
  declare function watch(input: WatchInput, deps?: WatchDeps): Promise<WatchController>;
1072
1309
 
1073
- export { type CheckDeps, type CheckInput, type CheckSummary, type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type DiffDeps, type DiffInput, type DiffSummary, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type ImportWorkbookDeps, type ImportWorkbookInput, type LoadConfigOptions, type LocaleCheckSummary, type LocaleDiff, type LocaleNotice, type LocaleSummary, type ProviderConfig, type ProviderId, type RunSummary, type RunTranslate, SdkError, type SdkErrorCode, type SdkFs, type SdkNotice, type SdkNoticeCode, type SupportedFormat$1 as SupportedFormat, type TranslateDeps, type TranslateInput, type VerbatraConfig, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, check, defineConfig, diff, exportWorkbook, importWorkbook, loadConfig, scaffoldingMetadata, translate, verbatraConfigSchema, watch };
1310
+ export { type CheckDeps, type CheckInput, type CheckSummary, type ConfigSource, type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type DiffDeps, type DiffInput, type DiffSummary, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type GlossaryProvenance, type ImportWorkbookDeps, type ImportWorkbookInput, LOCK_FILE_NAME, type LoadConfigOptions, type LoadLockFileDeps, type LoadLockFileInput, type LoadedConfig, type LocaleCheckSummary, type LocaleDiff, type LocaleNotice, type LocaleSummary, type LockFile, type LockLocaleState, type LockStateDeps, type LockStateInput, type LockStateResult, type ProviderConfig, type ProviderId, type RunSummary, type RunTranslate, type ScaffoldableProviderId, SdkError, type SdkErrorCode, type SdkFs, type SdkNotice, type SdkNoticeCode, type SupportedFormat$1 as SupportedFormat, type TranslateDeps, type TranslateInput, type VerbatraConfig, type VerbatraConfigInput, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, check, defineConfig, diff, exportWorkbook, importWorkbook, loadConfig, loadConfigWithMeta, loadLockFile, lockState, scaffoldingMetadata, translate, verbatraConfigSchema, watch };