@verbatra/sdk 0.5.0-next.4 → 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.js CHANGED
@@ -3,7 +3,7 @@ import { dirname, resolve, join, basename, extname } from 'path';
3
3
  import { cosmiconfig } from 'cosmiconfig';
4
4
  import { TypeScriptLoader } from 'cosmiconfig-typescript-loader';
5
5
  import { randomUUID } from 'crypto';
6
- import { access, writeFile, rename, rm, open } from 'fs/promises';
6
+ import { rm, access, mkdir, open, writeFile, rename } from 'fs/promises';
7
7
  import { z } from 'zod';
8
8
  import Anthropic from '@anthropic-ai/sdk';
9
9
  import * as deepl from 'deepl-node';
@@ -115,6 +115,24 @@ async function atomicWrite(path, data) {
115
115
  throw error;
116
116
  }
117
117
  }
118
+ async function createExclusive(path, data) {
119
+ await mkdir(dirname(path), { recursive: true });
120
+ let handle;
121
+ try {
122
+ handle = await open(path, "wx");
123
+ } catch (error) {
124
+ if (error.code === "EEXIST") {
125
+ return false;
126
+ }
127
+ throw error;
128
+ }
129
+ try {
130
+ await handle.writeFile(data, "utf8");
131
+ } finally {
132
+ await handle.close();
133
+ }
134
+ return true;
135
+ }
118
136
  var defaultFs = {
119
137
  async fileExists(path) {
120
138
  try {
@@ -127,7 +145,11 @@ var defaultFs = {
127
145
  readFileBounded: (path, maxBytes) => readBounded(path, maxBytes),
128
146
  readBytesBounded: (path, maxBytes) => readBoundedBytes(path, maxBytes),
129
147
  writeFile: (path, data) => atomicWrite(path, data),
130
- writeBytes: (path, data) => atomicWrite(path, data)
148
+ writeBytes: (path, data) => atomicWrite(path, data),
149
+ createExclusive: (path, data) => createExclusive(path, data),
150
+ deleteFile: async (path) => {
151
+ await rm(path, { force: true });
152
+ }
131
153
  };
132
154
  var MAX_GLOSSARY_FILE_BYTES = 1024 * 1024;
133
155
  var glossaryRecordSchema = z.record(z.string(), z.string());
@@ -316,29 +338,18 @@ var TIMEOUT_STATUS = 408;
316
338
  var AUTH_FAILED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
317
339
  var RATE_LIMITED_CLASS_NAMES = /* @__PURE__ */ new Set([
318
340
  "RateLimitError",
319
- // openai, @anthropic-ai/sdk
320
341
  "TooManyRequestsError"
321
- // deepl-node
322
342
  ]);
323
343
  var AUTH_FAILED_CLASS_NAMES = /* @__PURE__ */ new Set([
324
344
  "AuthenticationError",
325
- // openai, @anthropic-ai/sdk (401)
326
345
  "PermissionDeniedError",
327
- // openai, @anthropic-ai/sdk (403)
328
346
  "AuthorizationError"
329
- // deepl-node
330
347
  ]);
331
348
  var TIMEOUT_CLASS_NAMES = /* @__PURE__ */ new Set([
332
349
  "APIConnectionTimeoutError",
333
- // openai, @anthropic-ai/sdk
334
- // deepl-node has no distinct timeout class; ConnectionError is its only network-failure
335
- // class, so it is folded into TIMEOUT rather than the PROVIDER_ERROR fallback.
336
350
  "ConnectionError"
337
351
  ]);
338
- var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set([
339
- "APIUserAbortError"
340
- // openai, @anthropic-ai/sdk
341
- ]);
352
+ var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set(["APIUserAbortError"]);
342
353
  function getErrorStatus(error) {
343
354
  if (typeof error !== "object" || error === null || !("status" in error)) {
344
355
  return void 0;
@@ -392,7 +403,6 @@ function isAbortError(error, signal) {
392
403
  }
393
404
  var REDACTED = "[REDACTED]";
394
405
  var KEY_PATTERNS = [
395
- // The `\b` anchors `sk-` to a word start so hyphenated words like "risk-" or "task-" pass through.
396
406
  /\bsk-[A-Za-z0-9_-]{8,}/g,
397
407
  /AIza[0-9A-Za-z_-]{35}/g,
398
408
  /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(?::fx)?/g
@@ -414,6 +424,10 @@ var ProviderError = class extends Error {
414
424
  /** The stable {@link ProviderErrorCode} for this failure; branch on this, not the message. */
415
425
  code;
416
426
  /**
427
+ * Redacts the message as a defense-in-depth backstop before storing it. The redaction runs with an
428
+ * empty secret argument so the ANTHROPIC_API_KEY environment default is not re-applied, keeping
429
+ * this generic error decoupled from any one provider's environment variable.
430
+ *
417
431
  * @param code - The stable failure code.
418
432
  * @param message - A fixed, safe message; callers must never pass key, SDK, or request-derived text.
419
433
  */
@@ -981,9 +995,6 @@ function createDeepLProvider(config, deps = {}) {
981
995
  return {
982
996
  id: PROVIDER_ID2,
983
997
  kind: "machine-translation",
984
- // Honest capability signal: DeepL only ever applies a pre-created native glossary id, never a
985
- // generic term map (see buildTranslateOptions). Without one configured, a supplied term map is
986
- // silently ignored and surfaces only as a GLOSSARY_IGNORED notice.
987
998
  supportsGlossary: validConfig.glossaryId !== void 0,
988
999
  translateBatch: (request) => translate(bundle, validConfig, request)
989
1000
  };
@@ -1095,14 +1106,14 @@ function throwAbort(signal) {
1095
1106
  throw new DOMException("This operation was aborted.", "AbortError");
1096
1107
  }
1097
1108
  function delay(ms, signal) {
1098
- return new Promise((resolve7) => {
1109
+ return new Promise((resolve9) => {
1099
1110
  const onAbort = () => {
1100
1111
  clearTimeout(timer);
1101
- resolve7();
1112
+ resolve9();
1102
1113
  };
1103
1114
  const timer = setTimeout(() => {
1104
1115
  signal?.removeEventListener("abort", onAbort);
1105
- resolve7();
1116
+ resolve9();
1106
1117
  }, ms);
1107
1118
  signal?.addEventListener("abort", onAbort, { once: true });
1108
1119
  });
@@ -1150,12 +1161,10 @@ var TYPE_MAP = {
1150
1161
  object: "OBJECT"
1151
1162
  };
1152
1163
  var HANDLED_KEYWORDS = /* @__PURE__ */ new Set([
1153
- // transformed or recursed
1154
1164
  "type",
1155
1165
  "required",
1156
1166
  "properties",
1157
1167
  "items",
1158
- // deliberately dropped (not part of Google's Schema dialect)
1159
1168
  "$schema",
1160
1169
  "additionalProperties"
1161
1170
  ]);
@@ -1723,6 +1732,60 @@ async function loadConfig(options = {}) {
1723
1732
  const { config } = await loadConfigWithMeta(options);
1724
1733
  return config;
1725
1734
  }
1735
+ var LOCAL_DIR_NAME = ".verbatra-local";
1736
+ var LOCK_FILE_GUARD_STEM = "_lockfile";
1737
+ var DEFAULT_POLL_INTERVAL_MS = 100;
1738
+ var DEFAULT_ACQUIRE_TIMEOUT_MS = 10 * 6e4;
1739
+ function lockPath(cwd, stem) {
1740
+ return resolve(cwd, LOCAL_DIR_NAME, "locks", `${stem}.lock`);
1741
+ }
1742
+ function localeLockPath(cwd, locale) {
1743
+ return lockPath(cwd, locale);
1744
+ }
1745
+ function lockFileGuardPath(cwd) {
1746
+ return lockPath(cwd, LOCK_FILE_GUARD_STEM);
1747
+ }
1748
+ function sleep(ms) {
1749
+ return new Promise((res) => {
1750
+ setTimeout(res, ms);
1751
+ });
1752
+ }
1753
+ function lockPayload() {
1754
+ return JSON.stringify({ pid: process.pid, acquiredAt: (/* @__PURE__ */ new Date()).toISOString() });
1755
+ }
1756
+ async function acquireLock(path, fs, pollIntervalMs, deadline) {
1757
+ for (; ; ) {
1758
+ if (await fs.createExclusive(path, lockPayload())) {
1759
+ return;
1760
+ }
1761
+ if (Date.now() >= deadline) {
1762
+ throw new SdkError(
1763
+ "LOCK_CONTENDED",
1764
+ `Could not acquire the write lock at ${path}: another process may be holding it. If no verbatra process is currently running, this lock file was likely left behind by one that was killed; delete it and retry.`
1765
+ );
1766
+ }
1767
+ const jitter = Math.random() * pollIntervalMs;
1768
+ await sleep(pollIntervalMs + jitter);
1769
+ }
1770
+ }
1771
+ async function withFileLock(path, fs, fn, options) {
1772
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
1773
+ const acquireTimeoutMs = options.acquireTimeoutMs ?? DEFAULT_ACQUIRE_TIMEOUT_MS;
1774
+ await acquireLock(path, fs, pollIntervalMs, Date.now() + acquireTimeoutMs);
1775
+ try {
1776
+ return await fn();
1777
+ } finally {
1778
+ await fs.deleteFile(path);
1779
+ }
1780
+ }
1781
+ async function withLocaleWriteLock(cwd, locale, fs, fn, options = {}) {
1782
+ return withFileLock(localeLockPath(cwd, locale), fs, fn, options);
1783
+ }
1784
+ async function withLockFileGuard(cwd, fs, fn, options = {}) {
1785
+ return withFileLock(lockFileGuardPath(cwd), fs, fn, options);
1786
+ }
1787
+
1788
+ // src/lock/lock-file.ts
1726
1789
  var LOCK_FILE_NAME = "verbatra.lock.json";
1727
1790
  var CURRENT_VERSION = 1;
1728
1791
  var EMPTY_LOCK = { version: CURRENT_VERSION, locales: {} };
@@ -1734,8 +1797,7 @@ var lockFileSchema = z.object({
1734
1797
  function lockFilePath(cwd) {
1735
1798
  return resolve(cwd, LOCK_FILE_NAME);
1736
1799
  }
1737
- async function readLockFile(path, fs) {
1738
- const read = await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES);
1800
+ function parseLockFileRead(read, path) {
1739
1801
  if (read.kind === "missing") {
1740
1802
  return EMPTY_LOCK;
1741
1803
  }
@@ -1763,6 +1825,9 @@ async function readLockFile(path, fs) {
1763
1825
  }
1764
1826
  return result.data;
1765
1827
  }
1828
+ async function readLockFile(path, fs) {
1829
+ return parseLockFileRead(await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES), path);
1830
+ }
1766
1831
  function baselineFor(lock, locale) {
1767
1832
  return new Map(Object.entries(lock.locales[locale] ?? {}));
1768
1833
  }
@@ -1778,14 +1843,30 @@ function byKey(a, b) {
1778
1843
  function sortRecord(record) {
1779
1844
  return Object.fromEntries(Object.entries(record).sort(byKey));
1780
1845
  }
1781
- async function writeLockFile(path, lock, fs) {
1846
+ function serializeLockFile(lock) {
1782
1847
  const locales = {};
1783
1848
  for (const [locale, entries] of Object.entries(lock.locales).sort(byKey)) {
1784
1849
  locales[locale] = sortRecord(entries);
1785
1850
  }
1786
1851
  const ordered = { version: lock.version, locales };
1787
- await fs.writeFile(path, `${JSON.stringify(ordered, null, 2)}
1788
- `);
1852
+ return `${JSON.stringify(ordered, null, 2)}
1853
+ `;
1854
+ }
1855
+ function applyLockLocalePatch(currentEntries, patch) {
1856
+ if (patch.mode === "replace") {
1857
+ return patch.entries;
1858
+ }
1859
+ return { ...currentEntries, ...patch.entries };
1860
+ }
1861
+ async function updateLockFileLocale(cwd, fs, locale, patch) {
1862
+ return withLockFileGuard(cwd, fs, async () => {
1863
+ const path = lockFilePath(cwd);
1864
+ const lock = parseLockFileRead(await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES), path);
1865
+ const nextEntries = applyLockLocalePatch(lock.locales[locale], patch);
1866
+ const next = updateLockLocale(lock, locale, nextEntries);
1867
+ await fs.writeFile(path, serializeLockFile(next));
1868
+ return next;
1869
+ });
1789
1870
  }
1790
1871
  var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
1791
1872
  var INVALID = { placeholders: [], isPlural: false, valid: false };
@@ -2047,10 +2128,68 @@ var AdapterError = class extends Error {
2047
2128
  };
2048
2129
  var MAX_DEPTH = 100;
2049
2130
  var MAX_INPUT_BYTES = 16 * 1024 * 1024;
2050
- var jsonTreeSchema = z.lazy(
2051
- () => z.union([z.string(), z.number(), z.boolean(), z.null(), z.record(z.string(), jsonTreeSchema)])
2052
- );
2053
- var rootSchema = z.record(z.string(), jsonTreeSchema);
2131
+ var SENTINEL_ESCAPE = "\\u0001";
2132
+ function isJsonWhitespace(char) {
2133
+ return char === " " || char === " " || char === "\n" || char === "\r";
2134
+ }
2135
+ function endOfStringToken(content, openQuote) {
2136
+ let index = openQuote + 1;
2137
+ while (index < content.length) {
2138
+ const char = content[index];
2139
+ if (char === "\\") {
2140
+ index += 2;
2141
+ } else if (char === '"') {
2142
+ return index + 1;
2143
+ } else {
2144
+ index += 1;
2145
+ }
2146
+ }
2147
+ return index;
2148
+ }
2149
+ function isKeyToken(content, end) {
2150
+ let index = end;
2151
+ while (isJsonWhitespace(content[index])) {
2152
+ index += 1;
2153
+ }
2154
+ return content[index] === ":";
2155
+ }
2156
+ function prefixKeyToken(token) {
2157
+ if (token.includes(SENTINEL_ESCAPE)) {
2158
+ throw new AdapterError(
2159
+ "INVALID_STRUCTURE",
2160
+ "A key contains a reserved control-character escape."
2161
+ );
2162
+ }
2163
+ return `"${SENTINEL_ESCAPE}${token.slice(1)}`;
2164
+ }
2165
+ function prefixKeyTokens(content) {
2166
+ let out = "";
2167
+ let index = 0;
2168
+ while (index < content.length) {
2169
+ const openQuote = content.indexOf('"', index);
2170
+ if (openQuote === -1) {
2171
+ return out + content.slice(index);
2172
+ }
2173
+ const end = endOfStringToken(content, openQuote);
2174
+ const token = content.slice(openQuote, end);
2175
+ out += content.slice(index, openQuote);
2176
+ out += isKeyToken(content, end) ? prefixKeyToken(token) : token;
2177
+ index = end;
2178
+ }
2179
+ return out;
2180
+ }
2181
+ function childrenOf(node) {
2182
+ if (node instanceof Map) {
2183
+ return node.values();
2184
+ }
2185
+ if (Array.isArray(node)) {
2186
+ return node;
2187
+ }
2188
+ if (typeof node === "object" && node !== null) {
2189
+ return Object.values(node);
2190
+ }
2191
+ return null;
2192
+ }
2054
2193
  function assertWithinDepth(value, max) {
2055
2194
  const stack = [{ node: value, depth: 1 }];
2056
2195
  while (stack.length > 0) {
@@ -2058,44 +2197,117 @@ function assertWithinDepth(value, max) {
2058
2197
  if (top === void 0) {
2059
2198
  break;
2060
2199
  }
2061
- const { node, depth } = top;
2062
- if (typeof node !== "object" || node === null) {
2200
+ const children = childrenOf(top.node);
2201
+ if (children === null) {
2063
2202
  continue;
2064
2203
  }
2065
- if (depth > max) {
2204
+ if (top.depth > max) {
2066
2205
  throw new AdapterError("MAX_DEPTH_EXCEEDED", "The file nests objects too deeply.");
2067
2206
  }
2068
- for (const child of Object.values(node)) {
2069
- stack.push({ node: child, depth: depth + 1 });
2207
+ for (const child of children) {
2208
+ stack.push({ node: child, depth: top.depth + 1 });
2070
2209
  }
2071
2210
  }
2072
2211
  }
2073
- function assertJsonRecord(value) {
2074
- assertWithinDepth(value, MAX_DEPTH);
2075
- const result = rootSchema.safeParse(value);
2076
- if (!result.success) {
2077
- throw new AdapterError(
2078
- "INVALID_STRUCTURE",
2079
- "The file is not a valid object (expected nested objects of string, number, boolean, or null leaves)."
2080
- );
2212
+ function toOrdered(node) {
2213
+ if (Array.isArray(node)) {
2214
+ return node.map((child) => toOrdered(child));
2081
2215
  }
2082
- return result.data;
2216
+ if (typeof node === "object" && node !== null) {
2217
+ const out = /* @__PURE__ */ new Map();
2218
+ for (const [key, child] of Object.entries(node)) {
2219
+ out.set(key.slice(1), toOrdered(child));
2220
+ }
2221
+ return out;
2222
+ }
2223
+ return node;
2083
2224
  }
2084
- function parseJsonObject(content) {
2225
+ function parseOrderedJson(content) {
2226
+ const prefixed = prefixKeyTokens(content);
2085
2227
  let parsed;
2086
2228
  try {
2087
- parsed = JSON.parse(content);
2229
+ parsed = JSON.parse(prefixed);
2088
2230
  } catch {
2089
2231
  throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
2090
2232
  }
2091
- return assertJsonRecord(parsed);
2233
+ assertWithinDepth(parsed, MAX_DEPTH);
2234
+ return toOrdered(parsed);
2092
2235
  }
2093
- function serializeJsonTree(tree) {
2094
- return `${JSON.stringify(tree, null, 2)}
2236
+ function printRecord(record, indent) {
2237
+ if (record.size === 0) {
2238
+ return "{}";
2239
+ }
2240
+ const childIndent = `${indent} `;
2241
+ const lines = [];
2242
+ for (const [key, child] of record) {
2243
+ lines.push(`${childIndent}${JSON.stringify(key)}: ${printValue(child, childIndent)}`);
2244
+ }
2245
+ return `{
2246
+ ${lines.join(",\n")}
2247
+ ${indent}}`;
2248
+ }
2249
+ function printArray(items, indent) {
2250
+ if (items.length === 0) {
2251
+ return "[]";
2252
+ }
2253
+ const childIndent = `${indent} `;
2254
+ const lines = items.map((item) => `${childIndent}${printValue(item, childIndent)}`);
2255
+ return `[
2256
+ ${lines.join(",\n")}
2257
+ ${indent}]`;
2258
+ }
2259
+ function printValue(value, indent) {
2260
+ if (value instanceof Map) {
2261
+ return printRecord(value, indent);
2262
+ }
2263
+ if (Array.isArray(value)) {
2264
+ return printArray(value, indent);
2265
+ }
2266
+ return JSON.stringify(value);
2267
+ }
2268
+ function serializeOrderedJson(value) {
2269
+ return `${printValue(value, "")}
2095
2270
  `;
2096
2271
  }
2272
+ var INVALID_STRUCTURE_MESSAGE = "The file is not a valid object (expected nested objects of string, number, boolean, or null leaves).";
2273
+ function isJsonLeaf(value) {
2274
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null;
2275
+ }
2276
+ function assertNodesValid(root) {
2277
+ const stack = [root];
2278
+ while (stack.length > 0) {
2279
+ const node = stack.pop();
2280
+ if (node === void 0) {
2281
+ break;
2282
+ }
2283
+ for (const [key, child] of node) {
2284
+ if (typeof key !== "string") {
2285
+ throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
2286
+ }
2287
+ if (child instanceof Map) {
2288
+ stack.push(child);
2289
+ } else if (!isJsonLeaf(child)) {
2290
+ throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
2291
+ }
2292
+ }
2293
+ }
2294
+ }
2295
+ function assertJsonRecord(value) {
2296
+ assertWithinDepth(value, MAX_DEPTH);
2297
+ if (!(value instanceof Map)) {
2298
+ throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
2299
+ }
2300
+ assertNodesValid(value);
2301
+ return value;
2302
+ }
2303
+ function parseJsonObject(content) {
2304
+ return assertJsonRecord(parseOrderedJson(content));
2305
+ }
2306
+ function serializeJsonTree(tree) {
2307
+ return serializeOrderedJson(tree);
2308
+ }
2097
2309
  function isJsonNode(value) {
2098
- return typeof value === "object" && value !== null;
2310
+ return value instanceof Map;
2099
2311
  }
2100
2312
  function namespaceOf(filePath) {
2101
2313
  return basename(filePath, extname(filePath));
@@ -2297,7 +2509,7 @@ function addLeaf(ctx, segments, key, value) {
2297
2509
  ctx.out.set(mapKey, { key: mapKey, namespace: ctx.namespace, value, placeholders, isPlural });
2298
2510
  }
2299
2511
  function addEntries(ctx, prefix, node) {
2300
- for (const [key, value] of Object.entries(node)) {
2512
+ for (const [key, value] of node) {
2301
2513
  const segments = [...prefix, key];
2302
2514
  if (isJsonNode(value)) {
2303
2515
  addEntries(ctx, segments, value);
@@ -2307,7 +2519,7 @@ function addEntries(ctx, prefix, node) {
2307
2519
  }
2308
2520
  }
2309
2521
  function addPathEntries(node, prefix, namespace, derive, out, claimedPaths, excluded) {
2310
- for (const [key, value] of Object.entries(node)) {
2522
+ for (const [key, value] of node) {
2311
2523
  const path = prefix === "" ? key : `${prefix}.${key}`;
2312
2524
  if (claimedPaths.has(path)) {
2313
2525
  throw new AdapterError(
@@ -2337,17 +2549,14 @@ function flattenTree(tree, namespace, derive, keyMode = "literal-leaf") {
2337
2549
  addEntries(ctx, [], tree);
2338
2550
  return { entries: out, excludedLeafPaths: ctx.excluded };
2339
2551
  }
2340
- function emptyNode() {
2341
- return /* @__PURE__ */ Object.create(null);
2342
- }
2343
2552
  function descend(node, segment) {
2344
- const next = node[segment];
2553
+ const next = node.get(segment);
2345
2554
  if (next === void 0) {
2346
- const created = emptyNode();
2347
- node[segment] = created;
2555
+ const created = /* @__PURE__ */ new Map();
2556
+ node.set(segment, created);
2348
2557
  return created;
2349
2558
  }
2350
- if (typeof next === "object") {
2559
+ if (next instanceof Map) {
2351
2560
  return next;
2352
2561
  }
2353
2562
  throw new AdapterError("INVALID_STRUCTURE", "A leaf key collides with a nested key path.");
@@ -2361,13 +2570,13 @@ function setPath(root, segments, value) {
2361
2570
  for (const segment of segments.slice(0, -1)) {
2362
2571
  node = descend(node, segment);
2363
2572
  }
2364
- if (typeof node[leaf] === "object") {
2573
+ if (node.get(leaf) instanceof Map) {
2365
2574
  throw new AdapterError("INVALID_STRUCTURE", "A leaf key collides with a nested key path.");
2366
2575
  }
2367
- node[leaf] = value;
2576
+ node.set(leaf, value);
2368
2577
  }
2369
2578
  function unflattenEntries(entries) {
2370
- const root = emptyNode();
2579
+ const root = /* @__PURE__ */ new Map();
2371
2580
  for (const [key, entry] of entries) {
2372
2581
  setPath(root, decodeKeyToSegments(key), entry.value);
2373
2582
  }
@@ -2444,13 +2653,8 @@ function isMetadataKey(key) {
2444
2653
  return key.startsWith("@");
2445
2654
  }
2446
2655
  function parseArbObject(content) {
2447
- let parsed;
2448
- try {
2449
- parsed = JSON.parse(content);
2450
- } catch {
2451
- throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
2452
- }
2453
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2656
+ const parsed = parseOrderedJson(content);
2657
+ if (!(parsed instanceof Map)) {
2454
2658
  throw new AdapterError(
2455
2659
  "INVALID_STRUCTURE",
2456
2660
  "The file is not a valid object (expected nested objects of string values)."
@@ -2459,10 +2663,10 @@ function parseArbObject(content) {
2459
2663
  return parsed;
2460
2664
  }
2461
2665
  function stripArbMetadata(tree) {
2462
- const out = /* @__PURE__ */ Object.create(null);
2463
- for (const [key, value] of Object.entries(tree)) {
2666
+ const out = /* @__PURE__ */ new Map();
2667
+ for (const [key, value] of tree) {
2464
2668
  if (!isMetadataKey(key)) {
2465
- out[key] = value;
2669
+ out.set(key, value);
2466
2670
  }
2467
2671
  }
2468
2672
  return out;
@@ -2474,16 +2678,16 @@ function messageKeyForMetadata(key) {
2474
2678
  return isMetadataKey(key) && !key.startsWith("@@") ? key.slice(1) : null;
2475
2679
  }
2476
2680
  function descriptionOf(value) {
2477
- if (typeof value !== "object" || value === null) {
2681
+ if (!(value instanceof Map)) {
2478
2682
  return void 0;
2479
2683
  }
2480
- const description = value.description;
2684
+ const description = value.get("description");
2481
2685
  return typeof description === "string" ? description : void 0;
2482
2686
  }
2483
2687
  function extractArbDescriptions(content) {
2484
2688
  const tree = parseArbObject(content);
2485
2689
  const out = /* @__PURE__ */ new Map();
2486
- for (const [key, value] of Object.entries(tree)) {
2690
+ for (const [key, value] of tree) {
2487
2691
  const messageKey = messageKeyForMetadata(key);
2488
2692
  if (messageKey === null) {
2489
2693
  continue;
@@ -2515,25 +2719,25 @@ async function readDestinationPairs(filePath) {
2515
2719
  }
2516
2720
  throw error;
2517
2721
  }
2518
- return Object.entries(parseArbObject(content));
2722
+ return [...parseArbObject(content)];
2519
2723
  }
2520
2724
  async function buildArbWriteTree(entries, filePath) {
2521
2725
  const messages = messagesFromEntries(entries);
2522
2726
  const pairs = await readDestinationPairs(filePath);
2523
- const out = /* @__PURE__ */ Object.create(null);
2727
+ const out = /* @__PURE__ */ new Map();
2524
2728
  const consumed = /* @__PURE__ */ new Set();
2525
2729
  for (const [key, value] of pairs ?? []) {
2526
2730
  const translated = isMetadataKey(key) ? void 0 : messages.get(key);
2527
2731
  if (translated !== void 0) {
2528
2732
  consumed.add(key);
2529
- out[key] = translated;
2733
+ out.set(key, translated);
2530
2734
  } else if (isMetadataKey(key) || typeof value === "string") {
2531
- out[key] = value;
2735
+ out.set(key, value);
2532
2736
  }
2533
2737
  }
2534
2738
  for (const [key, value] of messages) {
2535
2739
  if (!consumed.has(key)) {
2536
- out[key] = value;
2740
+ out.set(key, value);
2537
2741
  }
2538
2742
  }
2539
2743
  return out;
@@ -2622,7 +2826,7 @@ function createNextIntlJsonAdapter() {
2622
2826
  });
2623
2827
  }
2624
2828
  function assertNoDottedNestedKey(tree) {
2625
- for (const [key, value] of Object.entries(tree)) {
2829
+ for (const [key, value] of tree) {
2626
2830
  if (!isJsonNode(value)) {
2627
2831
  continue;
2628
2832
  }
@@ -2638,7 +2842,7 @@ function assertNoDottedNestedKey(tree) {
2638
2842
  function assertNotMixed(tree) {
2639
2843
  let hasNested = false;
2640
2844
  let hasFlatDottedKey = false;
2641
- for (const [key, value] of Object.entries(tree)) {
2845
+ for (const [key, value] of tree) {
2642
2846
  if (isJsonNode(value)) {
2643
2847
  hasNested = true;
2644
2848
  } else if (key.includes(".")) {
@@ -2675,9 +2879,9 @@ async function detectStyle(filePath) {
2675
2879
  return "flat";
2676
2880
  }
2677
2881
  function buildFlatTree(entries) {
2678
- const out = /* @__PURE__ */ Object.create(null);
2882
+ const out = /* @__PURE__ */ new Map();
2679
2883
  for (const [key, entry] of entries) {
2680
- out[key] = entry.value;
2884
+ out.set(key, entry.value);
2681
2885
  }
2682
2886
  return out;
2683
2887
  }
@@ -2695,7 +2899,6 @@ function createNgxTranslateJsonAdapter() {
2695
2899
  }),
2696
2900
  validateTree: assertNotMixed,
2697
2901
  buildWriteTree: buildNgxWriteTree,
2698
- // ngx-translate flat style uses dotted keys as path notation, not literal leaves.
2699
2902
  keyMode: "path-notation"
2700
2903
  });
2701
2904
  }
@@ -2721,6 +2924,11 @@ var AdapterRegistry = class {
2721
2924
  }
2722
2925
  return { status: "resolved", adapter };
2723
2926
  }
2927
+ /**
2928
+ * Resolve by asking every registered adapter's `canHandle`. Several adapters can claim one file
2929
+ * (all JSON adapters claim `.json`), so more than one match is reported as `ambiguous` rather
2930
+ * than guessed at.
2931
+ */
2724
2932
  resolveByDetection(filePath, sample) {
2725
2933
  const matches = this.adapters.filter((adapter) => adapter.canHandle(filePath, sample));
2726
2934
  const first = matches[0];
@@ -3075,14 +3283,34 @@ function createXliffAdapter() {
3075
3283
  extractPlaceholders: extractXliffPlaceholders
3076
3284
  });
3077
3285
  }
3286
+ function normalizeKey(key) {
3287
+ if (typeof key === "object" && key !== null) {
3288
+ throw new AdapterError(
3289
+ "INVALID_STRUCTURE",
3290
+ "A mapping key is a map or sequence (expected scalar keys)."
3291
+ );
3292
+ }
3293
+ return String(key);
3294
+ }
3295
+ function normalizeYamlTree(value) {
3296
+ if (!(value instanceof Map)) {
3297
+ return value;
3298
+ }
3299
+ const out = /* @__PURE__ */ new Map();
3300
+ for (const [key, child] of value) {
3301
+ out.set(normalizeKey(key), normalizeYamlTree(child));
3302
+ }
3303
+ return out;
3304
+ }
3078
3305
  function parseYamlObject(content) {
3079
3306
  let parsed;
3080
3307
  try {
3081
- parsed = parse$1(content, { maxAliasCount: 100 });
3308
+ parsed = parse$1(content, { mapAsMap: true, maxAliasCount: 100 });
3082
3309
  } catch {
3083
3310
  throw new AdapterError("INVALID_YAML", "The file is not valid YAML.");
3084
3311
  }
3085
- return assertJsonRecord(parsed);
3312
+ assertWithinDepth(parsed, MAX_DEPTH);
3313
+ return assertJsonRecord(normalizeYamlTree(parsed));
3086
3314
  }
3087
3315
  function createYamlAdapter() {
3088
3316
  return createTreeFileAdapter({
@@ -3113,6 +3341,15 @@ function selectAdapter(format, registry = createDefaultRegistry()) {
3113
3341
  );
3114
3342
  }
3115
3343
 
3344
+ // src/flow/read-target.ts
3345
+ async function readTargetResource(input) {
3346
+ const path = localeFilePath(input.cwd, input.filesPattern, input.locale);
3347
+ if (!await input.fs.fileExists(path)) {
3348
+ return { locale: input.locale, namespace: "", format: input.format, entries: /* @__PURE__ */ new Map() };
3349
+ }
3350
+ return (await input.adapter.read(path, input.locale)).resource;
3351
+ }
3352
+
3116
3353
  // src/flow/select-locales.ts
3117
3354
  function selectLocales(config, requested) {
3118
3355
  if (requested === void 0) {
@@ -3153,11 +3390,14 @@ async function readSource(config, cwd, fs, adapter) {
3153
3390
 
3154
3391
  // src/flow/diff-locales.ts
3155
3392
  async function readTarget(cwd, config, adapter, fs, locale) {
3156
- const path = localeFilePath(cwd, config.files.pattern, locale);
3157
- if (!await fs.fileExists(path)) {
3158
- return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
3159
- }
3160
- return (await adapter.read(path, locale)).resource;
3393
+ return readTargetResource({
3394
+ cwd,
3395
+ filesPattern: config.files.pattern,
3396
+ format: config.format,
3397
+ locale,
3398
+ adapter,
3399
+ fs
3400
+ });
3161
3401
  }
3162
3402
  async function diffLocales(input, deps = {}) {
3163
3403
  const config = input.config;
@@ -3207,6 +3447,166 @@ async function diff(input, deps = {}) {
3207
3447
  return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
3208
3448
  }
3209
3449
 
3450
+ // src/flow/integrity-gate.ts
3451
+ function gateCandidateValue(sourceEntry, candidateValue, adapter) {
3452
+ const placeholderResult = adapter.comparePlaceholders?.(sourceEntry.value, candidateValue) ?? checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(candidateValue));
3453
+ if (!placeholderResult.matches) {
3454
+ return { accepted: false, reason: "placeholder" };
3455
+ }
3456
+ if (!adapter.validateMessage(candidateValue)) {
3457
+ return { accepted: false, reason: "icu" };
3458
+ }
3459
+ return { accepted: true };
3460
+ }
3461
+
3462
+ // src/flow/edit-entry.ts
3463
+ async function editEntry(input, deps = {}) {
3464
+ const config = input.config;
3465
+ const cwd = input.cwd ?? process.cwd();
3466
+ const fs = deps.fs ?? defaultFs;
3467
+ const adapter = selectAdapter(config.format, deps.adapterRegistry);
3468
+ const [locale] = selectLocales(config, [input.locale]);
3469
+ if (locale === void 0) {
3470
+ throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
3471
+ }
3472
+ const source = await readSource(config, cwd, fs, adapter);
3473
+ const sourceEntry = source.resource.entries.get(input.key);
3474
+ if (sourceEntry === void 0) {
3475
+ throw new SdkError(
3476
+ "UNKNOWN_KEY",
3477
+ `The key "${input.key}" was not found in the source resource.`
3478
+ );
3479
+ }
3480
+ return withLocaleWriteLock(cwd, locale, fs, async () => {
3481
+ const target = await readTarget(cwd, config, adapter, fs, locale);
3482
+ const gate = gateCandidateValue(sourceEntry, input.value, adapter);
3483
+ if (!gate.accepted) {
3484
+ return { accepted: false, reason: gate.reason, value: input.value };
3485
+ }
3486
+ const merged = new Map(target.entries);
3487
+ merged.set(input.key, { ...sourceEntry, value: input.value, namespace: target.namespace });
3488
+ const path = localeFilePath(cwd, config.files.pattern, locale);
3489
+ await adapter.write(
3490
+ { locale, namespace: target.namespace, format: config.format, entries: merged },
3491
+ path
3492
+ );
3493
+ await updateLockFileLocale(cwd, fs, locale, {
3494
+ mode: "merge",
3495
+ entries: { [input.key]: contentHash(sourceEntry) }
3496
+ });
3497
+ return { accepted: true, value: input.value };
3498
+ });
3499
+ }
3500
+
3501
+ // src/flow/key-integrity.ts
3502
+ function checkEntryIntegrity(adapter, sourceEntry, targetEntry) {
3503
+ const result = adapter.comparePlaceholders?.(sourceEntry.value, targetEntry.value) ?? checkPlaceholders(sourceEntry.placeholders, targetEntry.placeholders);
3504
+ return {
3505
+ key: sourceEntry.key,
3506
+ hasPlaceholders: sourceEntry.placeholders.length > 0,
3507
+ matches: result.matches,
3508
+ missing: result.missing,
3509
+ extra: result.extra,
3510
+ icuValid: adapter.validateMessage(targetEntry.value)
3511
+ };
3512
+ }
3513
+ function selectChangedKeys(changed, requested) {
3514
+ if (requested === void 0) {
3515
+ return changed;
3516
+ }
3517
+ const wanted = new Set(requested);
3518
+ return changed.filter((key) => wanted.has(key));
3519
+ }
3520
+ function integrityEntriesFor(source, target, adapter, changedKeys) {
3521
+ const entries = [];
3522
+ for (const key of changedKeys) {
3523
+ const sourceEntry = source.entries.get(key);
3524
+ const targetEntry = target.entries.get(key);
3525
+ if (sourceEntry === void 0 || targetEntry === void 0) {
3526
+ continue;
3527
+ }
3528
+ entries.push(checkEntryIntegrity(adapter, sourceEntry, targetEntry));
3529
+ }
3530
+ return entries;
3531
+ }
3532
+ async function keyIntegrity(input, deps = {}) {
3533
+ const config = input.config;
3534
+ const cwd = input.cwd ?? process.cwd();
3535
+ const fs = deps.fs ?? defaultFs;
3536
+ const adapter = selectAdapter(config.format, deps.adapterRegistry);
3537
+ const source = await readSource(config, cwd, fs, adapter);
3538
+ const lock = await readLockFile(lockFilePath(cwd), fs);
3539
+ return Promise.all(
3540
+ selectLocales(config, input.locales).map(async (locale) => {
3541
+ const target = await readTarget(cwd, config, adapter, fs, locale);
3542
+ const diffResult = diffResources(source.resource, target, {
3543
+ baseline: baselineFor(lock, locale)
3544
+ });
3545
+ const changedKeys = selectChangedKeys(diffResult.changed, input.keys);
3546
+ const entries = integrityEntriesFor(source.resource, target, adapter, changedKeys);
3547
+ return { locale, entries };
3548
+ })
3549
+ );
3550
+ }
3551
+
3552
+ // src/flow/key-value.ts
3553
+ async function keyValue(input, deps = {}) {
3554
+ const config = input.config;
3555
+ const cwd = input.cwd ?? process.cwd();
3556
+ const fs = deps.fs ?? defaultFs;
3557
+ const adapter = selectAdapter(config.format, deps.adapterRegistry);
3558
+ const [locale] = selectLocales(config, [input.locale]);
3559
+ if (locale === void 0) {
3560
+ throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
3561
+ }
3562
+ const source = await readSource(config, cwd, fs, adapter);
3563
+ const sourceEntry = source.resource.entries.get(input.key);
3564
+ if (sourceEntry === void 0) {
3565
+ throw new SdkError(
3566
+ "UNKNOWN_KEY",
3567
+ `The key "${input.key}" was not found in the source resource.`
3568
+ );
3569
+ }
3570
+ const target = await readTarget(cwd, config, adapter, fs, locale);
3571
+ const targetEntry = target.entries.get(input.key);
3572
+ return {
3573
+ source: sourceEntry.value,
3574
+ ...targetEntry !== void 0 ? { target: targetEntry.value } : {}
3575
+ };
3576
+ }
3577
+
3578
+ // src/flow/locale-snapshot.ts
3579
+ async function readLocaleFileSnapshot(input, deps = {}) {
3580
+ const cwd = input.cwd ?? process.cwd();
3581
+ const fs = deps.fs ?? defaultFs;
3582
+ const adapter = selectAdapter(input.config.format, deps.adapterRegistry);
3583
+ const resource = await readTarget(cwd, input.config, adapter, fs, input.locale);
3584
+ const hashes = /* @__PURE__ */ new Map();
3585
+ for (const [key, entry] of resource.entries) {
3586
+ hashes.set(key, contentHash(entry));
3587
+ }
3588
+ return { locale: input.locale, hashes };
3589
+ }
3590
+ function diffLocaleSnapshots(previous, current) {
3591
+ let added = 0;
3592
+ let changed = 0;
3593
+ for (const [key, hash] of current.hashes) {
3594
+ const previousHash = previous.hashes.get(key);
3595
+ if (previousHash === void 0) {
3596
+ added += 1;
3597
+ } else if (previousHash !== hash) {
3598
+ changed += 1;
3599
+ }
3600
+ }
3601
+ let removed = 0;
3602
+ for (const key of previous.hashes.keys()) {
3603
+ if (!current.hashes.has(key)) {
3604
+ removed += 1;
3605
+ }
3606
+ }
3607
+ return { added, changed, removed };
3608
+ }
3609
+
3210
3610
  // src/flow/lock-state.ts
3211
3611
  function toLockLocaleState(locale, keyCount, diff2) {
3212
3612
  return {
@@ -3254,6 +3654,174 @@ function selectProvider(config, createProvider = buildProvider) {
3254
3654
  }
3255
3655
  }
3256
3656
 
3657
+ // src/flow/retranslate-entry.ts
3658
+ function buildSingleEntryRequest(config, locale, sourceEntry, adapter) {
3659
+ return {
3660
+ sourceLocale: config.sourceLocale,
3661
+ targetLocale: locale,
3662
+ entries: [sourceEntry],
3663
+ extractPlaceholders: adapter.extractPlaceholders,
3664
+ ...config.glossary !== void 0 ? { glossary: config.glossary } : {},
3665
+ ...config.tone !== void 0 ? { tone: config.tone } : {},
3666
+ ...adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: adapter.comparePlaceholders } : {}
3667
+ };
3668
+ }
3669
+ async function retranslateEntry(input, deps = {}) {
3670
+ const config = input.config;
3671
+ const cwd = input.cwd ?? process.cwd();
3672
+ const fs = deps.fs ?? defaultFs;
3673
+ const adapter = selectAdapter(config.format, deps.adapterRegistry);
3674
+ const [locale] = selectLocales(config, [input.locale]);
3675
+ if (locale === void 0) {
3676
+ throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
3677
+ }
3678
+ const source = await readSource(config, cwd, fs, adapter);
3679
+ const sourceEntry = source.resource.entries.get(input.key);
3680
+ if (sourceEntry === void 0) {
3681
+ throw new SdkError(
3682
+ "UNKNOWN_KEY",
3683
+ `The key "${input.key}" was not found in the source resource.`
3684
+ );
3685
+ }
3686
+ const provider = selectProvider(config.provider, deps.createProvider);
3687
+ return withLocaleWriteLock(cwd, locale, fs, async () => {
3688
+ const target = await readTarget(cwd, config, adapter, fs, locale);
3689
+ const result = await provider.translateBatch(
3690
+ buildSingleEntryRequest(config, locale, sourceEntry, adapter)
3691
+ );
3692
+ const value = result.values.get(input.key);
3693
+ if (value === void 0) {
3694
+ throw new ProviderError(
3695
+ "INVALID_RESPONSE",
3696
+ `The provider returned no translated value for key "${input.key}".`
3697
+ );
3698
+ }
3699
+ const gate = gateCandidateValue(sourceEntry, value, adapter);
3700
+ if (!gate.accepted) {
3701
+ return { accepted: false, reason: gate.reason, value };
3702
+ }
3703
+ const merged = new Map(target.entries);
3704
+ merged.set(input.key, { ...sourceEntry, value, namespace: target.namespace });
3705
+ const path = localeFilePath(cwd, config.files.pattern, locale);
3706
+ await adapter.write(
3707
+ { locale, namespace: target.namespace, format: config.format, entries: merged },
3708
+ path
3709
+ );
3710
+ await updateLockFileLocale(cwd, fs, locale, {
3711
+ mode: "merge",
3712
+ entries: { [input.key]: contentHash(sourceEntry) }
3713
+ });
3714
+ const reviewReasons = result.reviewFlags?.get(input.key)?.reasons ?? [];
3715
+ return { accepted: true, value, reviewReasons };
3716
+ });
3717
+ }
3718
+ var RUN_STATUS_DIR_NAME = ".verbatra-local";
3719
+ var RUN_STATUS_FILE_NAME = "run-status.json";
3720
+ var CURRENT_VERSION2 = 1;
3721
+ var MAX_RUN_STATUS_FILE_BYTES = 16 * 1024 * 1024;
3722
+ var reviewReasonCodeSchema = z.enum([
3723
+ "LENGTH_RATIO_OUTLIER",
3724
+ "EQUALS_SOURCE",
3725
+ "GLOSSARY_TERM_MISSED",
3726
+ "INTEGRITY_REORDERED",
3727
+ "PROVIDER_DEGRADED"
3728
+ ]);
3729
+ var needsReviewEntrySchema = z.object({
3730
+ key: z.string(),
3731
+ reasons: z.array(reviewReasonCodeSchema)
3732
+ });
3733
+ var usageSummarySchema = z.object({
3734
+ inputTokens: z.number().int().nonnegative(),
3735
+ outputTokens: z.number().int().nonnegative()
3736
+ });
3737
+ var runBudgetSchema = z.object({
3738
+ maxTokens: z.number().int().nonnegative(),
3739
+ behavior: z.enum(["warn", "stop"]),
3740
+ supported: z.boolean(),
3741
+ tokensUsed: z.number().int().nonnegative(),
3742
+ exceeded: z.boolean()
3743
+ });
3744
+ var runStatusLocaleSchema = z.object({
3745
+ locale: z.string(),
3746
+ status: z.enum(["succeeded", "failed"]),
3747
+ needsReview: z.array(needsReviewEntrySchema),
3748
+ usage: usageSummarySchema.optional()
3749
+ });
3750
+ var runStatusFileSchema = z.object({
3751
+ version: z.number().int().positive(),
3752
+ generatedAt: z.string(),
3753
+ usage: usageSummarySchema.optional(),
3754
+ budget: runBudgetSchema.optional(),
3755
+ locales: z.array(runStatusLocaleSchema)
3756
+ });
3757
+ function runStatusFilePath(cwd) {
3758
+ return resolve(cwd, RUN_STATUS_DIR_NAME, RUN_STATUS_FILE_NAME);
3759
+ }
3760
+ function toRunStatusLocale(locale) {
3761
+ return {
3762
+ locale: locale.locale,
3763
+ status: locale.status,
3764
+ needsReview: locale.needsReview,
3765
+ ...locale.usage !== void 0 ? { usage: locale.usage } : {}
3766
+ };
3767
+ }
3768
+ function buildRunStatusFile(summary, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3769
+ return {
3770
+ version: CURRENT_VERSION2,
3771
+ generatedAt,
3772
+ ...summary.usage !== void 0 ? { usage: summary.usage } : {},
3773
+ ...summary.budget !== void 0 ? { budget: summary.budget } : {},
3774
+ locales: summary.locales.map(toRunStatusLocale)
3775
+ };
3776
+ }
3777
+ function fromParsed(data) {
3778
+ return {
3779
+ version: data.version,
3780
+ generatedAt: data.generatedAt,
3781
+ ...data.usage !== void 0 ? { usage: data.usage } : {},
3782
+ ...data.budget !== void 0 ? { budget: data.budget } : {},
3783
+ locales: data.locales.map((locale) => ({
3784
+ locale: locale.locale,
3785
+ status: locale.status,
3786
+ needsReview: locale.needsReview,
3787
+ ...locale.usage !== void 0 ? { usage: locale.usage } : {}
3788
+ }))
3789
+ };
3790
+ }
3791
+ async function readRunStatusFile(path, fs) {
3792
+ const read = await fs.readFileBounded(path, MAX_RUN_STATUS_FILE_BYTES);
3793
+ if (read.kind !== "ok") {
3794
+ return void 0;
3795
+ }
3796
+ let parsed;
3797
+ try {
3798
+ parsed = JSON.parse(read.content);
3799
+ } catch {
3800
+ return void 0;
3801
+ }
3802
+ const result = runStatusFileSchema.safeParse(parsed);
3803
+ if (!result.success || result.data.version !== CURRENT_VERSION2) {
3804
+ return void 0;
3805
+ }
3806
+ return fromParsed(result.data);
3807
+ }
3808
+ async function writeRunStatusFile(path, data, fs) {
3809
+ await mkdir(dirname(path), { recursive: true });
3810
+ await fs.writeFile(path, `${JSON.stringify(data, null, 2)}
3811
+ `);
3812
+ }
3813
+
3814
+ // src/flow/run-status.ts
3815
+ async function runStatus(input = {}, deps = {}) {
3816
+ const cwd = input.cwd ?? process.cwd();
3817
+ const fs = deps.fs ?? defaultFs;
3818
+ const file = await readRunStatusFile(runStatusFilePath(cwd), fs);
3819
+ if (file === void 0) {
3820
+ return { available: false };
3821
+ }
3822
+ return { available: true, ...file };
3823
+ }
3824
+
3257
3825
  // src/flow/budget.ts
3258
3826
  function createBudgetTracker(maxTokens, behavior) {
3259
3827
  return { maxTokens, behavior, tokensUsed: 0, usageSeen: false, exceeded: false, stopped: false };
@@ -3536,7 +4104,6 @@ function syntheticEntry(item) {
3536
4104
  ...item.sourceEntry,
3537
4105
  key: item.targetKey,
3538
4106
  isPlural: true,
3539
- // The CLDR category travels as data context (the meaning field), never the instruction channel.
3540
4107
  meaning: `CLDR plural category "${item.category}"`
3541
4108
  };
3542
4109
  }
@@ -3613,17 +4180,17 @@ async function runGenerationSubBatch(context, batch, accepted, withheld, provide
3613
4180
  return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
3614
4181
  }
3615
4182
  for (const item of batch) {
3616
- foldGenerationItem(item, result, accepted, withheld, providerFailures);
4183
+ foldGenerationItem(item, result, context.adapter, accepted, withheld, providerFailures);
3617
4184
  }
3618
4185
  return { notices: readNotices(result), usage: result.usage };
3619
4186
  }
3620
- function foldGenerationItem(item, result, accepted, withheld, providerFailures) {
4187
+ function foldGenerationItem(item, result, adapter, accepted, withheld, providerFailures) {
3621
4188
  const value = result.values.get(item.targetKey);
3622
4189
  if (value === void 0) {
3623
4190
  providerFailures.push(item.targetKey);
3624
4191
  return;
3625
4192
  }
3626
- if (result.integrity.get(item.targetKey)?.matches === true) {
4193
+ if (gateCandidateValue(item.sourceEntry, value, adapter).accepted) {
3627
4194
  accepted.push({
3628
4195
  targetKey: item.targetKey,
3629
4196
  entry: { ...syntheticEntry(item), value },
@@ -3635,16 +4202,6 @@ function foldGenerationItem(item, result, accepted, withheld, providerFailures)
3635
4202
  }
3636
4203
 
3637
4204
  // src/flow/locale-run.ts
3638
- function emptyResource(locale, format) {
3639
- return { locale, namespace: "", format, entries: /* @__PURE__ */ new Map() };
3640
- }
3641
- async function readTarget2(params) {
3642
- const path = localeFilePath(params.cwd, params.filesPattern, params.targetLocale);
3643
- if (!await params.fs.fileExists(path)) {
3644
- return emptyResource(params.targetLocale, params.format);
3645
- }
3646
- return (await params.adapter.read(path, params.targetLocale)).resource;
3647
- }
3648
4205
  function buildRequest3(params, entries) {
3649
4206
  return {
3650
4207
  sourceLocale: params.sourceLocale,
@@ -3657,7 +4214,14 @@ function buildRequest3(params, entries) {
3657
4214
  };
3658
4215
  }
3659
4216
  async function runLocale(params) {
3660
- const target = await readTarget2(params);
4217
+ const target = await readTargetResource({
4218
+ cwd: params.cwd,
4219
+ filesPattern: params.filesPattern,
4220
+ format: params.format,
4221
+ locale: params.targetLocale,
4222
+ adapter: params.adapter,
4223
+ fs: params.fs
4224
+ });
3661
4225
  const diff2 = diffResources(params.source, target, { baseline: params.baseline });
3662
4226
  const orphaned = params.generatePlurals ? diff2.orphaned.filter((key) => !isGeneratedPluralKey(key, sourcePluralBaseKeys(params.source))) : diff2.orphaned;
3663
4227
  const pruned = params.prune ? orphaned : [];
@@ -3711,8 +4275,11 @@ async function runLocale(params) {
3711
4275
  for (const key of pruned) {
3712
4276
  merged.delete(key);
3713
4277
  }
3714
- for (const [key, { value, source }] of accepted) {
3715
- merged.set(key, { ...source, value, namespace: target.namespace });
4278
+ for (const key of params.source.entries.keys()) {
4279
+ const hit = accepted.get(key);
4280
+ if (hit !== void 0) {
4281
+ merged.set(key, { ...hit.source, value: hit.value, namespace: target.namespace });
4282
+ }
3716
4283
  }
3717
4284
  const generation = await runGeneration(params, provider);
3718
4285
  for (const form of generation.accepted) {
@@ -3741,7 +4308,6 @@ async function runLocale(params) {
3741
4308
  ...invalidIcuSource,
3742
4309
  ...generation.withheld,
3743
4310
  ...generation.providerFailures,
3744
- // Source-present keys withheld by the budget guardrail must keep their prior lock hash too.
3745
4311
  ...budgetWithheld
3746
4312
  ]);
3747
4313
  const localeUsage = combineUsage(translation.usage, generation.usage);
@@ -3753,9 +4319,7 @@ async function runLocale(params) {
3753
4319
  invalidIcuSource,
3754
4320
  translated: [...accepted.keys()],
3755
4321
  generated: generation.accepted.map((form) => form.targetKey).sort(),
3756
- // Withheld generated forms surface alongside withheld translations: both failed integrity.
3757
4322
  integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
3758
- // A generation sub-batch whose provider call itself threw is a provider failure, never integrity.
3759
4323
  providerFailures: [...providerFailures, ...generation.providerFailures].sort(),
3760
4324
  budgetWithheld: [...budgetWithheld, ...generation.budgetWithheld].sort(),
3761
4325
  pruned,
@@ -3873,7 +4437,7 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
3873
4437
  return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
3874
4438
  }
3875
4439
  for (const entry of batch) {
3876
- foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures);
4440
+ foldEntryResult(entry, result, params.adapter, accepted, integrityMismatches, providerFailures);
3877
4441
  }
3878
4442
  if (result.reviewFlags !== void 0) {
3879
4443
  for (const [key, flag] of result.reviewFlags) {
@@ -3882,13 +4446,13 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
3882
4446
  }
3883
4447
  return { notices: readNotices(result), usage: result.usage };
3884
4448
  }
3885
- function foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures) {
4449
+ function foldEntryResult(entry, result, adapter, accepted, integrityMismatches, providerFailures) {
3886
4450
  const value = result.values.get(entry.key);
3887
4451
  if (value === void 0) {
3888
4452
  providerFailures.push(entry.key);
3889
4453
  return;
3890
4454
  }
3891
- if (result.integrity.get(entry.key)?.matches === true) {
4455
+ if (gateCandidateValue(entry, value, adapter).accepted) {
3892
4456
  accepted.set(entry.key, { value, source: entry });
3893
4457
  } else {
3894
4458
  integrityMismatches.push(entry.key);
@@ -3930,6 +4494,79 @@ function carryGeneratedLock(lockEntries, baseline, key, sourceBaseKeys) {
3930
4494
  }
3931
4495
 
3932
4496
  // src/flow/translate-project.ts
4497
+ async function recordRunStatus(cwd, dryRun, summary, fs) {
4498
+ if (dryRun) {
4499
+ return;
4500
+ }
4501
+ try {
4502
+ await writeRunStatusFile(runStatusFilePath(cwd), buildRunStatusFile(summary), fs);
4503
+ } catch {
4504
+ }
4505
+ }
4506
+ function buildLocaleRunParams(context, targetLocale, baseline) {
4507
+ return {
4508
+ source: context.source.resource,
4509
+ sourceInvalidIcuKeys: context.source.invalidIcuKeys,
4510
+ baseline,
4511
+ adapter: context.adapter,
4512
+ provider: context.provider,
4513
+ cwd: context.cwd,
4514
+ filesPattern: context.config.files.pattern,
4515
+ sourceLocale: context.config.sourceLocale,
4516
+ targetLocale,
4517
+ format: context.config.format,
4518
+ glossary: context.config.glossary,
4519
+ tone: context.config.tone,
4520
+ prune: context.prune,
4521
+ generatePlurals: context.generatePlurals,
4522
+ maxBatchSize: context.maxBatchSize,
4523
+ fs: context.fs,
4524
+ budget: context.budget
4525
+ };
4526
+ }
4527
+ async function runDryLocale(context, targetLocale, lock) {
4528
+ const params = buildLocaleRunParams(context, targetLocale, baselineFor(lock, targetLocale));
4529
+ return (await runLocale(params)).summary;
4530
+ }
4531
+ async function runLiveLocale(context, targetLocale) {
4532
+ return withLocaleWriteLock(context.cwd, targetLocale, context.fs, async () => {
4533
+ const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
4534
+ const params = buildLocaleRunParams(context, targetLocale, baselineFor(lock, targetLocale));
4535
+ const result = await runLocale(params);
4536
+ await updateLockFileLocale(context.cwd, context.fs, targetLocale, {
4537
+ mode: "replace",
4538
+ entries: result.lockEntries
4539
+ });
4540
+ return result.summary;
4541
+ });
4542
+ }
4543
+ async function runOneLocale(targetLocale, run) {
4544
+ try {
4545
+ return await run();
4546
+ } catch (error) {
4547
+ if (error instanceof SdkError && error.code === "LOCK_FILE_INVALID") {
4548
+ throw error;
4549
+ }
4550
+ return failureSummary(targetLocale, error);
4551
+ }
4552
+ }
4553
+ async function runAllLocalesDry(context, targetLocales) {
4554
+ const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
4555
+ const summaries = [];
4556
+ for (const targetLocale of targetLocales) {
4557
+ summaries.push(
4558
+ await runOneLocale(targetLocale, () => runDryLocale(context, targetLocale, lock))
4559
+ );
4560
+ }
4561
+ return summaries;
4562
+ }
4563
+ async function runAllLocalesLive(context, targetLocales) {
4564
+ const summaries = [];
4565
+ for (const targetLocale of targetLocales) {
4566
+ summaries.push(await runOneLocale(targetLocale, () => runLiveLocale(context, targetLocale)));
4567
+ }
4568
+ return summaries;
4569
+ }
3933
4570
  async function translate2(input, deps = {}) {
3934
4571
  const config = input.config;
3935
4572
  const cwd = input.cwd ?? process.cwd();
@@ -3945,47 +4582,26 @@ async function translate2(input, deps = {}) {
3945
4582
  const adapter = selectAdapter(config.format, deps.adapterRegistry);
3946
4583
  const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
3947
4584
  const source = await readSource(config, cwd, fs, adapter);
3948
- const lockPath = lockFilePath(cwd);
3949
- let lock = await readLockFile(lockPath, fs);
3950
- const summaries = [];
3951
- for (const targetLocale of config.targetLocales) {
3952
- try {
3953
- const params = {
3954
- source: source.resource,
3955
- sourceInvalidIcuKeys: source.invalidIcuKeys,
3956
- baseline: baselineFor(lock, targetLocale),
3957
- adapter,
3958
- provider,
3959
- cwd,
3960
- filesPattern: config.files.pattern,
3961
- sourceLocale: config.sourceLocale,
3962
- targetLocale,
3963
- format: config.format,
3964
- glossary: config.glossary,
3965
- tone: config.tone,
3966
- prune,
3967
- generatePlurals,
3968
- maxBatchSize,
3969
- fs,
3970
- budget
3971
- };
3972
- const { summary, lockEntries } = await runLocale(params);
3973
- if (!dryRun) {
3974
- lock = updateLockLocale(lock, targetLocale, lockEntries);
3975
- await writeLockFile(lockPath, lock, fs);
3976
- }
3977
- summaries.push(summary);
3978
- } catch (error) {
3979
- summaries.push(failureSummary(targetLocale, error));
3980
- }
3981
- }
4585
+ const context = {
4586
+ source,
4587
+ adapter,
4588
+ provider,
4589
+ cwd,
4590
+ config,
4591
+ prune,
4592
+ generatePlurals,
4593
+ maxBatchSize,
4594
+ fs,
4595
+ budget
4596
+ };
4597
+ const summaries = dryRun ? await runAllLocalesDry(context, config.targetLocales) : await runAllLocalesLive(context, config.targetLocales);
3982
4598
  const { succeeded, failed } = partition(summaries);
3983
4599
  const usage = summaries.reduce(
3984
- (total, summary) => combineUsage(total, summary.usage),
4600
+ (total, summary2) => combineUsage(total, summary2.usage),
3985
4601
  void 0
3986
4602
  );
3987
4603
  const budgetSummary = toBudgetSummary(budget);
3988
- return {
4604
+ const summary = {
3989
4605
  dryRun,
3990
4606
  locales: summaries,
3991
4607
  succeeded,
@@ -3993,6 +4609,8 @@ async function translate2(input, deps = {}) {
3993
4609
  ...usage !== void 0 ? { usage } : {},
3994
4610
  ...budgetSummary !== void 0 ? { budget: budgetSummary } : {}
3995
4611
  };
4612
+ await recordRunStatus(cwd, dryRun, summary, fs);
4613
+ return summary;
3996
4614
  }
3997
4615
  var ExchangeError = class extends Error {
3998
4616
  code;
@@ -4278,8 +4896,6 @@ var rowSchema = z.object({
4278
4896
  sourceHash: z.string(),
4279
4897
  translation: z.string(),
4280
4898
  context: z.string(),
4281
- // A legacy workbook exported before these columns existed has no such cell; cellString then yields
4282
- // "", and .catch maps it to "ok" / "" rather than rejecting the row.
4283
4899
  reviewStatus: z.enum(["ok", "review"]).catch("ok"),
4284
4900
  reviewReasons: z.string().catch("")
4285
4901
  });
@@ -4315,10 +4931,7 @@ function parseRow(sheet, row) {
4315
4931
  status: cellString(row.getCell(COLUMN.status)),
4316
4932
  sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
4317
4933
  translation: cellString(row.getCell(COLUMN.translation)),
4318
- // A workbook built before the Context column existed has no cell here; getCell auto-vivifies an
4319
- // empty one, so cellString yields "" and the row still validates, keeping import backward-compatible.
4320
4934
  context: cellString(row.getCell(COLUMN.context)),
4321
- // Same backward-compatible pattern for a workbook built before the Review columns existed.
4322
4935
  reviewStatus: cellString(row.getCell(COLUMN.reviewStatus)),
4323
4936
  reviewReasons: cellString(row.getCell(COLUMN.reviewReasons))
4324
4937
  };
@@ -4387,13 +5000,6 @@ async function readWorkbook(bytes, options = {}) {
4387
5000
 
4388
5001
  // src/flow/workbook/export-workbook.ts
4389
5002
  var DEFAULT_WORKBOOK_PATH = "verbatra-translations.xlsx";
4390
- async function readTarget3(cwd, config, adapter, fs, locale) {
4391
- const path = localeFilePath(cwd, config.files.pattern, locale);
4392
- if (!await fs.fileExists(path)) {
4393
- return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
4394
- }
4395
- return (await adapter.read(path, locale)).resource;
4396
- }
4397
5003
  function reasonLabel(reason) {
4398
5004
  return reason.toLowerCase().replace(/_/g, "-");
4399
5005
  }
@@ -4467,7 +5073,7 @@ async function exportWorkbook(input, deps = {}) {
4467
5073
  const locales = selectLocales(config, input.locales);
4468
5074
  const sheets = await Promise.all(
4469
5075
  locales.map(async (locale) => {
4470
- const target = await readTarget3(cwd, config, adapter, fs, locale);
5076
+ const target = await readTarget(cwd, config, adapter, fs, locale);
4471
5077
  const rows = buildRows(
4472
5078
  source.resource,
4473
5079
  target,
@@ -4505,14 +5111,8 @@ function judge(row, sourceEntry, adapter) {
4505
5111
  if (contentHash(sourceEntry) !== row.sourceHash) {
4506
5112
  return "drift";
4507
5113
  }
4508
- const integrity = adapter.comparePlaceholders !== void 0 ? adapter.comparePlaceholders(sourceEntry.value, row.translation) : checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(row.translation));
4509
- if (!integrity.matches) {
4510
- return "placeholder";
4511
- }
4512
- if (!adapter.validateMessage(row.translation)) {
4513
- return "icu";
4514
- }
4515
- return void 0;
5114
+ const gate = gateCandidateValue(sourceEntry, row.translation, adapter);
5115
+ return gate.accepted ? void 0 : gate.reason;
4516
5116
  }
4517
5117
  function trackBlankDrift(row, params, buckets) {
4518
5118
  const sourceEntry = params.source.entries.get(row.key);
@@ -4569,19 +5169,13 @@ function importLocale(params) {
4569
5169
  translated: [...buckets.accepted.keys()].sort(),
4570
5170
  unchanged: diff2.unchanged,
4571
5171
  orphaned: diff2.orphaned,
4572
- // Import never prunes: orphans are reported but never removed here (pruning is a translate-flow concern).
4573
5172
  pruned: [],
4574
5173
  invalidIcuSource,
4575
5174
  integrityMismatches: [...buckets.mismatches].sort(),
4576
- // A workbook import never calls a provider, so a provider-call failure cannot occur here.
4577
5175
  providerFailures: [],
4578
- // A workbook import never calls a provider, so the budget guardrail never withholds anything here.
4579
5176
  budgetWithheld: [],
4580
- // Plural generation is a translate-flow concern; the manual workbook import never generates forms.
4581
5177
  generated: [],
4582
5178
  notices: buckets.blankDrifted.size > 0 ? [blankRowBaselineNotice(buckets.blankDrifted.size)] : [],
4583
- // A workbook import never calls a provider and never recomputes review flags on its own path; the
4584
- // Excel workbook's Review status/reasons columns are informational only (see export-workbook.ts).
4585
5179
  needsReview: []
4586
5180
  };
4587
5181
  return { summary, accepted: buckets.accepted, withheld: buckets.withheld };
@@ -4602,13 +5196,6 @@ async function readWorkbookBytes(path, fs) {
4602
5196
  }
4603
5197
  return read.bytes;
4604
5198
  }
4605
- async function readTarget4(cwd, config, adapter, fs, locale) {
4606
- const path = localeFilePath(cwd, config.files.pattern, locale);
4607
- if (!await fs.fileExists(path)) {
4608
- return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
4609
- }
4610
- return (await adapter.read(path, locale)).resource;
4611
- }
4612
5199
  function mergeAccepted(target, accepted) {
4613
5200
  const merged = new Map(target.entries);
4614
5201
  for (const [key, { value, source }] of accepted) {
@@ -4639,7 +5226,7 @@ async function runSheet(ctx, sheet, lock) {
4639
5226
  `The workbook has a sheet for locale "${sheet.locale}", which is not a configured target locale.`
4640
5227
  );
4641
5228
  }
4642
- const target = await readTarget4(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
5229
+ const target = await readTarget(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
4643
5230
  const baseline = baselineFor(lock, sheet.locale);
4644
5231
  const { summary, accepted } = importLocale({
4645
5232
  sheet,
@@ -4682,8 +5269,7 @@ async function importWorkbook(input, deps = {}) {
4682
5269
  } catch (error) {
4683
5270
  throw new SdkError("SOURCE_INVALID", error.message);
4684
5271
  }
4685
- const lockPath = lockFilePath(cwd);
4686
- let lock = await readLockFile(lockPath, fs);
5272
+ const lock = await readLockFile(lockFilePath(cwd), fs);
4687
5273
  const ctx = {
4688
5274
  config,
4689
5275
  cwd,
@@ -4696,10 +5282,18 @@ async function importWorkbook(input, deps = {}) {
4696
5282
  const summaries = [];
4697
5283
  for (const sheet of data.sheets) {
4698
5284
  try {
4699
- const { summary, lockEntries } = await runSheet(ctx, sheet, lock);
4700
- if (!dryRun) {
4701
- lock = updateLockLocale(lock, sheet.locale, lockEntries);
4702
- await writeLockFile(lockPath, lock, fs);
5285
+ let summary;
5286
+ if (dryRun) {
5287
+ summary = (await runSheet(ctx, sheet, lock)).summary;
5288
+ } else {
5289
+ summary = await withLocaleWriteLock(cwd, sheet.locale, fs, async () => {
5290
+ const result = await runSheet(ctx, sheet, lock);
5291
+ await updateLockFileLocale(cwd, fs, sheet.locale, {
5292
+ mode: "replace",
5293
+ entries: result.lockEntries
5294
+ });
5295
+ return result.summary;
5296
+ });
4703
5297
  }
4704
5298
  summaries.push(summary);
4705
5299
  } catch (error) {
@@ -4832,6 +5426,6 @@ async function watch(input, deps = {}) {
4832
5426
  return { stop };
4833
5427
  }
4834
5428
 
4835
- export { DEFAULT_WORKBOOK_PATH, LOCK_FILE_NAME, SdkError, check, defineConfig, diff, exportWorkbook, importWorkbook, loadConfig, loadConfigWithMeta, loadLockFile, lockState, scaffoldingMetadata, translate2 as translate, verbatraConfigSchema, watch };
5429
+ export { DEFAULT_WORKBOOK_PATH, LOCK_FILE_NAME, SdkError, check, defineConfig, diff, diffLocaleSnapshots, editEntry, exportWorkbook, importWorkbook, keyIntegrity, keyValue, loadConfig, loadConfigWithMeta, loadLockFile, lockState, readLocaleFileSnapshot, retranslateEntry, runStatus, scaffoldingMetadata, translate2 as translate, verbatraConfigSchema, watch };
4836
5430
  //# sourceMappingURL=index.js.map
4837
5431
  //# sourceMappingURL=index.js.map