@verbatra/sdk 0.4.4 → 0.5.0-next.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
@@ -261,7 +261,7 @@ declare const verbatraConfigSchema: z.ZodObject<{
261
261
  glossaryId: z.ZodOptional<z.ZodString>;
262
262
  }, z.core.$strict>;
263
263
  }, z.core.$strip>], "id">;
264
- glossary: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
264
+ glossary: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodString>, z.ZodString]>>;
265
265
  tone: z.ZodOptional<z.ZodEnum<{
266
266
  formal: "formal";
267
267
  informal: "informal";
@@ -271,7 +271,22 @@ declare const verbatraConfigSchema: z.ZodObject<{
271
271
  generatePlurals: z.ZodOptional<z.ZodBoolean>;
272
272
  maxBatchSize: z.ZodOptional<z.ZodNumber>;
273
273
  }, z.core.$strict>;
274
- type VerbatraConfig = z.infer<typeof verbatraConfigSchema>;
274
+ /**
275
+ * The as-authored (or as-parsed) shape of the verbatra configuration, straight from the schema: `glossary`
276
+ * is still the union of an inline record or a file path. {@link defineConfig} and the authoring types are
277
+ * built on this; `loadConfig` accepts it as input and produces the resolved {@link VerbatraConfig}.
278
+ */
279
+ type VerbatraConfigInput = z.infer<typeof verbatraConfigSchema>;
280
+ /**
281
+ * The verbatra configuration after `loadConfig` has resolved it: identical to {@link VerbatraConfigInput}
282
+ * except `glossary`, which is always a plain record here. A glossary given as a file path is read,
283
+ * parsed, and validated by the loader before a `VerbatraConfig` is produced, so every downstream
284
+ * consumer (the translation flow, `watch`, the CLI) keeps receiving the same resolved shape it always
285
+ * did.
286
+ */
287
+ type VerbatraConfig = Omit<VerbatraConfigInput, "glossary"> & {
288
+ glossary?: Readonly<Record<string, string>>;
289
+ };
275
290
 
276
291
  type KnownModels<M extends string> = M extends string ? (string extends M ? never : M) : never;
277
292
  type AuthoringVariant<Id extends ProviderId, M extends string> = Extract<ProviderConfig, {
@@ -294,17 +309,19 @@ type AuthoringProviderVariant = {
294
309
  }>;
295
310
  };
296
311
  /**
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`.
312
+ * The authoring view of the whole config for a provider id: a {@link VerbatraConfigInput} whose
313
+ * `provider` is that id's single authoring variant, so `options.model` offers only that provider's
314
+ * models. `glossary` stays the as-authored union (inline record or file path); resolution happens only
315
+ * in `loadConfig`. When `TId` defaults to `ProviderId`, `provider` is the full authoring union. Every
316
+ * value here is assignable to {@link VerbatraConfigInput}, since a model literal is a subtype of
317
+ * `string`.
301
318
  */
302
- type AuthoringConfigFor<TId extends ProviderId = ProviderId> = Omit<VerbatraConfig, "provider"> & {
319
+ type AuthoringConfigFor<TId extends ProviderId = ProviderId> = Omit<VerbatraConfigInput, "provider"> & {
303
320
  provider: AuthoringProviderVariant[TId];
304
321
  };
305
322
  /**
306
323
  * The authoring view of the whole config across every provider (the `TId = ProviderId`
307
- * case of {@link AuthoringConfigFor}): identical to {@link VerbatraConfig} except that
324
+ * case of {@link AuthoringConfigFor}): identical to {@link VerbatraConfigInput} except that
308
325
  * `provider` offers per-provider model completions.
309
326
  */
310
327
  type AuthoringConfig = AuthoringConfigFor;
@@ -316,16 +333,66 @@ type AuthoringConfig = AuthoringConfigFor;
316
333
  *
317
334
  * The model restriction is a static authoring constraint only: `loadConfig` validates `model` as a
318
335
  * non-empty string, so a model the installed provider SDK does not yet list is flagged in the editor
319
- * but still runs.
336
+ * but still runs. Likewise, `glossary` accepts either an inline record or a file path here; `loadConfig`
337
+ * resolves a path to a record before the translation flow ever sees it.
320
338
  *
321
339
  * @param config - The verbatra configuration object.
322
- * @returns The same config, typed as {@link VerbatraConfig}.
340
+ * @returns The same config, typed as {@link VerbatraConfigInput}.
323
341
  */
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;
342
+ declare function defineConfig(config: AuthoringConfigFor<"anthropic">): VerbatraConfigInput;
343
+ declare function defineConfig(config: AuthoringConfigFor<"openai">): VerbatraConfigInput;
344
+ declare function defineConfig(config: AuthoringConfigFor<"gemini">): VerbatraConfigInput;
345
+ declare function defineConfig(config: AuthoringConfigFor<"deepl">): VerbatraConfigInput;
346
+ declare function defineConfig(config: AuthoringConfig): VerbatraConfigInput;
347
+
348
+ /** Outcome of a bounded read: the content, or why it could not be read in bounds. */
349
+ type BoundedFileRead = {
350
+ readonly kind: "ok";
351
+ readonly content: string;
352
+ } | {
353
+ readonly kind: "missing";
354
+ } | {
355
+ readonly kind: "too-large";
356
+ };
357
+ /** Outcome of a bounded binary read: the bytes, or why they could not be read in bounds. */
358
+ type BoundedBytesRead = {
359
+ readonly kind: "ok";
360
+ readonly bytes: Uint8Array;
361
+ } | {
362
+ readonly kind: "missing";
363
+ } | {
364
+ readonly kind: "too-large";
365
+ };
366
+ /**
367
+ * The minimal file-system surface the SDK needs. Reads are bounded and writes are atomic. Injectable so
368
+ * tests stay deterministic; the format adapters do their own file IO and bypass this seam.
369
+ */
370
+ interface SdkFs {
371
+ /** Whether a readable file exists at the path. */
372
+ fileExists(path: string): Promise<boolean>;
373
+ /**
374
+ * Read a file as UTF-8 through a single handle, bounded to maxBytes. TOCTOU-safe: the handle is
375
+ * fstat'd and the read never advances past the sized length, so swapping in a larger file cannot
376
+ * bypass the cap. A missing or unreadable path is "missing"; a file over the cap is "too-large".
377
+ */
378
+ readFileBounded(path: string, maxBytes: number): Promise<BoundedFileRead>;
379
+ /** Read a file as raw bytes with the same TOCTOU-safe, bounded discipline as {@link readFileBounded}. */
380
+ readBytesBounded(path: string, maxBytes: number): Promise<BoundedBytesRead>;
381
+ /** Write atomically: a temp file in the same directory, then rename over the target. */
382
+ writeFile(path: string, data: string): Promise<void>;
383
+ /** Write raw bytes atomically (temp file, then rename over the target). Used for the workbook. */
384
+ writeBytes(path: string, data: Uint8Array): Promise<void>;
385
+ }
386
+
387
+ /** Where a config's resolved glossary came from, recorded for later Studio consumption. */
388
+ type GlossaryProvenance = {
389
+ readonly source: "none";
390
+ } | {
391
+ readonly source: "inline";
392
+ } | {
393
+ readonly source: "file";
394
+ readonly path: string;
395
+ };
329
396
 
330
397
  interface LoadConfigOptions {
331
398
  /** Directory to start the search from. Defaults to the current working directory. */
@@ -342,7 +409,42 @@ interface LoadConfigOptions {
342
409
  * search, but `configOverride` takes precedence over it.
343
410
  */
344
411
  readonly configPath?: string;
412
+ /** The file-system seam a glossary file path is read through. Defaults to {@link defaultFs}. */
413
+ readonly fs?: SdkFs;
345
414
  }
415
+ /** Where the loaded config came from: an in-memory override, one explicit file, or a search result. */
416
+ type ConfigSource = {
417
+ readonly kind: "search" | "explicit";
418
+ readonly filepath: string;
419
+ } | {
420
+ readonly kind: "override";
421
+ };
422
+ /**
423
+ * The result of {@link loadConfigWithMeta}: the resolved config, where it was loaded from, and where its
424
+ * glossary came from. `filepath` (when present) and a `glossary` file path are always absolute.
425
+ */
426
+ interface LoadedConfig {
427
+ readonly config: VerbatraConfig;
428
+ readonly source: ConfigSource;
429
+ readonly glossary: GlossaryProvenance;
430
+ }
431
+ /**
432
+ * Load and validate the verbatra configuration, returning provenance alongside it: which source it was
433
+ * loaded from ({@link ConfigSource}), and whether its glossary is absent, inline, or resolved from a
434
+ * file ({@link GlossaryProvenance}). {@link loadConfig} is a thin wrapper that returns only the
435
+ * `config` field; use this directly when the provenance is needed (for example, to display it or to
436
+ * re-resolve a glossary file relative to the same base directory).
437
+ *
438
+ * @param options - Where/what to load: `cwd`, an in-memory `configOverride`, an explicit `configPath`,
439
+ * or a `fs` seam override.
440
+ * @returns The resolved {@link VerbatraConfig}, its {@link ConfigSource}, and its {@link GlossaryProvenance}.
441
+ * @throws {@link SdkError} `CONFIG_NOT_FOUND`: no config was found by search, or the explicit `configPath`
442
+ * does not exist.
443
+ * @throws {@link SdkError} `CONFIG_INVALID`: a config was found but is unparseable or fails validation, or
444
+ * its glossary file path could not be read as a valid, UTF-8, flat string record (a raw error never
445
+ * escapes).
446
+ */
447
+ declare function loadConfigWithMeta(options?: LoadConfigOptions): Promise<LoadedConfig>;
346
448
  /**
347
449
  * Load and validate the verbatra configuration. Supports a code-defined
348
450
  * verbatra.config.ts and file-based configs (.verbatrarc.json/.yaml, package.json
@@ -352,12 +454,17 @@ interface LoadConfigOptions {
352
454
  *
353
455
  * Precedence: `configOverride` (validate in-memory) > `configPath` (load one explicit file) > search.
354
456
  *
457
+ * A `glossary` given as a file path is read, parsed, and validated at load time (see
458
+ * {@link loadConfigWithMeta} for its provenance); a relative path resolves against the loaded config
459
+ * file's directory, or against `cwd` for a `configOverride`. Watch mode does not re-resolve a glossary
460
+ * file on later edits; restart to pick up a change.
461
+ *
355
462
  * @param options - Where/what to load: `cwd`, an in-memory `configOverride`, or an explicit `configPath`.
356
463
  * @returns The validated {@link VerbatraConfig}.
357
464
  * @throws {@link SdkError} `CONFIG_NOT_FOUND`: no config was found by search, or the explicit `configPath`
358
465
  * 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).
466
+ * @throws {@link SdkError} `CONFIG_INVALID`: a config was found but is unparseable or fails validation, or
467
+ * its glossary file path could not be resolved (a raw error never escapes).
361
468
  * @example
362
469
  * ```ts
363
470
  * import { loadConfig, translate } from "@verbatra/sdk";
@@ -570,45 +677,6 @@ declare class AdapterRegistry {
570
677
  resolve(filePath: string, options?: ResolveOptions): AdapterResolution;
571
678
  }
572
679
 
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
680
  /** Per-locale drift status: counts only, no key lists. */
613
681
  interface LocaleCheckSummary {
614
682
  /** The target locale this entry reports on. */
@@ -708,6 +776,61 @@ interface DiffDeps {
708
776
  */
709
777
  declare function diff(input: DiffInput, deps?: DiffDeps): Promise<DiffSummary>;
710
778
 
779
+ /** Per-locale key count and drift against the source and target files, from the recorded lock baseline. */
780
+ interface LockLocaleState {
781
+ /** The target locale this entry reports on. */
782
+ readonly locale: string;
783
+ /** Number of keys the lock records a baseline for, in this locale. */
784
+ readonly keyCount: number;
785
+ /** Keys present in source but absent from the target. */
786
+ readonly missing: number;
787
+ /** Keys whose source changed since the target was last translated. */
788
+ readonly stale: number;
789
+ /** Keys whose recorded baseline still matches the source. */
790
+ readonly upToDate: number;
791
+ }
792
+ /**
793
+ * The lock-file's existence, version, and per-locale drift. `exists` is the outcome of an explicit
794
+ * probe of the lock-file path, never inferred from {@link readLockFile}'s missing-file degrade, so
795
+ * "no lock-file yet" and "an empty but present lock-file" stay distinguishable.
796
+ */
797
+ type LockStateResult = {
798
+ readonly exists: false;
799
+ } | {
800
+ readonly exists: true;
801
+ readonly version: number;
802
+ readonly locales: readonly LockLocaleState[];
803
+ };
804
+ /** Input for {@link lockState}: the validated config and which locales to report on. */
805
+ interface LockStateInput {
806
+ /** The validated configuration (typically from {@link loadConfig}). */
807
+ readonly config: VerbatraConfig;
808
+ /** Directory the file pattern and lock-file resolve against; defaults to cwd. */
809
+ readonly cwd?: string;
810
+ /** Subset of target locales to report on; defaults to all configured target locales. */
811
+ readonly locales?: readonly string[];
812
+ }
813
+ /** Composition seam for {@link lockState}: inject a registry and a file system for tests. */
814
+ interface LockStateDeps {
815
+ readonly adapterRegistry?: AdapterRegistry;
816
+ readonly fs?: SdkFs;
817
+ }
818
+ /**
819
+ * Report the lock-file's existence, version, and per-locale drift (key count from the recorded
820
+ * baseline, plus missing, stale, and up-to-date counts against the current source and target),
821
+ * without calling a provider, writing any file, or mutating the lock. `exists` comes from an
822
+ * explicit probe of the lock-file path: when it is absent, the result is `{ exists: false }` and
823
+ * no source or target file is read, since there is no recorded baseline to report drift against.
824
+ *
825
+ * @param input - The validated config and which locales to report on.
826
+ * @param deps - Optional composition seams (registry, file system) for tests.
827
+ * @returns The lock-file's existence and, when present, its version and per-locale drift.
828
+ * @throws {@link SdkError} `UNKNOWN_LOCALE` when a requested locale is not among the configured
829
+ * target locales; when the lock-file exists, also `UNKNOWN_FORMAT`, `SOURCE_UNREADABLE`,
830
+ * `SOURCE_INVALID`, or `LOCK_FILE_INVALID` with the same meanings as in `translate`.
831
+ */
832
+ declare function lockState(input: LockStateInput, deps?: LockStateDeps): Promise<LockStateResult>;
833
+
711
834
  /** A stable code for an SDK-originated notice (not a provider notice). */
712
835
  type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED";
713
836
  /**
@@ -956,6 +1079,42 @@ interface ImportWorkbookDeps {
956
1079
  */
957
1080
  declare function importWorkbook(input: ImportWorkbookInput, deps?: ImportWorkbookDeps): Promise<RunSummary>;
958
1081
 
1082
+ /**
1083
+ * The lock-file shape: for each target locale, a map of key to the source content hash from which that
1084
+ * target key was last translated. The baseline core's diff consumes to detect changed source strings.
1085
+ */
1086
+ interface LockFile {
1087
+ readonly version: number;
1088
+ readonly locales: Readonly<Record<string, Readonly<Record<string, string>>>>;
1089
+ }
1090
+
1091
+ /** Input for {@link loadLockFile}: the directory the lock-file resolves against. */
1092
+ interface LoadLockFileInput {
1093
+ /** Directory the lock-file resolves against; defaults to cwd. */
1094
+ readonly cwd?: string;
1095
+ }
1096
+ /** Composition seam for {@link loadLockFile}: inject a file system for tests. */
1097
+ interface LoadLockFileDeps {
1098
+ readonly fs?: SdkFs;
1099
+ }
1100
+ /**
1101
+ * Read the project's lock-file: a thin public wrapper around the internal lock-file reader,
1102
+ * following the same input/deps convention as {@link check}, {@link diff}, and {@link lockState}.
1103
+ * A missing lock-file degrades to an empty lock (first-run), the same behavior `translate` and
1104
+ * `watch` rely on internally. Callers that need to distinguish "no lock-file yet" from "an empty
1105
+ * but present lock-file" should use {@link lockState} instead, which probes the file's existence
1106
+ * explicitly.
1107
+ *
1108
+ * @param input - The directory the lock-file resolves against.
1109
+ * @param deps - Optional composition seam (file system) for tests.
1110
+ * @returns The parsed lock-file, or an empty lock when none exists yet.
1111
+ * @throws {@link SdkError} `LOCK_FILE_INVALID` when the lock-file is present but corrupt or oversized.
1112
+ */
1113
+ declare function loadLockFile(input?: LoadLockFileInput, deps?: LoadLockFileDeps): Promise<LockFile>;
1114
+
1115
+ /** The committed lock-file name, chosen to be obviously JSON and to not match a `*.lock` ignore rule. */
1116
+ declare const LOCK_FILE_NAME = "verbatra.lock.json";
1117
+
959
1118
  /**
960
1119
  * Read-only metadata the CLI `init` scaffold derives its tables from, so the CLI never restates the
961
1120
  * provider, env-var, model, or format truth owned by core and ai-providers.
@@ -1070,4 +1229,4 @@ interface WatchController {
1070
1229
  */
1071
1230
  declare function watch(input: WatchInput, deps?: WatchDeps): Promise<WatchController>;
1072
1231
 
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 };
1232
+ 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, 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 };