@verbatra/sdk 0.5.0-next.3 → 0.5.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
@@ -3,19 +3,6 @@ import Anthropic from '@anthropic-ai/sdk';
3
3
  import { Interactions } from '@google/genai';
4
4
  import OpenAI from 'openai';
5
5
 
6
- /** Zod schema accepting exactly one of {@link SUPPORTED_FORMATS}. */
7
- declare const supportedFormatSchema$1: z.ZodEnum<{
8
- "i18next-json": "i18next-json";
9
- "vue-i18n-json": "vue-i18n-json";
10
- "next-intl-json": "next-intl-json";
11
- "ngx-translate-json": "ngx-translate-json";
12
- xliff: "xliff";
13
- yaml: "yaml";
14
- arb: "arb";
15
- }>;
16
- /** One of the supported source formats; a member of {@link SUPPORTED_FORMATS}. */
17
- type SupportedFormat$1 = z.infer<typeof supportedFormatSchema$1>;
18
-
19
6
  /**
20
7
  * A single, format-neutral translation unit. Placeholders are supplied already
21
8
  * extracted; core never derives them from the value.
@@ -124,6 +111,24 @@ interface ProviderNotice {
124
111
  /** A static, safe description; never a key or translatable content. */
125
112
  readonly message: string;
126
113
  }
114
+ /**
115
+ * Stable codes for a derived, per-key "needs review" signal. This is verbatra's own computed
116
+ * assessment, never a raw model self-score: four are recomputable from plain source/translated
117
+ * values (see the shared heuristic function), and `PROVIDER_DEGRADED` is applied by the caller from
118
+ * a provider notice.
119
+ *
120
+ * - `LENGTH_RATIO_OUTLIER`: the translated value's length is far shorter or longer than the source's.
121
+ * - `EQUALS_SOURCE`: the translated value equals the source value verbatim.
122
+ * - `GLOSSARY_TERM_MISSED`: a configured glossary target term did not appear in the translation.
123
+ * - `INTEGRITY_REORDERED`: the placeholder set matched but landed in a different order.
124
+ * - `PROVIDER_DEGRADED`: the batch this key came from carried a graceful-degradation notice.
125
+ */
126
+ type ReviewReasonCode = "LENGTH_RATIO_OUTLIER" | "EQUALS_SOURCE" | "GLOSSARY_TERM_MISSED" | "INTEGRITY_REORDERED" | "PROVIDER_DEGRADED";
127
+ /** A key flagged for human review, carrying every reason code that applies. */
128
+ interface ReviewFlag {
129
+ readonly status: "review";
130
+ readonly reasons: readonly ReviewReasonCode[];
131
+ }
127
132
  /** Result of a batch translation: per-key values, per-key integrity outcomes, and any notices. */
128
133
  interface TranslateResult {
129
134
  /** The translated value for each requested key. */
@@ -138,6 +143,11 @@ interface TranslateResult {
138
143
  * report returns an empty array rather than omitting the field.
139
144
  */
140
145
  readonly notices?: readonly ProviderNotice[];
146
+ /**
147
+ * Derived per-key review flags for this batch. A key absent from the map is implicitly "ok"; a
148
+ * present key carries one or more {@link ReviewReasonCode}s. Optional and additive.
149
+ */
150
+ readonly reviewFlags?: ReadonlyMap<string, ReviewFlag>;
141
151
  }
142
152
  /**
143
153
  * The single contract every provider implements. It is narrow enough that a machine-translation API like
@@ -212,6 +222,19 @@ type GeminiModel = Interactions.Model;
212
222
  */
213
223
  type OpenAiModel = OpenAI.ChatModel;
214
224
 
225
+ /** Zod schema accepting exactly one of {@link SUPPORTED_FORMATS}. */
226
+ declare const supportedFormatSchema$1: z.ZodEnum<{
227
+ "i18next-json": "i18next-json";
228
+ "vue-i18n-json": "vue-i18n-json";
229
+ "next-intl-json": "next-intl-json";
230
+ "ngx-translate-json": "ngx-translate-json";
231
+ xliff: "xliff";
232
+ yaml: "yaml";
233
+ arb: "arb";
234
+ }>;
235
+ /** One of the supported source formats; a member of {@link SUPPORTED_FORMATS}. */
236
+ type SupportedFormat$1 = z.infer<typeof supportedFormatSchema$1>;
237
+
215
238
  /**
216
239
  * The provider section of the config: a discriminated union over the provider id, reusing each
217
240
  * provider's own config schema. There is no key field anywhere in this union except
@@ -250,7 +273,9 @@ declare const providerConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
250
273
  apiKeyEnvVar: z.ZodOptional<z.ZodString>;
251
274
  }, z.core.$strict>;
252
275
  }, z.core.$strip>], "id">;
276
+ /** One validated provider section: the provider id plus that provider's own options. */
253
277
  type ProviderConfig = z.infer<typeof providerConfigSchema>;
278
+ /** The closed set of configurable provider ids. */
254
279
  type ProviderId = ProviderConfig["id"];
255
280
 
256
281
  /**
@@ -314,6 +339,11 @@ declare const verbatraConfigSchema: z.ZodObject<{
314
339
  prune: z.ZodOptional<z.ZodBoolean>;
315
340
  generatePlurals: z.ZodOptional<z.ZodBoolean>;
316
341
  maxBatchSize: z.ZodOptional<z.ZodNumber>;
342
+ maxTokens: z.ZodOptional<z.ZodNumber>;
343
+ budgetBehavior: z.ZodOptional<z.ZodEnum<{
344
+ warn: "warn";
345
+ stop: "stop";
346
+ }>>;
317
347
  }, z.core.$strict>;
318
348
  /**
319
349
  * The as-authored (or as-parsed) shape of the verbatra configuration, straight from the schema: `glossary`
@@ -332,7 +362,14 @@ type VerbatraConfig = Omit<VerbatraConfigInput, "glossary"> & {
332
362
  glossary?: Readonly<Record<string, string>>;
333
363
  };
334
364
 
365
+ /**
366
+ * A provider's known model literals, with the open `string` arm stripped, so the authoring field
367
+ * accepts only that provider's models. Static authoring aid only; the runtime schema stays a
368
+ * non-empty string, so a brand-new model the installed SDK does not yet list is flagged but still
369
+ * runs.
370
+ */
335
371
  type KnownModels<M extends string> = M extends string ? (string extends M ? never : M) : never;
372
+ /** One provider's config variant with `options.model` narrowed to that provider's known literals. */
336
373
  type AuthoringVariant<Id extends ProviderId, M extends string> = Extract<ProviderConfig, {
337
374
  id: Id;
338
375
  }> extends infer Variant ? Variant extends {
@@ -344,6 +381,12 @@ type AuthoringVariant<Id extends ProviderId, M extends string> = Extract<Provide
344
381
  model: KnownModels<M>;
345
382
  };
346
383
  } : never : never;
384
+ /**
385
+ * The authoring view of one provider variant, keyed by id. LLM providers with a known SDK model
386
+ * union restrict `options.model` to that provider's literals. DeepL has no model field and
387
+ * openai-compatible's model is whatever the local server exposes (no known-model list to restrict
388
+ * against), so both are carried through unchanged.
389
+ */
347
390
  type AuthoringProviderVariant = {
348
391
  anthropic: AuthoringVariant<"anthropic", AnthropicModel>;
349
392
  openai: AuthoringVariant<"openai", OpenAiModel>;
@@ -429,9 +472,19 @@ interface SdkFs {
429
472
  writeFile(path: string, data: string): Promise<void>;
430
473
  /** Write raw bytes atomically (temp file, then rename over the target). Used for the workbook. */
431
474
  writeBytes(path: string, data: Uint8Array): Promise<void>;
475
+ /**
476
+ * Atomically create a file only if it does not already exist (O_EXCL semantics), creating any
477
+ * missing parent directory first. Returns true if this call created the file, false if a file
478
+ * was already present (no write performed). The backbone of {@link withLocaleWriteLock}'s
479
+ * exclusive-create lock: the parent-directory creation lives here so that primitive never touches
480
+ * `node:fs` directly and a fake `SdkFs` in tests never needs a real directory to exist.
481
+ */
482
+ createExclusive(path: string, data: string): Promise<boolean>;
483
+ /** Delete a file if present; a no-op if it is already absent. */
484
+ deleteFile(path: string): Promise<void>;
432
485
  }
433
486
 
434
- /** Where a config's resolved glossary came from, recorded for later Studio consumption. */
487
+ /** Where a config's resolved glossary came from: absent, inline, or a file at the recorded path. */
435
488
  type GlossaryProvenance = {
436
489
  readonly source: "none";
437
490
  } | {
@@ -533,22 +586,35 @@ declare function loadConfig(options?: LoadConfigOptions): Promise<VerbatraConfig
533
586
  *
534
587
  * - `CONFIG_NOT_FOUND`: no config was found by search, or an explicit `configPath` does not exist
535
588
  * (thrown by `loadConfig`).
536
- * - `CONFIG_INVALID`: a config was found but is unparseable or fails validation (thrown by `loadConfig`).
537
- * - `UNKNOWN_FORMAT`: no adapter is registered for the configured format (thrown by `translate`).
538
- * - `UNKNOWN_LOCALE`: a requested locale is not among the configured target locales (thrown by
539
- * `check`, `diff`, and `exportWorkbook` via the shared locale selection).
540
- * - `PROVIDER_CONSTRUCTION_FAILED`: the provider factory threw; wraps the provider's own error, including
541
- * a missing `*_API_KEY` reported as `MISSING_API_KEY` (thrown by `translate`, non-dry-run only).
542
- * - `SOURCE_UNREADABLE`: the source locale file is absent (thrown by `translate`, and by `watch` at startup).
543
- * - `SOURCE_INVALID`: the source locale file could not be read or parsed; wraps the adapter read error
544
- * (thrown by `translate`).
589
+ * - `CONFIG_INVALID`: a config was found but is unparseable or fails validation, or its glossary
590
+ * file path could not be resolved (thrown by `loadConfig`).
591
+ * - `UNKNOWN_FORMAT`: no adapter is registered for the configured format (thrown by every entry
592
+ * point that selects an adapter, before any file is read).
593
+ * - `UNKNOWN_LOCALE`: a requested locale is not among the configured target locales (thrown via
594
+ * the shared locale selection by `check`, `diff`, `keyIntegrity`, `lockState`, `exportWorkbook`,
595
+ * `keyValue`, `editEntry`, and `retranslateEntry`).
596
+ * - `UNKNOWN_KEY`: a requested key is not among the source resource's own keys (thrown by
597
+ * `keyValue`, `editEntry`, and `retranslateEntry`).
598
+ * - `PROVIDER_CONSTRUCTION_FAILED`: the provider factory threw; wraps the provider's own error,
599
+ * including a missing `*_API_KEY` reported as `MISSING_API_KEY` (thrown by non-dry-run
600
+ * `translate` and by `retranslateEntry`).
601
+ * - `SOURCE_UNREADABLE`: the source locale file is absent (thrown by every entry point that reads
602
+ * the source, and by `watch` at startup).
603
+ * - `SOURCE_INVALID`: the source locale file could not be read or parsed; wraps the adapter read
604
+ * error (thrown by every entry point that reads the source).
545
605
  * - `LOCK_FILE_INVALID`: the lock-file is present but corrupt, oversized, or at an unsupported
546
- * version (thrown by `translate`, `check`, `diff`, `exportWorkbook`, and `importWorkbook`).
606
+ * version (thrown wherever the lock-file is read or updated: `translate`, `check`, `diff`,
607
+ * `keyIntegrity`, `lockState`, `loadLockFile`, `exportWorkbook`, `importWorkbook`, `editEntry`,
608
+ * and `retranslateEntry`).
609
+ * - `LOCK_CONTENDED`: a locale's write lock (`withLocaleWriteLock`) could not be acquired before
610
+ * its timeout elapsed, because another process is holding it (or an orphaned lock file was left
611
+ * behind by a killed process); the message names the lock file's path (thrown by `translate`,
612
+ * `importWorkbook`, `editEntry`, and `retranslateEntry`).
547
613
  * - `LOCALE_FAILED` (NOT thrown): the fallback `code` recorded on a failed `LocaleSummary` when a
548
- * per-locale failure carries no string code of its own. See the surfaced-not-thrown distinction on
549
- * `translate`.
614
+ * per-locale failure carries no string code of its own. See the surfaced-not-thrown distinction
615
+ * on `translate`.
550
616
  */
551
- type SdkErrorCode = "CONFIG_NOT_FOUND" | "CONFIG_INVALID" | "UNKNOWN_FORMAT" | "UNKNOWN_LOCALE" | "PROVIDER_CONSTRUCTION_FAILED" | "SOURCE_UNREADABLE" | "SOURCE_INVALID" | "LOCK_FILE_INVALID" | "LOCALE_FAILED";
617
+ type SdkErrorCode = "CONFIG_NOT_FOUND" | "CONFIG_INVALID" | "UNKNOWN_FORMAT" | "UNKNOWN_LOCALE" | "UNKNOWN_KEY" | "PROVIDER_CONSTRUCTION_FAILED" | "SOURCE_UNREADABLE" | "SOURCE_INVALID" | "LOCK_FILE_INVALID" | "LOCK_CONTENDED" | "LOCALE_FAILED";
552
618
  /** The single structured error the SDK throws or records. Never carries a secret. */
553
619
  declare class SdkError extends Error {
554
620
  /** The stable {@link SdkErrorCode} for this failure; branch on this, not the message. */
@@ -623,6 +689,13 @@ interface ReadResult {
623
689
  readonly resource: LocaleResource;
624
690
  /** Keys whose values are invalid for the format's message syntax. Empty for non-ICU formats. */
625
691
  readonly invalidIcuKeys: readonly string[];
692
+ /**
693
+ * Dotted paths of leaves that were present in the source but excluded because they are not
694
+ * strings (a stray number, boolean, or null). These are never translated, hashed, diffed, or
695
+ * checked for placeholder or ICU integrity, and are not written back if the file is later
696
+ * rewritten. Empty for a file with no such leaves.
697
+ */
698
+ readonly excludedLeafPaths: readonly string[];
626
699
  }
627
700
  /**
628
701
  * The contract every format adapter implements. A new format attaches by implementing this interface
@@ -740,6 +813,11 @@ declare class AdapterRegistry {
740
813
  register(adapter: FormatAdapter): this;
741
814
  private formats;
742
815
  private resolveByFormat;
816
+ /**
817
+ * Resolve by asking every registered adapter's `canHandle`. Several adapters can claim one file
818
+ * (all JSON adapters claim `.json`), so more than one match is reported as `ambiguous` rather
819
+ * than guessed at.
820
+ */
743
821
  private resolveByDetection;
744
822
  /**
745
823
  * Resolve the adapter for a file, by explicit format when given, otherwise by detection.
@@ -787,10 +865,11 @@ interface CheckDeps {
787
865
  readonly fs?: SdkFs;
788
866
  }
789
867
  /**
790
- * Report which keys are missing or stale per target locale, without calling a provider, writing any
791
- * file, or touching the lock. Each locale is reported as counts only (missing, stale, up-to-date);
792
- * a locale is `inSync` when nothing is missing or stale, and the top-level `inSync` is true only when
793
- * every checked locale is. Orphaned keys and integrity are not reported, since they concern a write.
868
+ * Reports how many keys are missing or stale per target locale, without calling a provider, writing
869
+ * any file, or touching the lock. Each locale is reported as counts only (missing, stale,
870
+ * up-to-date); a locale is `inSync` when nothing is missing or stale, and the top-level `inSync` is
871
+ * true only when every checked locale is. Orphaned keys and integrity are not reported, since they
872
+ * concern a write; use {@link diff} for the key lists.
794
873
  *
795
874
  * @param input - The validated config and which locales to check.
796
875
  * @param deps - Optional composition seams (registry, file system) for tests.
@@ -836,11 +915,11 @@ interface DiffDeps {
836
915
  readonly fs?: SdkFs;
837
916
  }
838
917
  /**
839
- * Report the exact pending change per target locale as three key lists (missing, changed, orphaned),
840
- * without calling a provider, writing any file, or touching the lock. This is the detailed sibling of
841
- * {@link check}. A locale's `hasPendingChanges` is driven by missing or changed only; orphaned keys are
842
- * reported but do not flip it, since a default `translate` run does not prune. The top-level
843
- * `hasPendingChanges` is true when any checked locale has pending changes.
918
+ * Reports the exact pending change per target locale as three key lists (missing, changed,
919
+ * orphaned), without calling a provider, writing any file, or touching the lock. The detailed
920
+ * sibling of {@link check}. A locale's `hasPendingChanges` is driven by missing or changed only;
921
+ * orphaned keys are reported but do not flip it, since a default `translate` run does not prune.
922
+ * The top-level `hasPendingChanges` is true when any checked locale has pending changes.
844
923
  *
845
924
  * @param input - The validated config and which locales to diff.
846
925
  * @param deps - Optional composition seams (registry, file system) for tests.
@@ -851,6 +930,238 @@ interface DiffDeps {
851
930
  */
852
931
  declare function diff(input: DiffInput, deps?: DiffDeps): Promise<DiffSummary>;
853
932
 
933
+ /** Input for {@link editEntry}: the validated config, exactly one target locale/key pair, and the human-typed replacement value. */
934
+ interface EditEntryInput {
935
+ /** The validated configuration (typically from {@link loadConfig}). */
936
+ readonly config: VerbatraConfig;
937
+ /** Directory the file pattern and lock-file resolve against; defaults to cwd. */
938
+ readonly cwd?: string;
939
+ /** The target locale to write the correction into. Must be a configured target locale. */
940
+ readonly locale: string;
941
+ /** The source key being corrected. Must exist in the source resource. */
942
+ readonly key: string;
943
+ /** The human-typed replacement value. */
944
+ readonly value: string;
945
+ }
946
+ /** Composition seam for {@link editEntry}: inject a registry and a file system for tests. No provider builder: this seam never calls a provider. */
947
+ interface EditEntryDeps {
948
+ readonly adapterRegistry?: AdapterRegistry;
949
+ readonly fs?: SdkFs;
950
+ }
951
+ /**
952
+ * The two-armed result of one edit attempt: acceptance carries the newly written value; a
953
+ * rejection carries the candidate value and which {@link gateCandidateValue} check failed it,
954
+ * with nothing written. Deliberately has no review-reasons field: a human-typed correction never
955
+ * touches a provider, so there is no provider-derived review signal to report.
956
+ */
957
+ type EditEntryResult = {
958
+ readonly accepted: true;
959
+ readonly value: string;
960
+ } | {
961
+ readonly accepted: false;
962
+ readonly reason: "placeholder" | "icu";
963
+ readonly value: string;
964
+ };
965
+ /**
966
+ * Writes exactly one human-typed correction for exactly one target locale, gated through the
967
+ * shared {@link gateCandidateValue} before anything reaches disk. On acceptance, writes the target
968
+ * locale file (merging just this one key into its current entries, every other key untouched) and
969
+ * updates the lock entry for this key only (see {@link updateLockFileLocale}'s `"merge"` mode); on
970
+ * rejection, writes nothing. Never calls a provider: {@link EditEntryDeps} has no provider seam,
971
+ * so there is no way to construct or call one.
972
+ *
973
+ * `locale` and `key` are resolved fresh on every call, never from a cache: `locale` against
974
+ * `config.targetLocales`, `key` against the source resource read fresh from disk.
975
+ *
976
+ * The target-file write and the lock-file update run inside one held `withLocaleWriteLock`
977
+ * critical section, so no second writer for this locale can observe the target file updated but
978
+ * the lock entry not yet, or vice versa. Reading the source, resolving the key, and selecting the
979
+ * adapter stay outside the lock: they are read-only and need no protection.
980
+ *
981
+ * Never touches `.verbatra-local/run-status.json`: that file is rewritten wholesale by
982
+ * `translate()`/`watch()` outside any locale lock, so patching it here would race that rewrite. A
983
+ * key edited here stays stale in that persisted snapshot until the next real run, which self-heals
984
+ * it since this call already advanced the lock hash.
985
+ *
986
+ * @param input - The validated config, the target locale, the source key, and the replacement value.
987
+ * @param deps - Optional composition seams (registry, file system) for tests.
988
+ * @returns The two-armed {@link EditEntryResult}.
989
+ * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
990
+ * @throws {@link SdkError} `UNKNOWN_LOCALE`: `locale` is not among `config.targetLocales`.
991
+ * @throws {@link SdkError} `SOURCE_UNREADABLE` / `SOURCE_INVALID`: the source locale file is absent
992
+ * or unreadable.
993
+ * @throws {@link SdkError} `UNKNOWN_KEY`: `key` does not exist in the source resource.
994
+ * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is corrupt, oversized, or at an
995
+ * unsupported version.
996
+ * @throws {@link SdkError} `LOCK_CONTENDED`: this locale's write lock could not be acquired before
997
+ * its timeout, because another process is holding it.
998
+ */
999
+ declare function editEntry(input: EditEntryInput, deps?: EditEntryDeps): Promise<EditEntryResult>;
1000
+
1001
+ /**
1002
+ * One key's placeholder and ICU integrity result for one target locale. Only ever computed for a
1003
+ * "changed" key (a current source and a current target value both exist). `hasPlaceholders` is
1004
+ * false when the source value carries no placeholders, in which case `matches` is trivially true
1005
+ * and carries no signal on its own. `icuValid` is computed unconditionally, independent of
1006
+ * `matches`: unlike `gateCandidateValue`, which stops at the first failing check, this is an
1007
+ * information report and stays accurate even when the key already fails on placeholders; it is
1008
+ * always true for a non-ICU format. Carries no source or target string value: only the boolean
1009
+ * results and, on a placeholder mismatch, the specific tokens involved.
1010
+ */
1011
+ interface KeyIntegrityEntry {
1012
+ readonly key: string;
1013
+ readonly hasPlaceholders: boolean;
1014
+ readonly matches: boolean;
1015
+ readonly missing: readonly string[];
1016
+ readonly extra: readonly string[];
1017
+ readonly icuValid: boolean;
1018
+ }
1019
+ /** One target locale's integrity entries for the keys checked against it. */
1020
+ interface LocaleKeyIntegrity {
1021
+ readonly locale: string;
1022
+ readonly entries: readonly KeyIntegrityEntry[];
1023
+ }
1024
+ /** Input for {@link keyIntegrity}: the validated config, which locales to check, and an optional key filter. */
1025
+ interface KeyIntegrityInput {
1026
+ /** The validated configuration (typically from {@link loadConfig}). */
1027
+ readonly config: VerbatraConfig;
1028
+ /** Directory the file pattern and lock-file resolve against; defaults to cwd. */
1029
+ readonly cwd?: string;
1030
+ /** Subset of target locales to check; defaults to all configured target locales. */
1031
+ readonly locales?: readonly string[];
1032
+ /** Restrict the check to these keys; only the ones that are "changed" for a locale are checked. Defaults to every changed key. */
1033
+ readonly keys?: readonly string[];
1034
+ }
1035
+ /** Composition seam for {@link keyIntegrity}: inject a registry and a file system for tests. */
1036
+ interface KeyIntegrityDeps {
1037
+ readonly adapterRegistry?: AdapterRegistry;
1038
+ readonly fs?: SdkFs;
1039
+ }
1040
+ /**
1041
+ * Runs the placeholder check and the ICU message-validity check for each selected target locale's
1042
+ * "changed" keys (present with a current value on both sides, per core's `diffResources`) and
1043
+ * reports a per-key result. Uses the adapter's own `comparePlaceholders` when present (the
1044
+ * branch-aware ICU path), otherwise core's `checkPlaceholders` over the stored placeholder lists.
1045
+ * `icuValid` is `adapter.validateMessage(target)`, computed unconditionally and independently of
1046
+ * the placeholder result, so a target that is placeholder-valid but syntactically invalid ICU is
1047
+ * still reported accurately. Read-only: it calls no provider, writes no file, and never touches
1048
+ * the lock. Missing and orphaned keys are never checked, since one side's value does not exist for
1049
+ * those.
1050
+ *
1051
+ * The returned entries never carry a source or target string value, only the boolean results and,
1052
+ * on a placeholder mismatch, the specific tokens involved.
1053
+ *
1054
+ * @param input - The validated config, which locales to check, and an optional key filter.
1055
+ * @param deps - Optional composition seams (registry, file system) for tests.
1056
+ * @returns One entry per checked locale, each carrying the integrity result for its changed keys
1057
+ * (narrowed to `input.keys` when supplied).
1058
+ * @throws {@link SdkError} `UNKNOWN_FORMAT`, `SOURCE_UNREADABLE`, `SOURCE_INVALID`,
1059
+ * `LOCK_FILE_INVALID`, or `UNKNOWN_LOCALE`, with the same meanings as in {@link diff}.
1060
+ */
1061
+ declare function keyIntegrity(input: KeyIntegrityInput, deps?: KeyIntegrityDeps): Promise<readonly LocaleKeyIntegrity[]>;
1062
+
1063
+ /** Input for {@link keyValue}: the validated config and exactly one target locale/key pair. */
1064
+ interface KeyValueInput {
1065
+ /** The validated configuration (typically from {@link loadConfig}). */
1066
+ readonly config: VerbatraConfig;
1067
+ /** Directory the file pattern resolves against; defaults to cwd. */
1068
+ readonly cwd?: string;
1069
+ /** The target locale to read the current translation from. Must be a configured target locale. */
1070
+ readonly locale: string;
1071
+ /** The source key to read. Must exist in the source resource. */
1072
+ readonly key: string;
1073
+ }
1074
+ /** Composition seam for {@link keyValue}: inject a registry and a file system for tests. */
1075
+ interface KeyValueDeps {
1076
+ readonly adapterRegistry?: AdapterRegistry;
1077
+ readonly fs?: SdkFs;
1078
+ }
1079
+ /**
1080
+ * The current source and target values for one key/locale pair. `target` is absent exactly when
1081
+ * the key does not yet exist in that target locale; a present-but-empty-string target is a real
1082
+ * value and is returned as such.
1083
+ */
1084
+ interface KeyValueResult {
1085
+ readonly source: string;
1086
+ readonly target?: string;
1087
+ }
1088
+ /**
1089
+ * Reads a key's current source and target values for exactly one target locale. Read-only: it
1090
+ * calls no provider, writes no file, and mutates nothing. Exposes only the current values on disk,
1091
+ * never a previous one.
1092
+ *
1093
+ * `locale` and `key` are resolved fresh on every call, so a caller feeding an edit dialog with
1094
+ * this result and then submitting through `editEntry` is always working from live data, never a
1095
+ * cached snapshot.
1096
+ *
1097
+ * @param input - The validated config, the target locale, and the source key to read.
1098
+ * @param deps - Optional composition seams (registry, file system) for tests.
1099
+ * @returns The current source value and, when present, the current target value.
1100
+ * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
1101
+ * @throws {@link SdkError} `UNKNOWN_LOCALE`: `locale` is not among `config.targetLocales`.
1102
+ * @throws {@link SdkError} `SOURCE_UNREADABLE` / `SOURCE_INVALID`: the source locale file is absent
1103
+ * or unreadable.
1104
+ * @throws {@link SdkError} `UNKNOWN_KEY`: `key` does not exist in the source resource.
1105
+ */
1106
+ declare function keyValue(input: KeyValueInput, deps?: KeyValueDeps): Promise<KeyValueResult>;
1107
+
1108
+ /**
1109
+ * One locale file reduced to a content hash per key, taken at one point in time. Two snapshots of
1110
+ * the same file can be compared with {@link diffLocaleSnapshots} to count what changed between
1111
+ * them.
1112
+ */
1113
+ interface LocaleFileSnapshot {
1114
+ /** The locale this snapshot was read for (the configured source locale or a target locale). */
1115
+ readonly locale: string;
1116
+ /** Content hash per key, from core's `contentHash`. */
1117
+ readonly hashes: ReadonlyMap<string, string>;
1118
+ }
1119
+ /** Input for {@link readLocaleFileSnapshot}: the validated config and which locale to snapshot. */
1120
+ interface ReadLocaleFileSnapshotInput {
1121
+ /** The validated configuration (typically from {@link loadConfig}). */
1122
+ readonly config: VerbatraConfig;
1123
+ /** The locale to snapshot: the configured source locale or any configured target locale. */
1124
+ readonly locale: string;
1125
+ /** Directory the file pattern resolves against; defaults to cwd. */
1126
+ readonly cwd?: string;
1127
+ }
1128
+ /** Composition seam for {@link readLocaleFileSnapshot}: inject a registry and a file system for tests. */
1129
+ interface ReadLocaleFileSnapshotDeps {
1130
+ readonly adapterRegistry?: AdapterRegistry;
1131
+ readonly fs?: SdkFs;
1132
+ }
1133
+ /**
1134
+ * Reads one locale file through the configured adapter and reduces it to a content hash per key.
1135
+ * Uses the same tolerant read path as {@link readTarget}: a file that does not exist yet reads as
1136
+ * an empty snapshot rather than throwing, so a caller can establish a baseline before a locale's
1137
+ * first write.
1138
+ *
1139
+ * @param input - The validated config, the locale to snapshot, and the directory to resolve against.
1140
+ * @param deps - Optional composition seams (registry, file system) for tests.
1141
+ * @returns The locale's per-key content hash snapshot.
1142
+ * @throws {@link SdkError} `UNKNOWN_FORMAT` when the configured format has no registered adapter;
1143
+ * rejects with the adapter's own error when the file exists but its content is malformed.
1144
+ */
1145
+ declare function readLocaleFileSnapshot(input: ReadLocaleFileSnapshotInput, deps?: ReadLocaleFileSnapshotDeps): Promise<LocaleFileSnapshot>;
1146
+ /** A locale file's added, changed, and removed key counts between two of its own snapshots. Counts only, never key names. */
1147
+ interface LocaleSnapshotDelta {
1148
+ readonly added: number;
1149
+ readonly changed: number;
1150
+ readonly removed: number;
1151
+ }
1152
+ /**
1153
+ * Compares two snapshots of the same locale file, taken at different points in time, and counts
1154
+ * how many keys were added, changed, or removed. A direct key-to-hash comparison rather than
1155
+ * core's `diffResources`: that function's source/target/baseline shape exists for cross-resource
1156
+ * comparison, and forcing a same-file, two-points-in-time comparison through it would need a
1157
+ * confusing relabeling (missing becomes removed, orphaned becomes added).
1158
+ *
1159
+ * @param previous - The earlier snapshot of the file.
1160
+ * @param current - The later snapshot of the same file.
1161
+ * @returns The counts of keys added, changed, and removed between the two snapshots.
1162
+ */
1163
+ declare function diffLocaleSnapshots(previous: LocaleFileSnapshot, current: LocaleFileSnapshot): LocaleSnapshotDelta;
1164
+
854
1165
  /** Per-locale key count and drift against the source and target files, from the recorded lock baseline. */
855
1166
  interface LockLocaleState {
856
1167
  /** The target locale this entry reports on. */
@@ -906,8 +1217,113 @@ interface LockStateDeps {
906
1217
  */
907
1218
  declare function lockState(input: LockStateInput, deps?: LockStateDeps): Promise<LockStateResult>;
908
1219
 
1220
+ /** Builds the provider from its config. Injectable so tests stay offline. */
1221
+ type CreateProvider = (config: ProviderConfig) => TranslationProvider;
1222
+
1223
+ /** Input for {@link retranslateEntry}: the validated config and exactly one target locale/key pair. */
1224
+ interface RetranslateEntryInput {
1225
+ /** The validated configuration (typically from {@link loadConfig}). */
1226
+ readonly config: VerbatraConfig;
1227
+ /** Directory the file pattern and lock-file resolve against; defaults to cwd. */
1228
+ readonly cwd?: string;
1229
+ /** The target locale to retranslate the key into. Must be a configured target locale. */
1230
+ readonly locale: string;
1231
+ /** The source key to retranslate. Must exist in the source resource. */
1232
+ readonly key: string;
1233
+ }
1234
+ /** Composition seam for {@link retranslateEntry}: inject a registry, a provider builder, and a file system for tests. */
1235
+ interface RetranslateEntryDeps {
1236
+ readonly adapterRegistry?: AdapterRegistry;
1237
+ readonly createProvider?: CreateProvider;
1238
+ readonly fs?: SdkFs;
1239
+ }
1240
+ /**
1241
+ * The two-armed result of one retranslate attempt: `accepted` carries the newly written value and
1242
+ * any derived "needs review" reasons; a rejection carries the candidate value and which
1243
+ * {@link gateCandidateValue} check failed it, without writing anything.
1244
+ */
1245
+ type RetranslateEntryResult = {
1246
+ readonly accepted: true;
1247
+ readonly value: string;
1248
+ readonly reviewReasons: readonly ReviewReasonCode[];
1249
+ } | {
1250
+ readonly accepted: false;
1251
+ readonly reason: "placeholder" | "icu";
1252
+ readonly value: string;
1253
+ };
1254
+ /**
1255
+ * Retranslate exactly one key for exactly one target locale: a single-entry `translateBatch` call
1256
+ * through the same provider registry `translate()` uses, gated through the shared
1257
+ * {@link gateCandidateValue} before anything reaches disk. On acceptance, writes the target locale
1258
+ * file (merging just this one key into its current entries, every other key untouched) and updates
1259
+ * the lock entry for this key only (see {@link updateLockFileLocale}'s `"merge"` mode); on rejection,
1260
+ * writes nothing. Reuses exactly the building blocks `translate()` already uses
1261
+ * (`selectProvider`/`provider.translateBatch`) and nothing else: no bespoke per-provider plumbing.
1262
+ *
1263
+ * `locale` and `key` are resolved fresh on every call, never cached: `locale` against
1264
+ * `config.targetLocales` (`UNKNOWN_LOCALE`), `key` against the source resource's own keys, read
1265
+ * fresh from disk (`UNKNOWN_KEY`), via a `Map`-backed lookup, never `key in obj` or
1266
+ * `obj[key] !== undefined` against a plain object.
1267
+ *
1268
+ * The target-file write and the lock-file update run inside the same held
1269
+ * `withLocaleWriteLock(cwd, locale, fs, ...)` critical section as the provider call itself, so no
1270
+ * second writer for this locale can ever observe the target file updated but the lock entry not
1271
+ * yet, or vice versa. The same locking also covers `translate()`/`watch()` and `importWorkbook()`.
1272
+ *
1273
+ * Not atomic across the two writes for a single caller's own run: the target locale file is
1274
+ * written before the lock entry is updated. If `updateLockFileLocale` then throws
1275
+ * (`LOCK_FILE_INVALID` for a corrupt lock-file, or `LOCK_CONTENDED` if its own internal lock-file
1276
+ * guard cannot be acquired before its timeout), the target file already carries the new value
1277
+ * while the lock still records the old hash, and the thrown error does not imply nothing was
1278
+ * saved. The same ordering, and the same partial-write shape on that rare path, also exists in
1279
+ * `translate()` and `importWorkbook()`.
1280
+ *
1281
+ * @param input - The validated config, the target locale, and the source key to retranslate.
1282
+ * @param deps - Optional composition seams (registry, provider builder, file system) for tests.
1283
+ * @returns The two-armed {@link RetranslateEntryResult}.
1284
+ * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
1285
+ * @throws {@link SdkError} `UNKNOWN_LOCALE`: `locale` is not among `config.targetLocales`.
1286
+ * @throws {@link SdkError} `SOURCE_UNREADABLE` / `SOURCE_INVALID`: the source locale file is absent
1287
+ * or unreadable.
1288
+ * @throws {@link SdkError} `UNKNOWN_KEY`: `key` does not exist in the source resource.
1289
+ * @throws {@link SdkError} `PROVIDER_CONSTRUCTION_FAILED`: the provider factory threw (wraps a
1290
+ * missing `*_API_KEY` as `MISSING_API_KEY`).
1291
+ * @throws {@link ProviderError} the provider call itself failed, or returned no value at all for
1292
+ * this key (`INVALID_RESPONSE`).
1293
+ * @throws {@link SdkError} `LOCK_FILE_INVALID`: the lock-file is corrupt, oversized, or at an
1294
+ * unsupported version.
1295
+ * @throws {@link SdkError} `LOCK_CONTENDED`: this locale's write lock could not be acquired before
1296
+ * its timeout, because another process (another CLI run, or a second Studio write) is holding it.
1297
+ */
1298
+ declare function retranslateEntry(input: RetranslateEntryInput, deps?: RetranslateEntryDeps): Promise<RetranslateEntryResult>;
1299
+
909
1300
  /** A stable code for an SDK-originated notice (not a provider notice). */
910
- type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED";
1301
+ type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED" | "BUDGET_TOKENS_EXCEEDED";
1302
+ /** Summed token usage across every provider call in a scope (one locale, or the whole run). */
1303
+ interface UsageSummary {
1304
+ readonly inputTokens: number;
1305
+ readonly outputTokens: number;
1306
+ }
1307
+ /** The configured token-budget behavior: warn and continue, or stop making further provider calls. */
1308
+ type BudgetBehavior = "warn" | "stop";
1309
+ /**
1310
+ * The run-wide token-budget outcome, present on {@link RunSummary} only when `maxTokens` is configured.
1311
+ * `supported: false` means the configured provider never reported usage in this run (a token-less
1312
+ * provider such as DeepL, or a dry-run): the guardrail is honestly inert, `tokensUsed` stays `0`, and
1313
+ * `exceeded` stays `false`, never a false trip.
1314
+ */
1315
+ interface RunBudget {
1316
+ /** The configured ceiling. */
1317
+ readonly maxTokens: number;
1318
+ /** The configured behavior once the ceiling is reached. */
1319
+ readonly behavior: BudgetBehavior;
1320
+ /** Whether the configured provider reported usage at all this run. */
1321
+ readonly supported: boolean;
1322
+ /** Cumulative input plus output tokens across the run so far. */
1323
+ readonly tokensUsed: number;
1324
+ /** Whether `tokensUsed` reached or passed `maxTokens` at any point this run. */
1325
+ readonly exceeded: boolean;
1326
+ }
911
1327
  /**
912
1328
  * A notice raised by the SDK itself (not a provider), structurally identical to a {@link ProviderNotice}
913
1329
  * so both share the {@link LocaleSummary.notices} channel. Carries only a stable code and a static,
@@ -921,6 +1337,13 @@ interface SdkNotice {
921
1337
  }
922
1338
  /** A notice on a locale summary: either a provider-emitted notice or an SDK-emitted one. */
923
1339
  type LocaleNotice = ProviderNotice | SdkNotice;
1340
+ /** One key flagged for human review, and every reason code that applies to it. */
1341
+ interface NeedsReviewEntry {
1342
+ /** The flagged key. */
1343
+ readonly key: string;
1344
+ /** Every {@link ReviewReasonCode} that applies to this key. */
1345
+ readonly reasons: readonly ReviewReasonCode[];
1346
+ }
924
1347
  /** Structured outcome for one target locale; surfaced as data on the run, never thrown. */
925
1348
  interface LocaleSummary {
926
1349
  /** The target locale this summary is for. */
@@ -928,7 +1351,7 @@ interface LocaleSummary {
928
1351
  /** Whether this locale's run succeeded or failed (a failure does not abort the run). */
929
1352
  readonly status: "succeeded" | "failed";
930
1353
  /**
931
- * Keys translated and written this run. In dry-run, the keys that WOULD be translated
1354
+ * Keys translated and written this run. In dry-run, the keys that would be translated
932
1355
  * (the provider is not called and nothing is written).
933
1356
  */
934
1357
  readonly translated: readonly string[];
@@ -938,7 +1361,7 @@ interface LocaleSummary {
938
1361
  readonly orphaned: readonly string[];
939
1362
  /**
940
1363
  * Orphaned keys actually removed this run because pruning was on. In a dry-run with pruning on, the keys
941
- * that WOULD be removed. Empty when pruning is off (the orphans then survive and are reported in
1364
+ * that would be removed. Empty when pruning is off (the orphans then survive and are reported in
942
1365
  * `orphaned` only). A subset of `orphaned`; never includes a source-present key.
943
1366
  */
944
1367
  readonly pruned: readonly string[];
@@ -966,6 +1389,20 @@ interface LocaleSummary {
966
1389
  * and acted, and empty in a dry-run.
967
1390
  */
968
1391
  readonly generated: readonly string[];
1392
+ /**
1393
+ * Candidate keys never sent to the provider because a configured `maxTokens` budget already tripped
1394
+ * in `"stop"` mode: keys remaining in the current locale once the ceiling was crossed, and every
1395
+ * candidate key of a later, fully-skipped locale. Always present, empty by default, same convention as
1396
+ * {@link LocaleSummary.integrityMismatches} and {@link LocaleSummary.providerFailures}. Each key keeps
1397
+ * its prior lock hash and is picked up again next run, exactly like a `providerFailures` key.
1398
+ */
1399
+ readonly budgetWithheld: readonly string[];
1400
+ /**
1401
+ * Summed token usage across every provider call for this locale (main translation plus plural
1402
+ * generation). Absent when no call in this locale reported usage: a dry-run, a token-less provider
1403
+ * (DeepL), or a locale with nothing to translate. Never a fabricated `{ inputTokens: 0, outputTokens: 0 }`.
1404
+ */
1405
+ readonly usage?: UsageSummary;
969
1406
  /**
970
1407
  * Notices for this locale: provider notices (e.g. DeepL graceful-degradation) and SDK notices
971
1408
  * (e.g. a target language needing more CLDR plural categories than the source supplies). Empty when
@@ -973,7 +1410,16 @@ interface LocaleSummary {
973
1410
  */
974
1411
  readonly notices: readonly LocaleNotice[];
975
1412
  /**
976
- * Present only when status is "failed": a structured, secret-free error. `code` is a PRESERVED string
1413
+ * Keys accepted and written this run that the review heuristics flagged for a second look, sorted
1414
+ * by key. Distinct from the placeholder/ICU integrity gate: a review flag is advisory only and never
1415
+ * withholds a key, so this never double-reports a key already surfaced through
1416
+ * {@link LocaleSummary.integrityMismatches} or {@link LocaleSummary.providerFailures}. Empty when
1417
+ * nothing was flagged, always empty for a workbook import (which never calls a provider or recomputes
1418
+ * flags on its own path).
1419
+ */
1420
+ readonly needsReview: readonly NeedsReviewEntry[];
1421
+ /**
1422
+ * Present only when status is "failed": a structured, secret-free error. `code` is a preserved string
977
1423
  * (the underlying provider/adapter error's `code`, or `"LOCALE_FAILED"` as a fallback), intentionally
978
1424
  * wider than {@link SdkErrorCode}, so do not treat it as a closed set.
979
1425
  */
@@ -992,10 +1438,77 @@ interface RunSummary {
992
1438
  readonly succeeded: readonly string[];
993
1439
  /** Locales whose run failed (see each locale's `error`). */
994
1440
  readonly failed: readonly string[];
1441
+ /**
1442
+ * Summed token usage across every locale's {@link LocaleSummary.usage}. Absent when no locale in the
1443
+ * run reported usage (a dry-run, or an all-token-less-provider run).
1444
+ */
1445
+ readonly usage?: UsageSummary;
1446
+ /** The run-wide token-budget outcome; present only when `maxTokens` is configured. */
1447
+ readonly budget?: RunBudget;
995
1448
  }
996
1449
 
997
- /** Builds the provider from its config. Injectable so tests stay offline. */
998
- type CreateProvider = (config: ProviderConfig) => TranslationProvider;
1450
+ /**
1451
+ * One target locale's snapshot inside the persisted run-status file. `status` is carried over from
1452
+ * `LocaleSummary.status` unchanged, so an empty `needsReview` on a failed locale is never misread as
1453
+ * "this locale ran clean with nothing to review".
1454
+ */
1455
+ interface RunStatusLocale {
1456
+ readonly locale: string;
1457
+ readonly status: "succeeded" | "failed";
1458
+ /** The full, unmodified list from `LocaleSummary.needsReview`; already bounded and content-free. */
1459
+ readonly needsReview: readonly NeedsReviewEntry[];
1460
+ /** Absent exactly when the source `LocaleSummary.usage` was absent; never a fabricated zero. */
1461
+ readonly usage?: UsageSummary;
1462
+ }
1463
+ /**
1464
+ * The persisted shape of `.verbatra-local/run-status.json`: one whole-run snapshot, overwritten on
1465
+ * every non-dry-run `translate()`/`watch()` run that reaches the end of its per-locale loop. Contains
1466
+ * only the review-flag and token/usage data already computed for `RunSummary`; never a translation
1467
+ * string, a provider error message, or a provider notice message.
1468
+ */
1469
+ interface RunStatusFile {
1470
+ readonly version: number;
1471
+ /** ISO timestamp of when this snapshot was written. */
1472
+ readonly generatedAt: string;
1473
+ /** Summed run-wide token usage, mirroring `RunSummary.usage`; absent under the same conditions. */
1474
+ readonly usage?: UsageSummary;
1475
+ /** The run-wide token-budget outcome, mirroring `RunSummary.budget`; present only when configured. */
1476
+ readonly budget?: RunBudget;
1477
+ readonly locales: readonly RunStatusLocale[];
1478
+ }
1479
+
1480
+ /** Input for {@link runStatus}: the persisted file is self-contained, so no `config` is needed. */
1481
+ interface RunStatusInput {
1482
+ /** Directory the run-status file resolves against; defaults to cwd. */
1483
+ readonly cwd?: string;
1484
+ }
1485
+ /** Composition seam for {@link runStatus}: inject a file system for tests. */
1486
+ interface RunStatusDeps {
1487
+ readonly fs?: SdkFs;
1488
+ }
1489
+ /**
1490
+ * The persisted run-status snapshot, or its absence. `available: false` covers a project that has
1491
+ * never run a non-dry-run `translate()`/`watch()`, exactly like every other degrade case
1492
+ * {@link runStatus} folds into it: never a distinct error, so a caller does not need to special-case
1493
+ * "first run" apart from "corrupt file" apart from "unrecognized version".
1494
+ */
1495
+ type RunStatusResult = {
1496
+ readonly available: false;
1497
+ } | ({
1498
+ readonly available: true;
1499
+ } & RunStatusFile);
1500
+ /**
1501
+ * Read the most recent run's persisted review-flag and token/usage snapshot from
1502
+ * `.verbatra-local/run-status.json`, without calling a provider, writing any file, or mutating the
1503
+ * lock-file. Never throws: a missing file, invalid JSON, a schema mismatch, or an unrecognized
1504
+ * `version` all degrade to `{ available: false }`, since this file is best-effort telemetry, not a
1505
+ * correctness baseline (contrast {@link lockState}, which can throw `LOCK_FILE_INVALID`).
1506
+ *
1507
+ * @param input - Only the directory the file resolves against; no config is needed.
1508
+ * @param deps - Optional composition seam (file system) for tests.
1509
+ * @returns `{ available: false }`, or `{ available: true, ...the persisted fields }`.
1510
+ */
1511
+ declare function runStatus(input?: RunStatusInput, deps?: RunStatusDeps): Promise<RunStatusResult>;
999
1512
 
1000
1513
  /** Everything the one-shot run needs: the validated config and where/how to run it. */
1001
1514
  interface TranslateInput {
@@ -1036,12 +1549,23 @@ interface TranslateDeps {
1036
1549
  * locales that succeeded. Dry-run reads + diffs + reports without constructing/calling
1037
1550
  * the provider and without writing any file or the lock-file.
1038
1551
  *
1039
- * A per-locale failure does NOT throw: it is recorded on that locale's {@link LocaleSummary} as
1552
+ * A per-locale failure does not throw: it is recorded on that locale's {@link LocaleSummary} as
1040
1553
  * `status: "failed"` with a secret-free `{ code, message }`, where `code` is a preserved string (the
1041
1554
  * underlying provider/adapter code, or `"LOCALE_FAILED"` as a fallback), not necessarily an
1042
1555
  * {@link SdkErrorCode}. DeepL notices, integrity mismatches, and invalid-ICU source keys likewise
1043
1556
  * surface on each `LocaleSummary`, never as throws.
1044
1557
  *
1558
+ * A dry-run reads the lock once, outside any lock, and reuses that one snapshot for every locale:
1559
+ * it never writes, so there is nothing to serialize against. A live run instead re-reads the lock
1560
+ * fresh inside each locale's own write lock (see `runLiveLocale`), so two concurrent live calls
1561
+ * never both diff against the same stale pre-loop snapshot. The token-budget tracker is fresh per
1562
+ * invocation: the `maxTokens` ceiling is per-run, not cumulative across watch cycles.
1563
+ *
1564
+ * On a non-dry-run that reaches the end of the loop, the run's review-flag and token/usage data is
1565
+ * also written to `.verbatra-local/run-status.json` (readable back through {@link runStatus}). This
1566
+ * write is best-effort: any failure is caught and swallowed, never re-thrown and never reflected on
1567
+ * the returned {@link RunSummary}.
1568
+ *
1045
1569
  * @param input - The validated config and run options (cwd, dryRun, prune, generatePlurals).
1046
1570
  * @param deps - Optional composition seams (registry, provider builder, file system) for tests.
1047
1571
  * @returns A {@link RunSummary}: the per-locale {@link LocaleSummary}s and the succeeded/failed locale lists.
@@ -1147,7 +1671,11 @@ interface ImportWorkbookDeps {
1147
1671
  * structured {@link SdkError}. A per-sheet failure (a locale not in config, a broken-round-trip key,
1148
1672
  * a write failure) is isolated as that locale's `status: "failed"`, not a throw; per-row rejections
1149
1673
  * are withheld and reported on the locale. Dry-run validates and reports without writing any locale or
1150
- * lock file.
1674
+ * lock file, and skips lock acquisition (there is nothing to protect).
1675
+ *
1676
+ * The lock-file is read once, up front, for every sheet's diff baseline. On a non-dry-run, each
1677
+ * sheet's write-and-lock-update step then holds that locale's `withLocaleWriteLock` for its whole
1678
+ * critical section, so a concurrent writer touching the same locale can never interleave with it.
1151
1679
  *
1152
1680
  * @param input - The validated config, the workbook path, and run options.
1153
1681
  * @param deps - Optional composition seams (registry, file system) for tests.
@@ -1317,4 +1845,4 @@ interface WatchController {
1317
1845
  */
1318
1846
  declare function watch(input: WatchInput, deps?: WatchDeps): Promise<WatchController>;
1319
1847
 
1320
- 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 };
1848
+ export { type BudgetBehavior, type CheckDeps, type CheckInput, type CheckSummary, type ConfigSource, type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type DiffDeps, type DiffInput, type DiffSummary, type EditEntryDeps, type EditEntryInput, type EditEntryResult, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type GlossaryProvenance, type ImportWorkbookDeps, type ImportWorkbookInput, type KeyIntegrityDeps, type KeyIntegrityEntry, type KeyIntegrityInput, type KeyValueDeps, type KeyValueInput, type KeyValueResult, LOCK_FILE_NAME, type LoadConfigOptions, type LoadLockFileDeps, type LoadLockFileInput, type LoadedConfig, type LocaleCheckSummary, type LocaleDiff, type LocaleFileSnapshot, type LocaleKeyIntegrity, type LocaleNotice, type LocaleSnapshotDelta, type LocaleSummary, type LockFile, type LockLocaleState, type LockStateDeps, type LockStateInput, type LockStateResult, type NeedsReviewEntry, type ProviderConfig, type ProviderId, type ReadLocaleFileSnapshotDeps, type ReadLocaleFileSnapshotInput, type RetranslateEntryDeps, type RetranslateEntryInput, type RetranslateEntryResult, type ReviewReasonCode, type RunBudget, type RunStatusDeps, type RunStatusFile, type RunStatusInput, type RunStatusLocale, type RunStatusResult, 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 UsageSummary, type VerbatraConfig, type VerbatraConfigInput, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, check, defineConfig, diff, diffLocaleSnapshots, editEntry, exportWorkbook, importWorkbook, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, lockState, readLocaleFileSnapshot, retranslateEntry, runStatus, scaffoldingMetadata, translate, verbatraConfigSchema, watch };