@verbatra/sdk 0.5.0-next.4 → 0.6.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.cjs CHANGED
@@ -145,6 +145,24 @@ async function atomicWrite(path, data) {
145
145
  throw error;
146
146
  }
147
147
  }
148
+ async function createExclusive(path$1, data) {
149
+ await promises.mkdir(path.dirname(path$1), { recursive: true });
150
+ let handle;
151
+ try {
152
+ handle = await promises.open(path$1, "wx");
153
+ } catch (error) {
154
+ if (error.code === "EEXIST") {
155
+ return false;
156
+ }
157
+ throw error;
158
+ }
159
+ try {
160
+ await handle.writeFile(data, "utf8");
161
+ } finally {
162
+ await handle.close();
163
+ }
164
+ return true;
165
+ }
148
166
  var defaultFs = {
149
167
  async fileExists(path) {
150
168
  try {
@@ -157,7 +175,11 @@ var defaultFs = {
157
175
  readFileBounded: (path, maxBytes) => readBounded(path, maxBytes),
158
176
  readBytesBounded: (path, maxBytes) => readBoundedBytes(path, maxBytes),
159
177
  writeFile: (path, data) => atomicWrite(path, data),
160
- writeBytes: (path, data) => atomicWrite(path, data)
178
+ writeBytes: (path, data) => atomicWrite(path, data),
179
+ createExclusive: (path, data) => createExclusive(path, data),
180
+ deleteFile: async (path) => {
181
+ await promises.rm(path, { force: true });
182
+ }
161
183
  };
162
184
  var MAX_GLOSSARY_FILE_BYTES = 1024 * 1024;
163
185
  var glossaryRecordSchema = zod.z.record(zod.z.string(), zod.z.string());
@@ -346,29 +368,18 @@ var TIMEOUT_STATUS = 408;
346
368
  var AUTH_FAILED_STATUSES = /* @__PURE__ */ new Set([401, 403]);
347
369
  var RATE_LIMITED_CLASS_NAMES = /* @__PURE__ */ new Set([
348
370
  "RateLimitError",
349
- // openai, @anthropic-ai/sdk
350
371
  "TooManyRequestsError"
351
- // deepl-node
352
372
  ]);
353
373
  var AUTH_FAILED_CLASS_NAMES = /* @__PURE__ */ new Set([
354
374
  "AuthenticationError",
355
- // openai, @anthropic-ai/sdk (401)
356
375
  "PermissionDeniedError",
357
- // openai, @anthropic-ai/sdk (403)
358
376
  "AuthorizationError"
359
- // deepl-node
360
377
  ]);
361
378
  var TIMEOUT_CLASS_NAMES = /* @__PURE__ */ new Set([
362
379
  "APIConnectionTimeoutError",
363
- // openai, @anthropic-ai/sdk
364
- // deepl-node has no distinct timeout class; ConnectionError is its only network-failure
365
- // class, so it is folded into TIMEOUT rather than the PROVIDER_ERROR fallback.
366
380
  "ConnectionError"
367
381
  ]);
368
- var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set([
369
- "APIUserAbortError"
370
- // openai, @anthropic-ai/sdk
371
- ]);
382
+ var ABORT_ERROR_CLASS_NAMES = /* @__PURE__ */ new Set(["APIUserAbortError"]);
372
383
  function getErrorStatus(error) {
373
384
  if (typeof error !== "object" || error === null || !("status" in error)) {
374
385
  return void 0;
@@ -422,7 +433,6 @@ function isAbortError(error, signal) {
422
433
  }
423
434
  var REDACTED = "[REDACTED]";
424
435
  var KEY_PATTERNS = [
425
- // The `\b` anchors `sk-` to a word start so hyphenated words like "risk-" or "task-" pass through.
426
436
  /\bsk-[A-Za-z0-9_-]{8,}/g,
427
437
  /AIza[0-9A-Za-z_-]{35}/g,
428
438
  /[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
@@ -444,6 +454,10 @@ var ProviderError = class extends Error {
444
454
  /** The stable {@link ProviderErrorCode} for this failure; branch on this, not the message. */
445
455
  code;
446
456
  /**
457
+ * Redacts the message as a defense-in-depth backstop before storing it. The redaction runs with an
458
+ * empty secret argument so the ANTHROPIC_API_KEY environment default is not re-applied, keeping
459
+ * this generic error decoupled from any one provider's environment variable.
460
+ *
447
461
  * @param code - The stable failure code.
448
462
  * @param message - A fixed, safe message; callers must never pass key, SDK, or request-derived text.
449
463
  */
@@ -1011,9 +1025,6 @@ function createDeepLProvider(config, deps = {}) {
1011
1025
  return {
1012
1026
  id: PROVIDER_ID2,
1013
1027
  kind: "machine-translation",
1014
- // Honest capability signal: DeepL only ever applies a pre-created native glossary id, never a
1015
- // generic term map (see buildTranslateOptions). Without one configured, a supplied term map is
1016
- // silently ignored and surfaces only as a GLOSSARY_IGNORED notice.
1017
1028
  supportsGlossary: validConfig.glossaryId !== void 0,
1018
1029
  translateBatch: (request) => translate(bundle, validConfig, request)
1019
1030
  };
@@ -1125,14 +1136,14 @@ function throwAbort(signal) {
1125
1136
  throw new DOMException("This operation was aborted.", "AbortError");
1126
1137
  }
1127
1138
  function delay(ms, signal) {
1128
- return new Promise((resolve7) => {
1139
+ return new Promise((resolve9) => {
1129
1140
  const onAbort = () => {
1130
1141
  clearTimeout(timer);
1131
- resolve7();
1142
+ resolve9();
1132
1143
  };
1133
1144
  const timer = setTimeout(() => {
1134
1145
  signal?.removeEventListener("abort", onAbort);
1135
- resolve7();
1146
+ resolve9();
1136
1147
  }, ms);
1137
1148
  signal?.addEventListener("abort", onAbort, { once: true });
1138
1149
  });
@@ -1180,12 +1191,10 @@ var TYPE_MAP = {
1180
1191
  object: "OBJECT"
1181
1192
  };
1182
1193
  var HANDLED_KEYWORDS = /* @__PURE__ */ new Set([
1183
- // transformed or recursed
1184
1194
  "type",
1185
1195
  "required",
1186
1196
  "properties",
1187
1197
  "items",
1188
- // deliberately dropped (not part of Google's Schema dialect)
1189
1198
  "$schema",
1190
1199
  "additionalProperties"
1191
1200
  ]);
@@ -1753,6 +1762,60 @@ async function loadConfig(options = {}) {
1753
1762
  const { config } = await loadConfigWithMeta(options);
1754
1763
  return config;
1755
1764
  }
1765
+ var LOCAL_DIR_NAME = ".verbatra-local";
1766
+ var LOCK_FILE_GUARD_STEM = "_lockfile";
1767
+ var DEFAULT_POLL_INTERVAL_MS = 100;
1768
+ var DEFAULT_ACQUIRE_TIMEOUT_MS = 10 * 6e4;
1769
+ function lockPath(cwd, stem) {
1770
+ return path.resolve(cwd, LOCAL_DIR_NAME, "locks", `${stem}.lock`);
1771
+ }
1772
+ function localeLockPath(cwd, locale) {
1773
+ return lockPath(cwd, locale);
1774
+ }
1775
+ function lockFileGuardPath(cwd) {
1776
+ return lockPath(cwd, LOCK_FILE_GUARD_STEM);
1777
+ }
1778
+ function sleep(ms) {
1779
+ return new Promise((res) => {
1780
+ setTimeout(res, ms);
1781
+ });
1782
+ }
1783
+ function lockPayload() {
1784
+ return JSON.stringify({ pid: process.pid, acquiredAt: (/* @__PURE__ */ new Date()).toISOString() });
1785
+ }
1786
+ async function acquireLock(path, fs, pollIntervalMs, deadline) {
1787
+ for (; ; ) {
1788
+ if (await fs.createExclusive(path, lockPayload())) {
1789
+ return;
1790
+ }
1791
+ if (Date.now() >= deadline) {
1792
+ throw new SdkError(
1793
+ "LOCK_CONTENDED",
1794
+ `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.`
1795
+ );
1796
+ }
1797
+ const jitter = Math.random() * pollIntervalMs;
1798
+ await sleep(pollIntervalMs + jitter);
1799
+ }
1800
+ }
1801
+ async function withFileLock(path, fs, fn, options) {
1802
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
1803
+ const acquireTimeoutMs = options.acquireTimeoutMs ?? DEFAULT_ACQUIRE_TIMEOUT_MS;
1804
+ await acquireLock(path, fs, pollIntervalMs, Date.now() + acquireTimeoutMs);
1805
+ try {
1806
+ return await fn();
1807
+ } finally {
1808
+ await fs.deleteFile(path);
1809
+ }
1810
+ }
1811
+ async function withLocaleWriteLock(cwd, locale, fs, fn, options = {}) {
1812
+ return withFileLock(localeLockPath(cwd, locale), fs, fn, options);
1813
+ }
1814
+ async function withLockFileGuard(cwd, fs, fn, options = {}) {
1815
+ return withFileLock(lockFileGuardPath(cwd), fs, fn, options);
1816
+ }
1817
+
1818
+ // src/lock/lock-file.ts
1756
1819
  var LOCK_FILE_NAME = "verbatra.lock.json";
1757
1820
  var CURRENT_VERSION = 1;
1758
1821
  var EMPTY_LOCK = { version: CURRENT_VERSION, locales: {} };
@@ -1764,8 +1827,7 @@ var lockFileSchema = zod.z.object({
1764
1827
  function lockFilePath(cwd) {
1765
1828
  return path.resolve(cwd, LOCK_FILE_NAME);
1766
1829
  }
1767
- async function readLockFile(path, fs) {
1768
- const read = await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES);
1830
+ function parseLockFileRead(read, path) {
1769
1831
  if (read.kind === "missing") {
1770
1832
  return EMPTY_LOCK;
1771
1833
  }
@@ -1793,6 +1855,9 @@ async function readLockFile(path, fs) {
1793
1855
  }
1794
1856
  return result.data;
1795
1857
  }
1858
+ async function readLockFile(path, fs) {
1859
+ return parseLockFileRead(await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES), path);
1860
+ }
1796
1861
  function baselineFor(lock, locale) {
1797
1862
  return new Map(Object.entries(lock.locales[locale] ?? {}));
1798
1863
  }
@@ -1808,14 +1873,30 @@ function byKey(a, b) {
1808
1873
  function sortRecord(record) {
1809
1874
  return Object.fromEntries(Object.entries(record).sort(byKey));
1810
1875
  }
1811
- async function writeLockFile(path, lock, fs) {
1876
+ function serializeLockFile(lock) {
1812
1877
  const locales = {};
1813
1878
  for (const [locale, entries] of Object.entries(lock.locales).sort(byKey)) {
1814
1879
  locales[locale] = sortRecord(entries);
1815
1880
  }
1816
1881
  const ordered = { version: lock.version, locales };
1817
- await fs.writeFile(path, `${JSON.stringify(ordered, null, 2)}
1818
- `);
1882
+ return `${JSON.stringify(ordered, null, 2)}
1883
+ `;
1884
+ }
1885
+ function applyLockLocalePatch(currentEntries, patch) {
1886
+ if (patch.mode === "replace") {
1887
+ return patch.entries;
1888
+ }
1889
+ return { ...currentEntries, ...patch.entries };
1890
+ }
1891
+ async function updateLockFileLocale(cwd, fs, locale, patch) {
1892
+ return withLockFileGuard(cwd, fs, async () => {
1893
+ const path = lockFilePath(cwd);
1894
+ const lock = parseLockFileRead(await fs.readFileBounded(path, MAX_LOCK_FILE_BYTES), path);
1895
+ const nextEntries = applyLockLocalePatch(lock.locales[locale], patch);
1896
+ const next = updateLockLocale(lock, locale, nextEntries);
1897
+ await fs.writeFile(path, serializeLockFile(next));
1898
+ return next;
1899
+ });
1819
1900
  }
1820
1901
  var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
1821
1902
  var INVALID = { placeholders: [], isPlural: false, valid: false };
@@ -2077,10 +2158,68 @@ var AdapterError = class extends Error {
2077
2158
  };
2078
2159
  var MAX_DEPTH = 100;
2079
2160
  var MAX_INPUT_BYTES = 16 * 1024 * 1024;
2080
- var jsonTreeSchema = zod.z.lazy(
2081
- () => zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null(), zod.z.record(zod.z.string(), jsonTreeSchema)])
2082
- );
2083
- var rootSchema = zod.z.record(zod.z.string(), jsonTreeSchema);
2161
+ var SENTINEL_ESCAPE = "\\u0001";
2162
+ function isJsonWhitespace(char) {
2163
+ return char === " " || char === " " || char === "\n" || char === "\r";
2164
+ }
2165
+ function endOfStringToken(content, openQuote) {
2166
+ let index = openQuote + 1;
2167
+ while (index < content.length) {
2168
+ const char = content[index];
2169
+ if (char === "\\") {
2170
+ index += 2;
2171
+ } else if (char === '"') {
2172
+ return index + 1;
2173
+ } else {
2174
+ index += 1;
2175
+ }
2176
+ }
2177
+ return index;
2178
+ }
2179
+ function isKeyToken(content, end) {
2180
+ let index = end;
2181
+ while (isJsonWhitespace(content[index])) {
2182
+ index += 1;
2183
+ }
2184
+ return content[index] === ":";
2185
+ }
2186
+ function prefixKeyToken(token) {
2187
+ if (token.includes(SENTINEL_ESCAPE)) {
2188
+ throw new AdapterError(
2189
+ "INVALID_STRUCTURE",
2190
+ "A key contains a reserved control-character escape."
2191
+ );
2192
+ }
2193
+ return `"${SENTINEL_ESCAPE}${token.slice(1)}`;
2194
+ }
2195
+ function prefixKeyTokens(content) {
2196
+ let out = "";
2197
+ let index = 0;
2198
+ while (index < content.length) {
2199
+ const openQuote = content.indexOf('"', index);
2200
+ if (openQuote === -1) {
2201
+ return out + content.slice(index);
2202
+ }
2203
+ const end = endOfStringToken(content, openQuote);
2204
+ const token = content.slice(openQuote, end);
2205
+ out += content.slice(index, openQuote);
2206
+ out += isKeyToken(content, end) ? prefixKeyToken(token) : token;
2207
+ index = end;
2208
+ }
2209
+ return out;
2210
+ }
2211
+ function childrenOf(node) {
2212
+ if (node instanceof Map) {
2213
+ return node.values();
2214
+ }
2215
+ if (Array.isArray(node)) {
2216
+ return node;
2217
+ }
2218
+ if (typeof node === "object" && node !== null) {
2219
+ return Object.values(node);
2220
+ }
2221
+ return null;
2222
+ }
2084
2223
  function assertWithinDepth(value, max) {
2085
2224
  const stack = [{ node: value, depth: 1 }];
2086
2225
  while (stack.length > 0) {
@@ -2088,44 +2227,117 @@ function assertWithinDepth(value, max) {
2088
2227
  if (top === void 0) {
2089
2228
  break;
2090
2229
  }
2091
- const { node, depth } = top;
2092
- if (typeof node !== "object" || node === null) {
2230
+ const children = childrenOf(top.node);
2231
+ if (children === null) {
2093
2232
  continue;
2094
2233
  }
2095
- if (depth > max) {
2234
+ if (top.depth > max) {
2096
2235
  throw new AdapterError("MAX_DEPTH_EXCEEDED", "The file nests objects too deeply.");
2097
2236
  }
2098
- for (const child of Object.values(node)) {
2099
- stack.push({ node: child, depth: depth + 1 });
2237
+ for (const child of children) {
2238
+ stack.push({ node: child, depth: top.depth + 1 });
2100
2239
  }
2101
2240
  }
2102
2241
  }
2103
- function assertJsonRecord(value) {
2104
- assertWithinDepth(value, MAX_DEPTH);
2105
- const result = rootSchema.safeParse(value);
2106
- if (!result.success) {
2107
- throw new AdapterError(
2108
- "INVALID_STRUCTURE",
2109
- "The file is not a valid object (expected nested objects of string, number, boolean, or null leaves)."
2110
- );
2242
+ function toOrdered(node) {
2243
+ if (Array.isArray(node)) {
2244
+ return node.map((child) => toOrdered(child));
2111
2245
  }
2112
- return result.data;
2246
+ if (typeof node === "object" && node !== null) {
2247
+ const out = /* @__PURE__ */ new Map();
2248
+ for (const [key, child] of Object.entries(node)) {
2249
+ out.set(key.slice(1), toOrdered(child));
2250
+ }
2251
+ return out;
2252
+ }
2253
+ return node;
2113
2254
  }
2114
- function parseJsonObject(content) {
2255
+ function parseOrderedJson(content) {
2256
+ const prefixed = prefixKeyTokens(content);
2115
2257
  let parsed;
2116
2258
  try {
2117
- parsed = JSON.parse(content);
2259
+ parsed = JSON.parse(prefixed);
2118
2260
  } catch {
2119
2261
  throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
2120
2262
  }
2121
- return assertJsonRecord(parsed);
2263
+ assertWithinDepth(parsed, MAX_DEPTH);
2264
+ return toOrdered(parsed);
2122
2265
  }
2123
- function serializeJsonTree(tree) {
2124
- return `${JSON.stringify(tree, null, 2)}
2266
+ function printRecord(record, indent) {
2267
+ if (record.size === 0) {
2268
+ return "{}";
2269
+ }
2270
+ const childIndent = `${indent} `;
2271
+ const lines = [];
2272
+ for (const [key, child] of record) {
2273
+ lines.push(`${childIndent}${JSON.stringify(key)}: ${printValue(child, childIndent)}`);
2274
+ }
2275
+ return `{
2276
+ ${lines.join(",\n")}
2277
+ ${indent}}`;
2278
+ }
2279
+ function printArray(items, indent) {
2280
+ if (items.length === 0) {
2281
+ return "[]";
2282
+ }
2283
+ const childIndent = `${indent} `;
2284
+ const lines = items.map((item) => `${childIndent}${printValue(item, childIndent)}`);
2285
+ return `[
2286
+ ${lines.join(",\n")}
2287
+ ${indent}]`;
2288
+ }
2289
+ function printValue(value, indent) {
2290
+ if (value instanceof Map) {
2291
+ return printRecord(value, indent);
2292
+ }
2293
+ if (Array.isArray(value)) {
2294
+ return printArray(value, indent);
2295
+ }
2296
+ return JSON.stringify(value);
2297
+ }
2298
+ function serializeOrderedJson(value) {
2299
+ return `${printValue(value, "")}
2125
2300
  `;
2126
2301
  }
2302
+ var INVALID_STRUCTURE_MESSAGE = "The file is not a valid object (expected nested objects of string, number, boolean, or null leaves).";
2303
+ function isJsonLeaf(value) {
2304
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null;
2305
+ }
2306
+ function assertNodesValid(root) {
2307
+ const stack = [root];
2308
+ while (stack.length > 0) {
2309
+ const node = stack.pop();
2310
+ if (node === void 0) {
2311
+ break;
2312
+ }
2313
+ for (const [key, child] of node) {
2314
+ if (typeof key !== "string") {
2315
+ throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
2316
+ }
2317
+ if (child instanceof Map) {
2318
+ stack.push(child);
2319
+ } else if (!isJsonLeaf(child)) {
2320
+ throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
2321
+ }
2322
+ }
2323
+ }
2324
+ }
2325
+ function assertJsonRecord(value) {
2326
+ assertWithinDepth(value, MAX_DEPTH);
2327
+ if (!(value instanceof Map)) {
2328
+ throw new AdapterError("INVALID_STRUCTURE", INVALID_STRUCTURE_MESSAGE);
2329
+ }
2330
+ assertNodesValid(value);
2331
+ return value;
2332
+ }
2333
+ function parseJsonObject(content) {
2334
+ return assertJsonRecord(parseOrderedJson(content));
2335
+ }
2336
+ function serializeJsonTree(tree) {
2337
+ return serializeOrderedJson(tree);
2338
+ }
2127
2339
  function isJsonNode(value) {
2128
- return typeof value === "object" && value !== null;
2340
+ return value instanceof Map;
2129
2341
  }
2130
2342
  function namespaceOf(filePath) {
2131
2343
  return path.basename(filePath, path.extname(filePath));
@@ -2327,7 +2539,7 @@ function addLeaf(ctx, segments, key, value) {
2327
2539
  ctx.out.set(mapKey, { key: mapKey, namespace: ctx.namespace, value, placeholders, isPlural });
2328
2540
  }
2329
2541
  function addEntries(ctx, prefix, node) {
2330
- for (const [key, value] of Object.entries(node)) {
2542
+ for (const [key, value] of node) {
2331
2543
  const segments = [...prefix, key];
2332
2544
  if (isJsonNode(value)) {
2333
2545
  addEntries(ctx, segments, value);
@@ -2337,7 +2549,7 @@ function addEntries(ctx, prefix, node) {
2337
2549
  }
2338
2550
  }
2339
2551
  function addPathEntries(node, prefix, namespace, derive, out, claimedPaths, excluded) {
2340
- for (const [key, value] of Object.entries(node)) {
2552
+ for (const [key, value] of node) {
2341
2553
  const path = prefix === "" ? key : `${prefix}.${key}`;
2342
2554
  if (claimedPaths.has(path)) {
2343
2555
  throw new AdapterError(
@@ -2367,17 +2579,14 @@ function flattenTree(tree, namespace, derive, keyMode = "literal-leaf") {
2367
2579
  addEntries(ctx, [], tree);
2368
2580
  return { entries: out, excludedLeafPaths: ctx.excluded };
2369
2581
  }
2370
- function emptyNode() {
2371
- return /* @__PURE__ */ Object.create(null);
2372
- }
2373
2582
  function descend(node, segment) {
2374
- const next = node[segment];
2583
+ const next = node.get(segment);
2375
2584
  if (next === void 0) {
2376
- const created = emptyNode();
2377
- node[segment] = created;
2585
+ const created = /* @__PURE__ */ new Map();
2586
+ node.set(segment, created);
2378
2587
  return created;
2379
2588
  }
2380
- if (typeof next === "object") {
2589
+ if (next instanceof Map) {
2381
2590
  return next;
2382
2591
  }
2383
2592
  throw new AdapterError("INVALID_STRUCTURE", "A leaf key collides with a nested key path.");
@@ -2391,13 +2600,13 @@ function setPath(root, segments, value) {
2391
2600
  for (const segment of segments.slice(0, -1)) {
2392
2601
  node = descend(node, segment);
2393
2602
  }
2394
- if (typeof node[leaf] === "object") {
2603
+ if (node.get(leaf) instanceof Map) {
2395
2604
  throw new AdapterError("INVALID_STRUCTURE", "A leaf key collides with a nested key path.");
2396
2605
  }
2397
- node[leaf] = value;
2606
+ node.set(leaf, value);
2398
2607
  }
2399
2608
  function unflattenEntries(entries) {
2400
- const root = emptyNode();
2609
+ const root = /* @__PURE__ */ new Map();
2401
2610
  for (const [key, entry] of entries) {
2402
2611
  setPath(root, decodeKeyToSegments(key), entry.value);
2403
2612
  }
@@ -2474,13 +2683,8 @@ function isMetadataKey(key) {
2474
2683
  return key.startsWith("@");
2475
2684
  }
2476
2685
  function parseArbObject(content) {
2477
- let parsed;
2478
- try {
2479
- parsed = JSON.parse(content);
2480
- } catch {
2481
- throw new AdapterError("INVALID_JSON", "The file is not valid JSON.");
2482
- }
2483
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2686
+ const parsed = parseOrderedJson(content);
2687
+ if (!(parsed instanceof Map)) {
2484
2688
  throw new AdapterError(
2485
2689
  "INVALID_STRUCTURE",
2486
2690
  "The file is not a valid object (expected nested objects of string values)."
@@ -2489,10 +2693,10 @@ function parseArbObject(content) {
2489
2693
  return parsed;
2490
2694
  }
2491
2695
  function stripArbMetadata(tree) {
2492
- const out = /* @__PURE__ */ Object.create(null);
2493
- for (const [key, value] of Object.entries(tree)) {
2696
+ const out = /* @__PURE__ */ new Map();
2697
+ for (const [key, value] of tree) {
2494
2698
  if (!isMetadataKey(key)) {
2495
- out[key] = value;
2699
+ out.set(key, value);
2496
2700
  }
2497
2701
  }
2498
2702
  return out;
@@ -2504,16 +2708,16 @@ function messageKeyForMetadata(key) {
2504
2708
  return isMetadataKey(key) && !key.startsWith("@@") ? key.slice(1) : null;
2505
2709
  }
2506
2710
  function descriptionOf(value) {
2507
- if (typeof value !== "object" || value === null) {
2711
+ if (!(value instanceof Map)) {
2508
2712
  return void 0;
2509
2713
  }
2510
- const description = value.description;
2714
+ const description = value.get("description");
2511
2715
  return typeof description === "string" ? description : void 0;
2512
2716
  }
2513
2717
  function extractArbDescriptions(content) {
2514
2718
  const tree = parseArbObject(content);
2515
2719
  const out = /* @__PURE__ */ new Map();
2516
- for (const [key, value] of Object.entries(tree)) {
2720
+ for (const [key, value] of tree) {
2517
2721
  const messageKey = messageKeyForMetadata(key);
2518
2722
  if (messageKey === null) {
2519
2723
  continue;
@@ -2545,25 +2749,25 @@ async function readDestinationPairs(filePath) {
2545
2749
  }
2546
2750
  throw error;
2547
2751
  }
2548
- return Object.entries(parseArbObject(content));
2752
+ return [...parseArbObject(content)];
2549
2753
  }
2550
2754
  async function buildArbWriteTree(entries, filePath) {
2551
2755
  const messages = messagesFromEntries(entries);
2552
2756
  const pairs = await readDestinationPairs(filePath);
2553
- const out = /* @__PURE__ */ Object.create(null);
2757
+ const out = /* @__PURE__ */ new Map();
2554
2758
  const consumed = /* @__PURE__ */ new Set();
2555
2759
  for (const [key, value] of pairs ?? []) {
2556
2760
  const translated = isMetadataKey(key) ? void 0 : messages.get(key);
2557
2761
  if (translated !== void 0) {
2558
2762
  consumed.add(key);
2559
- out[key] = translated;
2763
+ out.set(key, translated);
2560
2764
  } else if (isMetadataKey(key) || typeof value === "string") {
2561
- out[key] = value;
2765
+ out.set(key, value);
2562
2766
  }
2563
2767
  }
2564
2768
  for (const [key, value] of messages) {
2565
2769
  if (!consumed.has(key)) {
2566
- out[key] = value;
2770
+ out.set(key, value);
2567
2771
  }
2568
2772
  }
2569
2773
  return out;
@@ -2652,7 +2856,7 @@ function createNextIntlJsonAdapter() {
2652
2856
  });
2653
2857
  }
2654
2858
  function assertNoDottedNestedKey(tree) {
2655
- for (const [key, value] of Object.entries(tree)) {
2859
+ for (const [key, value] of tree) {
2656
2860
  if (!isJsonNode(value)) {
2657
2861
  continue;
2658
2862
  }
@@ -2668,7 +2872,7 @@ function assertNoDottedNestedKey(tree) {
2668
2872
  function assertNotMixed(tree) {
2669
2873
  let hasNested = false;
2670
2874
  let hasFlatDottedKey = false;
2671
- for (const [key, value] of Object.entries(tree)) {
2875
+ for (const [key, value] of tree) {
2672
2876
  if (isJsonNode(value)) {
2673
2877
  hasNested = true;
2674
2878
  } else if (key.includes(".")) {
@@ -2705,9 +2909,9 @@ async function detectStyle(filePath) {
2705
2909
  return "flat";
2706
2910
  }
2707
2911
  function buildFlatTree(entries) {
2708
- const out = /* @__PURE__ */ Object.create(null);
2912
+ const out = /* @__PURE__ */ new Map();
2709
2913
  for (const [key, entry] of entries) {
2710
- out[key] = entry.value;
2914
+ out.set(key, entry.value);
2711
2915
  }
2712
2916
  return out;
2713
2917
  }
@@ -2725,7 +2929,6 @@ function createNgxTranslateJsonAdapter() {
2725
2929
  }),
2726
2930
  validateTree: assertNotMixed,
2727
2931
  buildWriteTree: buildNgxWriteTree,
2728
- // ngx-translate flat style uses dotted keys as path notation, not literal leaves.
2729
2932
  keyMode: "path-notation"
2730
2933
  });
2731
2934
  }
@@ -2751,6 +2954,11 @@ var AdapterRegistry = class {
2751
2954
  }
2752
2955
  return { status: "resolved", adapter };
2753
2956
  }
2957
+ /**
2958
+ * Resolve by asking every registered adapter's `canHandle`. Several adapters can claim one file
2959
+ * (all JSON adapters claim `.json`), so more than one match is reported as `ambiguous` rather
2960
+ * than guessed at.
2961
+ */
2754
2962
  resolveByDetection(filePath, sample) {
2755
2963
  const matches = this.adapters.filter((adapter) => adapter.canHandle(filePath, sample));
2756
2964
  const first = matches[0];
@@ -3105,14 +3313,34 @@ function createXliffAdapter() {
3105
3313
  extractPlaceholders: extractXliffPlaceholders
3106
3314
  });
3107
3315
  }
3316
+ function normalizeKey(key) {
3317
+ if (typeof key === "object" && key !== null) {
3318
+ throw new AdapterError(
3319
+ "INVALID_STRUCTURE",
3320
+ "A mapping key is a map or sequence (expected scalar keys)."
3321
+ );
3322
+ }
3323
+ return String(key);
3324
+ }
3325
+ function normalizeYamlTree(value) {
3326
+ if (!(value instanceof Map)) {
3327
+ return value;
3328
+ }
3329
+ const out = /* @__PURE__ */ new Map();
3330
+ for (const [key, child] of value) {
3331
+ out.set(normalizeKey(key), normalizeYamlTree(child));
3332
+ }
3333
+ return out;
3334
+ }
3108
3335
  function parseYamlObject(content) {
3109
3336
  let parsed;
3110
3337
  try {
3111
- parsed = yaml.parse(content, { maxAliasCount: 100 });
3338
+ parsed = yaml.parse(content, { mapAsMap: true, maxAliasCount: 100 });
3112
3339
  } catch {
3113
3340
  throw new AdapterError("INVALID_YAML", "The file is not valid YAML.");
3114
3341
  }
3115
- return assertJsonRecord(parsed);
3342
+ assertWithinDepth(parsed, MAX_DEPTH);
3343
+ return assertJsonRecord(normalizeYamlTree(parsed));
3116
3344
  }
3117
3345
  function createYamlAdapter() {
3118
3346
  return createTreeFileAdapter({
@@ -3143,6 +3371,15 @@ function selectAdapter(format, registry = createDefaultRegistry()) {
3143
3371
  );
3144
3372
  }
3145
3373
 
3374
+ // src/flow/read-target.ts
3375
+ async function readTargetResource(input) {
3376
+ const path = localeFilePath(input.cwd, input.filesPattern, input.locale);
3377
+ if (!await input.fs.fileExists(path)) {
3378
+ return { locale: input.locale, namespace: "", format: input.format, entries: /* @__PURE__ */ new Map() };
3379
+ }
3380
+ return (await input.adapter.read(path, input.locale)).resource;
3381
+ }
3382
+
3146
3383
  // src/flow/select-locales.ts
3147
3384
  function selectLocales(config, requested) {
3148
3385
  if (requested === void 0) {
@@ -3183,11 +3420,14 @@ async function readSource(config, cwd, fs, adapter) {
3183
3420
 
3184
3421
  // src/flow/diff-locales.ts
3185
3422
  async function readTarget(cwd, config, adapter, fs, locale) {
3186
- const path = localeFilePath(cwd, config.files.pattern, locale);
3187
- if (!await fs.fileExists(path)) {
3188
- return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
3189
- }
3190
- return (await adapter.read(path, locale)).resource;
3423
+ return readTargetResource({
3424
+ cwd,
3425
+ filesPattern: config.files.pattern,
3426
+ format: config.format,
3427
+ locale,
3428
+ adapter,
3429
+ fs
3430
+ });
3191
3431
  }
3192
3432
  async function diffLocales(input, deps = {}) {
3193
3433
  const config = input.config;
@@ -3237,6 +3477,166 @@ async function diff(input, deps = {}) {
3237
3477
  return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
3238
3478
  }
3239
3479
 
3480
+ // src/flow/integrity-gate.ts
3481
+ function gateCandidateValue(sourceEntry, candidateValue, adapter) {
3482
+ const placeholderResult = adapter.comparePlaceholders?.(sourceEntry.value, candidateValue) ?? checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(candidateValue));
3483
+ if (!placeholderResult.matches) {
3484
+ return { accepted: false, reason: "placeholder" };
3485
+ }
3486
+ if (!adapter.validateMessage(candidateValue)) {
3487
+ return { accepted: false, reason: "icu" };
3488
+ }
3489
+ return { accepted: true };
3490
+ }
3491
+
3492
+ // src/flow/edit-entry.ts
3493
+ async function editEntry(input, deps = {}) {
3494
+ const config = input.config;
3495
+ const cwd = input.cwd ?? process.cwd();
3496
+ const fs = deps.fs ?? defaultFs;
3497
+ const adapter = selectAdapter(config.format, deps.adapterRegistry);
3498
+ const [locale] = selectLocales(config, [input.locale]);
3499
+ if (locale === void 0) {
3500
+ throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
3501
+ }
3502
+ const source = await readSource(config, cwd, fs, adapter);
3503
+ const sourceEntry = source.resource.entries.get(input.key);
3504
+ if (sourceEntry === void 0) {
3505
+ throw new SdkError(
3506
+ "UNKNOWN_KEY",
3507
+ `The key "${input.key}" was not found in the source resource.`
3508
+ );
3509
+ }
3510
+ return withLocaleWriteLock(cwd, locale, fs, async () => {
3511
+ const target = await readTarget(cwd, config, adapter, fs, locale);
3512
+ const gate = gateCandidateValue(sourceEntry, input.value, adapter);
3513
+ if (!gate.accepted) {
3514
+ return { accepted: false, reason: gate.reason, value: input.value };
3515
+ }
3516
+ const merged = new Map(target.entries);
3517
+ merged.set(input.key, { ...sourceEntry, value: input.value, namespace: target.namespace });
3518
+ const path = localeFilePath(cwd, config.files.pattern, locale);
3519
+ await adapter.write(
3520
+ { locale, namespace: target.namespace, format: config.format, entries: merged },
3521
+ path
3522
+ );
3523
+ await updateLockFileLocale(cwd, fs, locale, {
3524
+ mode: "merge",
3525
+ entries: { [input.key]: contentHash(sourceEntry) }
3526
+ });
3527
+ return { accepted: true, value: input.value };
3528
+ });
3529
+ }
3530
+
3531
+ // src/flow/key-integrity.ts
3532
+ function checkEntryIntegrity(adapter, sourceEntry, targetEntry) {
3533
+ const result = adapter.comparePlaceholders?.(sourceEntry.value, targetEntry.value) ?? checkPlaceholders(sourceEntry.placeholders, targetEntry.placeholders);
3534
+ return {
3535
+ key: sourceEntry.key,
3536
+ hasPlaceholders: sourceEntry.placeholders.length > 0,
3537
+ matches: result.matches,
3538
+ missing: result.missing,
3539
+ extra: result.extra,
3540
+ icuValid: adapter.validateMessage(targetEntry.value)
3541
+ };
3542
+ }
3543
+ function selectChangedKeys(changed, requested) {
3544
+ if (requested === void 0) {
3545
+ return changed;
3546
+ }
3547
+ const wanted = new Set(requested);
3548
+ return changed.filter((key) => wanted.has(key));
3549
+ }
3550
+ function integrityEntriesFor(source, target, adapter, changedKeys) {
3551
+ const entries = [];
3552
+ for (const key of changedKeys) {
3553
+ const sourceEntry = source.entries.get(key);
3554
+ const targetEntry = target.entries.get(key);
3555
+ if (sourceEntry === void 0 || targetEntry === void 0) {
3556
+ continue;
3557
+ }
3558
+ entries.push(checkEntryIntegrity(adapter, sourceEntry, targetEntry));
3559
+ }
3560
+ return entries;
3561
+ }
3562
+ async function keyIntegrity(input, deps = {}) {
3563
+ const config = input.config;
3564
+ const cwd = input.cwd ?? process.cwd();
3565
+ const fs = deps.fs ?? defaultFs;
3566
+ const adapter = selectAdapter(config.format, deps.adapterRegistry);
3567
+ const source = await readSource(config, cwd, fs, adapter);
3568
+ const lock = await readLockFile(lockFilePath(cwd), fs);
3569
+ return Promise.all(
3570
+ selectLocales(config, input.locales).map(async (locale) => {
3571
+ const target = await readTarget(cwd, config, adapter, fs, locale);
3572
+ const diffResult = diffResources(source.resource, target, {
3573
+ baseline: baselineFor(lock, locale)
3574
+ });
3575
+ const changedKeys = selectChangedKeys(diffResult.changed, input.keys);
3576
+ const entries = integrityEntriesFor(source.resource, target, adapter, changedKeys);
3577
+ return { locale, entries };
3578
+ })
3579
+ );
3580
+ }
3581
+
3582
+ // src/flow/key-value.ts
3583
+ async function keyValue(input, deps = {}) {
3584
+ const config = input.config;
3585
+ const cwd = input.cwd ?? process.cwd();
3586
+ const fs = deps.fs ?? defaultFs;
3587
+ const adapter = selectAdapter(config.format, deps.adapterRegistry);
3588
+ const [locale] = selectLocales(config, [input.locale]);
3589
+ if (locale === void 0) {
3590
+ throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
3591
+ }
3592
+ const source = await readSource(config, cwd, fs, adapter);
3593
+ const sourceEntry = source.resource.entries.get(input.key);
3594
+ if (sourceEntry === void 0) {
3595
+ throw new SdkError(
3596
+ "UNKNOWN_KEY",
3597
+ `The key "${input.key}" was not found in the source resource.`
3598
+ );
3599
+ }
3600
+ const target = await readTarget(cwd, config, adapter, fs, locale);
3601
+ const targetEntry = target.entries.get(input.key);
3602
+ return {
3603
+ source: sourceEntry.value,
3604
+ ...targetEntry !== void 0 ? { target: targetEntry.value } : {}
3605
+ };
3606
+ }
3607
+
3608
+ // src/flow/locale-snapshot.ts
3609
+ async function readLocaleFileSnapshot(input, deps = {}) {
3610
+ const cwd = input.cwd ?? process.cwd();
3611
+ const fs = deps.fs ?? defaultFs;
3612
+ const adapter = selectAdapter(input.config.format, deps.adapterRegistry);
3613
+ const resource = await readTarget(cwd, input.config, adapter, fs, input.locale);
3614
+ const hashes = /* @__PURE__ */ new Map();
3615
+ for (const [key, entry] of resource.entries) {
3616
+ hashes.set(key, contentHash(entry));
3617
+ }
3618
+ return { locale: input.locale, hashes };
3619
+ }
3620
+ function diffLocaleSnapshots(previous, current) {
3621
+ let added = 0;
3622
+ let changed = 0;
3623
+ for (const [key, hash] of current.hashes) {
3624
+ const previousHash = previous.hashes.get(key);
3625
+ if (previousHash === void 0) {
3626
+ added += 1;
3627
+ } else if (previousHash !== hash) {
3628
+ changed += 1;
3629
+ }
3630
+ }
3631
+ let removed = 0;
3632
+ for (const key of previous.hashes.keys()) {
3633
+ if (!current.hashes.has(key)) {
3634
+ removed += 1;
3635
+ }
3636
+ }
3637
+ return { added, changed, removed };
3638
+ }
3639
+
3240
3640
  // src/flow/lock-state.ts
3241
3641
  function toLockLocaleState(locale, keyCount, diff2) {
3242
3642
  return {
@@ -3284,6 +3684,174 @@ function selectProvider(config, createProvider = buildProvider) {
3284
3684
  }
3285
3685
  }
3286
3686
 
3687
+ // src/flow/retranslate-entry.ts
3688
+ function buildSingleEntryRequest(config, locale, sourceEntry, adapter) {
3689
+ return {
3690
+ sourceLocale: config.sourceLocale,
3691
+ targetLocale: locale,
3692
+ entries: [sourceEntry],
3693
+ extractPlaceholders: adapter.extractPlaceholders,
3694
+ ...config.glossary !== void 0 ? { glossary: config.glossary } : {},
3695
+ ...config.tone !== void 0 ? { tone: config.tone } : {},
3696
+ ...adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: adapter.comparePlaceholders } : {}
3697
+ };
3698
+ }
3699
+ async function retranslateEntry(input, deps = {}) {
3700
+ const config = input.config;
3701
+ const cwd = input.cwd ?? process.cwd();
3702
+ const fs = deps.fs ?? defaultFs;
3703
+ const adapter = selectAdapter(config.format, deps.adapterRegistry);
3704
+ const [locale] = selectLocales(config, [input.locale]);
3705
+ if (locale === void 0) {
3706
+ throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
3707
+ }
3708
+ const source = await readSource(config, cwd, fs, adapter);
3709
+ const sourceEntry = source.resource.entries.get(input.key);
3710
+ if (sourceEntry === void 0) {
3711
+ throw new SdkError(
3712
+ "UNKNOWN_KEY",
3713
+ `The key "${input.key}" was not found in the source resource.`
3714
+ );
3715
+ }
3716
+ const provider = selectProvider(config.provider, deps.createProvider);
3717
+ return withLocaleWriteLock(cwd, locale, fs, async () => {
3718
+ const target = await readTarget(cwd, config, adapter, fs, locale);
3719
+ const result = await provider.translateBatch(
3720
+ buildSingleEntryRequest(config, locale, sourceEntry, adapter)
3721
+ );
3722
+ const value = result.values.get(input.key);
3723
+ if (value === void 0) {
3724
+ throw new ProviderError(
3725
+ "INVALID_RESPONSE",
3726
+ `The provider returned no translated value for key "${input.key}".`
3727
+ );
3728
+ }
3729
+ const gate = gateCandidateValue(sourceEntry, value, adapter);
3730
+ if (!gate.accepted) {
3731
+ return { accepted: false, reason: gate.reason, value };
3732
+ }
3733
+ const merged = new Map(target.entries);
3734
+ merged.set(input.key, { ...sourceEntry, value, namespace: target.namespace });
3735
+ const path = localeFilePath(cwd, config.files.pattern, locale);
3736
+ await adapter.write(
3737
+ { locale, namespace: target.namespace, format: config.format, entries: merged },
3738
+ path
3739
+ );
3740
+ await updateLockFileLocale(cwd, fs, locale, {
3741
+ mode: "merge",
3742
+ entries: { [input.key]: contentHash(sourceEntry) }
3743
+ });
3744
+ const reviewReasons = result.reviewFlags?.get(input.key)?.reasons ?? [];
3745
+ return { accepted: true, value, reviewReasons };
3746
+ });
3747
+ }
3748
+ var RUN_STATUS_DIR_NAME = ".verbatra-local";
3749
+ var RUN_STATUS_FILE_NAME = "run-status.json";
3750
+ var CURRENT_VERSION2 = 1;
3751
+ var MAX_RUN_STATUS_FILE_BYTES = 16 * 1024 * 1024;
3752
+ var reviewReasonCodeSchema = zod.z.enum([
3753
+ "LENGTH_RATIO_OUTLIER",
3754
+ "EQUALS_SOURCE",
3755
+ "GLOSSARY_TERM_MISSED",
3756
+ "INTEGRITY_REORDERED",
3757
+ "PROVIDER_DEGRADED"
3758
+ ]);
3759
+ var needsReviewEntrySchema = zod.z.object({
3760
+ key: zod.z.string(),
3761
+ reasons: zod.z.array(reviewReasonCodeSchema)
3762
+ });
3763
+ var usageSummarySchema = zod.z.object({
3764
+ inputTokens: zod.z.number().int().nonnegative(),
3765
+ outputTokens: zod.z.number().int().nonnegative()
3766
+ });
3767
+ var runBudgetSchema = zod.z.object({
3768
+ maxTokens: zod.z.number().int().nonnegative(),
3769
+ behavior: zod.z.enum(["warn", "stop"]),
3770
+ supported: zod.z.boolean(),
3771
+ tokensUsed: zod.z.number().int().nonnegative(),
3772
+ exceeded: zod.z.boolean()
3773
+ });
3774
+ var runStatusLocaleSchema = zod.z.object({
3775
+ locale: zod.z.string(),
3776
+ status: zod.z.enum(["succeeded", "failed"]),
3777
+ needsReview: zod.z.array(needsReviewEntrySchema),
3778
+ usage: usageSummarySchema.optional()
3779
+ });
3780
+ var runStatusFileSchema = zod.z.object({
3781
+ version: zod.z.number().int().positive(),
3782
+ generatedAt: zod.z.string(),
3783
+ usage: usageSummarySchema.optional(),
3784
+ budget: runBudgetSchema.optional(),
3785
+ locales: zod.z.array(runStatusLocaleSchema)
3786
+ });
3787
+ function runStatusFilePath(cwd) {
3788
+ return path.resolve(cwd, RUN_STATUS_DIR_NAME, RUN_STATUS_FILE_NAME);
3789
+ }
3790
+ function toRunStatusLocale(locale) {
3791
+ return {
3792
+ locale: locale.locale,
3793
+ status: locale.status,
3794
+ needsReview: locale.needsReview,
3795
+ ...locale.usage !== void 0 ? { usage: locale.usage } : {}
3796
+ };
3797
+ }
3798
+ function buildRunStatusFile(summary, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
3799
+ return {
3800
+ version: CURRENT_VERSION2,
3801
+ generatedAt,
3802
+ ...summary.usage !== void 0 ? { usage: summary.usage } : {},
3803
+ ...summary.budget !== void 0 ? { budget: summary.budget } : {},
3804
+ locales: summary.locales.map(toRunStatusLocale)
3805
+ };
3806
+ }
3807
+ function fromParsed(data) {
3808
+ return {
3809
+ version: data.version,
3810
+ generatedAt: data.generatedAt,
3811
+ ...data.usage !== void 0 ? { usage: data.usage } : {},
3812
+ ...data.budget !== void 0 ? { budget: data.budget } : {},
3813
+ locales: data.locales.map((locale) => ({
3814
+ locale: locale.locale,
3815
+ status: locale.status,
3816
+ needsReview: locale.needsReview,
3817
+ ...locale.usage !== void 0 ? { usage: locale.usage } : {}
3818
+ }))
3819
+ };
3820
+ }
3821
+ async function readRunStatusFile(path, fs) {
3822
+ const read = await fs.readFileBounded(path, MAX_RUN_STATUS_FILE_BYTES);
3823
+ if (read.kind !== "ok") {
3824
+ return void 0;
3825
+ }
3826
+ let parsed;
3827
+ try {
3828
+ parsed = JSON.parse(read.content);
3829
+ } catch {
3830
+ return void 0;
3831
+ }
3832
+ const result = runStatusFileSchema.safeParse(parsed);
3833
+ if (!result.success || result.data.version !== CURRENT_VERSION2) {
3834
+ return void 0;
3835
+ }
3836
+ return fromParsed(result.data);
3837
+ }
3838
+ async function writeRunStatusFile(path$1, data, fs) {
3839
+ await promises.mkdir(path.dirname(path$1), { recursive: true });
3840
+ await fs.writeFile(path$1, `${JSON.stringify(data, null, 2)}
3841
+ `);
3842
+ }
3843
+
3844
+ // src/flow/run-status.ts
3845
+ async function runStatus(input = {}, deps = {}) {
3846
+ const cwd = input.cwd ?? process.cwd();
3847
+ const fs = deps.fs ?? defaultFs;
3848
+ const file = await readRunStatusFile(runStatusFilePath(cwd), fs);
3849
+ if (file === void 0) {
3850
+ return { available: false };
3851
+ }
3852
+ return { available: true, ...file };
3853
+ }
3854
+
3287
3855
  // src/flow/budget.ts
3288
3856
  function createBudgetTracker(maxTokens, behavior) {
3289
3857
  return { maxTokens, behavior, tokensUsed: 0, usageSeen: false, exceeded: false, stopped: false };
@@ -3566,7 +4134,6 @@ function syntheticEntry(item) {
3566
4134
  ...item.sourceEntry,
3567
4135
  key: item.targetKey,
3568
4136
  isPlural: true,
3569
- // The CLDR category travels as data context (the meaning field), never the instruction channel.
3570
4137
  meaning: `CLDR plural category "${item.category}"`
3571
4138
  };
3572
4139
  }
@@ -3643,17 +4210,17 @@ async function runGenerationSubBatch(context, batch, accepted, withheld, provide
3643
4210
  return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
3644
4211
  }
3645
4212
  for (const item of batch) {
3646
- foldGenerationItem(item, result, accepted, withheld, providerFailures);
4213
+ foldGenerationItem(item, result, context.adapter, accepted, withheld, providerFailures);
3647
4214
  }
3648
4215
  return { notices: readNotices(result), usage: result.usage };
3649
4216
  }
3650
- function foldGenerationItem(item, result, accepted, withheld, providerFailures) {
4217
+ function foldGenerationItem(item, result, adapter, accepted, withheld, providerFailures) {
3651
4218
  const value = result.values.get(item.targetKey);
3652
4219
  if (value === void 0) {
3653
4220
  providerFailures.push(item.targetKey);
3654
4221
  return;
3655
4222
  }
3656
- if (result.integrity.get(item.targetKey)?.matches === true) {
4223
+ if (gateCandidateValue(item.sourceEntry, value, adapter).accepted) {
3657
4224
  accepted.push({
3658
4225
  targetKey: item.targetKey,
3659
4226
  entry: { ...syntheticEntry(item), value },
@@ -3665,16 +4232,6 @@ function foldGenerationItem(item, result, accepted, withheld, providerFailures)
3665
4232
  }
3666
4233
 
3667
4234
  // src/flow/locale-run.ts
3668
- function emptyResource(locale, format) {
3669
- return { locale, namespace: "", format, entries: /* @__PURE__ */ new Map() };
3670
- }
3671
- async function readTarget2(params) {
3672
- const path = localeFilePath(params.cwd, params.filesPattern, params.targetLocale);
3673
- if (!await params.fs.fileExists(path)) {
3674
- return emptyResource(params.targetLocale, params.format);
3675
- }
3676
- return (await params.adapter.read(path, params.targetLocale)).resource;
3677
- }
3678
4235
  function buildRequest3(params, entries) {
3679
4236
  return {
3680
4237
  sourceLocale: params.sourceLocale,
@@ -3687,7 +4244,14 @@ function buildRequest3(params, entries) {
3687
4244
  };
3688
4245
  }
3689
4246
  async function runLocale(params) {
3690
- const target = await readTarget2(params);
4247
+ const target = await readTargetResource({
4248
+ cwd: params.cwd,
4249
+ filesPattern: params.filesPattern,
4250
+ format: params.format,
4251
+ locale: params.targetLocale,
4252
+ adapter: params.adapter,
4253
+ fs: params.fs
4254
+ });
3691
4255
  const diff2 = diffResources(params.source, target, { baseline: params.baseline });
3692
4256
  const orphaned = params.generatePlurals ? diff2.orphaned.filter((key) => !isGeneratedPluralKey(key, sourcePluralBaseKeys(params.source))) : diff2.orphaned;
3693
4257
  const pruned = params.prune ? orphaned : [];
@@ -3741,8 +4305,11 @@ async function runLocale(params) {
3741
4305
  for (const key of pruned) {
3742
4306
  merged.delete(key);
3743
4307
  }
3744
- for (const [key, { value, source }] of accepted) {
3745
- merged.set(key, { ...source, value, namespace: target.namespace });
4308
+ for (const key of params.source.entries.keys()) {
4309
+ const hit = accepted.get(key);
4310
+ if (hit !== void 0) {
4311
+ merged.set(key, { ...hit.source, value: hit.value, namespace: target.namespace });
4312
+ }
3746
4313
  }
3747
4314
  const generation = await runGeneration(params, provider);
3748
4315
  for (const form of generation.accepted) {
@@ -3771,7 +4338,6 @@ async function runLocale(params) {
3771
4338
  ...invalidIcuSource,
3772
4339
  ...generation.withheld,
3773
4340
  ...generation.providerFailures,
3774
- // Source-present keys withheld by the budget guardrail must keep their prior lock hash too.
3775
4341
  ...budgetWithheld
3776
4342
  ]);
3777
4343
  const localeUsage = combineUsage(translation.usage, generation.usage);
@@ -3783,9 +4349,7 @@ async function runLocale(params) {
3783
4349
  invalidIcuSource,
3784
4350
  translated: [...accepted.keys()],
3785
4351
  generated: generation.accepted.map((form) => form.targetKey).sort(),
3786
- // Withheld generated forms surface alongside withheld translations: both failed integrity.
3787
4352
  integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
3788
- // A generation sub-batch whose provider call itself threw is a provider failure, never integrity.
3789
4353
  providerFailures: [...providerFailures, ...generation.providerFailures].sort(),
3790
4354
  budgetWithheld: [...budgetWithheld, ...generation.budgetWithheld].sort(),
3791
4355
  pruned,
@@ -3903,7 +4467,7 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
3903
4467
  return { notices: [subBatchFailedNotice(batch.length, error)], usage: void 0 };
3904
4468
  }
3905
4469
  for (const entry of batch) {
3906
- foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures);
4470
+ foldEntryResult(entry, result, params.adapter, accepted, integrityMismatches, providerFailures);
3907
4471
  }
3908
4472
  if (result.reviewFlags !== void 0) {
3909
4473
  for (const [key, flag] of result.reviewFlags) {
@@ -3912,13 +4476,13 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
3912
4476
  }
3913
4477
  return { notices: readNotices(result), usage: result.usage };
3914
4478
  }
3915
- function foldEntryResult(entry, result, accepted, integrityMismatches, providerFailures) {
4479
+ function foldEntryResult(entry, result, adapter, accepted, integrityMismatches, providerFailures) {
3916
4480
  const value = result.values.get(entry.key);
3917
4481
  if (value === void 0) {
3918
4482
  providerFailures.push(entry.key);
3919
4483
  return;
3920
4484
  }
3921
- if (result.integrity.get(entry.key)?.matches === true) {
4485
+ if (gateCandidateValue(entry, value, adapter).accepted) {
3922
4486
  accepted.set(entry.key, { value, source: entry });
3923
4487
  } else {
3924
4488
  integrityMismatches.push(entry.key);
@@ -3960,6 +4524,79 @@ function carryGeneratedLock(lockEntries, baseline, key, sourceBaseKeys) {
3960
4524
  }
3961
4525
 
3962
4526
  // src/flow/translate-project.ts
4527
+ async function recordRunStatus(cwd, dryRun, summary, fs) {
4528
+ if (dryRun) {
4529
+ return;
4530
+ }
4531
+ try {
4532
+ await writeRunStatusFile(runStatusFilePath(cwd), buildRunStatusFile(summary), fs);
4533
+ } catch {
4534
+ }
4535
+ }
4536
+ function buildLocaleRunParams(context, targetLocale, baseline) {
4537
+ return {
4538
+ source: context.source.resource,
4539
+ sourceInvalidIcuKeys: context.source.invalidIcuKeys,
4540
+ baseline,
4541
+ adapter: context.adapter,
4542
+ provider: context.provider,
4543
+ cwd: context.cwd,
4544
+ filesPattern: context.config.files.pattern,
4545
+ sourceLocale: context.config.sourceLocale,
4546
+ targetLocale,
4547
+ format: context.config.format,
4548
+ glossary: context.config.glossary,
4549
+ tone: context.config.tone,
4550
+ prune: context.prune,
4551
+ generatePlurals: context.generatePlurals,
4552
+ maxBatchSize: context.maxBatchSize,
4553
+ fs: context.fs,
4554
+ budget: context.budget
4555
+ };
4556
+ }
4557
+ async function runDryLocale(context, targetLocale, lock) {
4558
+ const params = buildLocaleRunParams(context, targetLocale, baselineFor(lock, targetLocale));
4559
+ return (await runLocale(params)).summary;
4560
+ }
4561
+ async function runLiveLocale(context, targetLocale) {
4562
+ return withLocaleWriteLock(context.cwd, targetLocale, context.fs, async () => {
4563
+ const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
4564
+ const params = buildLocaleRunParams(context, targetLocale, baselineFor(lock, targetLocale));
4565
+ const result = await runLocale(params);
4566
+ await updateLockFileLocale(context.cwd, context.fs, targetLocale, {
4567
+ mode: "replace",
4568
+ entries: result.lockEntries
4569
+ });
4570
+ return result.summary;
4571
+ });
4572
+ }
4573
+ async function runOneLocale(targetLocale, run) {
4574
+ try {
4575
+ return await run();
4576
+ } catch (error) {
4577
+ if (error instanceof SdkError && error.code === "LOCK_FILE_INVALID") {
4578
+ throw error;
4579
+ }
4580
+ return failureSummary(targetLocale, error);
4581
+ }
4582
+ }
4583
+ async function runAllLocalesDry(context, targetLocales) {
4584
+ const lock = await readLockFile(lockFilePath(context.cwd), context.fs);
4585
+ const summaries = [];
4586
+ for (const targetLocale of targetLocales) {
4587
+ summaries.push(
4588
+ await runOneLocale(targetLocale, () => runDryLocale(context, targetLocale, lock))
4589
+ );
4590
+ }
4591
+ return summaries;
4592
+ }
4593
+ async function runAllLocalesLive(context, targetLocales) {
4594
+ const summaries = [];
4595
+ for (const targetLocale of targetLocales) {
4596
+ summaries.push(await runOneLocale(targetLocale, () => runLiveLocale(context, targetLocale)));
4597
+ }
4598
+ return summaries;
4599
+ }
3963
4600
  async function translate2(input, deps = {}) {
3964
4601
  const config = input.config;
3965
4602
  const cwd = input.cwd ?? process.cwd();
@@ -3975,47 +4612,26 @@ async function translate2(input, deps = {}) {
3975
4612
  const adapter = selectAdapter(config.format, deps.adapterRegistry);
3976
4613
  const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
3977
4614
  const source = await readSource(config, cwd, fs, adapter);
3978
- const lockPath = lockFilePath(cwd);
3979
- let lock = await readLockFile(lockPath, fs);
3980
- const summaries = [];
3981
- for (const targetLocale of config.targetLocales) {
3982
- try {
3983
- const params = {
3984
- source: source.resource,
3985
- sourceInvalidIcuKeys: source.invalidIcuKeys,
3986
- baseline: baselineFor(lock, targetLocale),
3987
- adapter,
3988
- provider,
3989
- cwd,
3990
- filesPattern: config.files.pattern,
3991
- sourceLocale: config.sourceLocale,
3992
- targetLocale,
3993
- format: config.format,
3994
- glossary: config.glossary,
3995
- tone: config.tone,
3996
- prune,
3997
- generatePlurals,
3998
- maxBatchSize,
3999
- fs,
4000
- budget
4001
- };
4002
- const { summary, lockEntries } = await runLocale(params);
4003
- if (!dryRun) {
4004
- lock = updateLockLocale(lock, targetLocale, lockEntries);
4005
- await writeLockFile(lockPath, lock, fs);
4006
- }
4007
- summaries.push(summary);
4008
- } catch (error) {
4009
- summaries.push(failureSummary(targetLocale, error));
4010
- }
4011
- }
4615
+ const context = {
4616
+ source,
4617
+ adapter,
4618
+ provider,
4619
+ cwd,
4620
+ config,
4621
+ prune,
4622
+ generatePlurals,
4623
+ maxBatchSize,
4624
+ fs,
4625
+ budget
4626
+ };
4627
+ const summaries = dryRun ? await runAllLocalesDry(context, config.targetLocales) : await runAllLocalesLive(context, config.targetLocales);
4012
4628
  const { succeeded, failed } = partition(summaries);
4013
4629
  const usage = summaries.reduce(
4014
- (total, summary) => combineUsage(total, summary.usage),
4630
+ (total, summary2) => combineUsage(total, summary2.usage),
4015
4631
  void 0
4016
4632
  );
4017
4633
  const budgetSummary = toBudgetSummary(budget);
4018
- return {
4634
+ const summary = {
4019
4635
  dryRun,
4020
4636
  locales: summaries,
4021
4637
  succeeded,
@@ -4023,6 +4639,8 @@ async function translate2(input, deps = {}) {
4023
4639
  ...usage !== void 0 ? { usage } : {},
4024
4640
  ...budgetSummary !== void 0 ? { budget: budgetSummary } : {}
4025
4641
  };
4642
+ await recordRunStatus(cwd, dryRun, summary, fs);
4643
+ return summary;
4026
4644
  }
4027
4645
  var ExchangeError = class extends Error {
4028
4646
  code;
@@ -4308,8 +4926,6 @@ var rowSchema = zod.z.object({
4308
4926
  sourceHash: zod.z.string(),
4309
4927
  translation: zod.z.string(),
4310
4928
  context: zod.z.string(),
4311
- // A legacy workbook exported before these columns existed has no such cell; cellString then yields
4312
- // "", and .catch maps it to "ok" / "" rather than rejecting the row.
4313
4929
  reviewStatus: zod.z.enum(["ok", "review"]).catch("ok"),
4314
4930
  reviewReasons: zod.z.string().catch("")
4315
4931
  });
@@ -4345,10 +4961,7 @@ function parseRow(sheet, row) {
4345
4961
  status: cellString(row.getCell(COLUMN.status)),
4346
4962
  sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
4347
4963
  translation: cellString(row.getCell(COLUMN.translation)),
4348
- // A workbook built before the Context column existed has no cell here; getCell auto-vivifies an
4349
- // empty one, so cellString yields "" and the row still validates, keeping import backward-compatible.
4350
4964
  context: cellString(row.getCell(COLUMN.context)),
4351
- // Same backward-compatible pattern for a workbook built before the Review columns existed.
4352
4965
  reviewStatus: cellString(row.getCell(COLUMN.reviewStatus)),
4353
4966
  reviewReasons: cellString(row.getCell(COLUMN.reviewReasons))
4354
4967
  };
@@ -4417,13 +5030,6 @@ async function readWorkbook(bytes, options = {}) {
4417
5030
 
4418
5031
  // src/flow/workbook/export-workbook.ts
4419
5032
  var DEFAULT_WORKBOOK_PATH = "verbatra-translations.xlsx";
4420
- async function readTarget3(cwd, config, adapter, fs, locale) {
4421
- const path = localeFilePath(cwd, config.files.pattern, locale);
4422
- if (!await fs.fileExists(path)) {
4423
- return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
4424
- }
4425
- return (await adapter.read(path, locale)).resource;
4426
- }
4427
5033
  function reasonLabel(reason) {
4428
5034
  return reason.toLowerCase().replace(/_/g, "-");
4429
5035
  }
@@ -4497,7 +5103,7 @@ async function exportWorkbook(input, deps = {}) {
4497
5103
  const locales = selectLocales(config, input.locales);
4498
5104
  const sheets = await Promise.all(
4499
5105
  locales.map(async (locale) => {
4500
- const target = await readTarget3(cwd, config, adapter, fs, locale);
5106
+ const target = await readTarget(cwd, config, adapter, fs, locale);
4501
5107
  const rows = buildRows(
4502
5108
  source.resource,
4503
5109
  target,
@@ -4535,14 +5141,8 @@ function judge(row, sourceEntry, adapter) {
4535
5141
  if (contentHash(sourceEntry) !== row.sourceHash) {
4536
5142
  return "drift";
4537
5143
  }
4538
- const integrity = adapter.comparePlaceholders !== void 0 ? adapter.comparePlaceholders(sourceEntry.value, row.translation) : checkPlaceholders(sourceEntry.placeholders, adapter.extractPlaceholders(row.translation));
4539
- if (!integrity.matches) {
4540
- return "placeholder";
4541
- }
4542
- if (!adapter.validateMessage(row.translation)) {
4543
- return "icu";
4544
- }
4545
- return void 0;
5144
+ const gate = gateCandidateValue(sourceEntry, row.translation, adapter);
5145
+ return gate.accepted ? void 0 : gate.reason;
4546
5146
  }
4547
5147
  function trackBlankDrift(row, params, buckets) {
4548
5148
  const sourceEntry = params.source.entries.get(row.key);
@@ -4599,19 +5199,13 @@ function importLocale(params) {
4599
5199
  translated: [...buckets.accepted.keys()].sort(),
4600
5200
  unchanged: diff2.unchanged,
4601
5201
  orphaned: diff2.orphaned,
4602
- // Import never prunes: orphans are reported but never removed here (pruning is a translate-flow concern).
4603
5202
  pruned: [],
4604
5203
  invalidIcuSource,
4605
5204
  integrityMismatches: [...buckets.mismatches].sort(),
4606
- // A workbook import never calls a provider, so a provider-call failure cannot occur here.
4607
5205
  providerFailures: [],
4608
- // A workbook import never calls a provider, so the budget guardrail never withholds anything here.
4609
5206
  budgetWithheld: [],
4610
- // Plural generation is a translate-flow concern; the manual workbook import never generates forms.
4611
5207
  generated: [],
4612
5208
  notices: buckets.blankDrifted.size > 0 ? [blankRowBaselineNotice(buckets.blankDrifted.size)] : [],
4613
- // A workbook import never calls a provider and never recomputes review flags on its own path; the
4614
- // Excel workbook's Review status/reasons columns are informational only (see export-workbook.ts).
4615
5209
  needsReview: []
4616
5210
  };
4617
5211
  return { summary, accepted: buckets.accepted, withheld: buckets.withheld };
@@ -4632,13 +5226,6 @@ async function readWorkbookBytes(path, fs) {
4632
5226
  }
4633
5227
  return read.bytes;
4634
5228
  }
4635
- async function readTarget4(cwd, config, adapter, fs, locale) {
4636
- const path = localeFilePath(cwd, config.files.pattern, locale);
4637
- if (!await fs.fileExists(path)) {
4638
- return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
4639
- }
4640
- return (await adapter.read(path, locale)).resource;
4641
- }
4642
5229
  function mergeAccepted(target, accepted) {
4643
5230
  const merged = new Map(target.entries);
4644
5231
  for (const [key, { value, source }] of accepted) {
@@ -4669,7 +5256,7 @@ async function runSheet(ctx, sheet, lock) {
4669
5256
  `The workbook has a sheet for locale "${sheet.locale}", which is not a configured target locale.`
4670
5257
  );
4671
5258
  }
4672
- const target = await readTarget4(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
5259
+ const target = await readTarget(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
4673
5260
  const baseline = baselineFor(lock, sheet.locale);
4674
5261
  const { summary, accepted } = importLocale({
4675
5262
  sheet,
@@ -4712,8 +5299,7 @@ async function importWorkbook(input, deps = {}) {
4712
5299
  } catch (error) {
4713
5300
  throw new SdkError("SOURCE_INVALID", error.message);
4714
5301
  }
4715
- const lockPath = lockFilePath(cwd);
4716
- let lock = await readLockFile(lockPath, fs);
5302
+ const lock = await readLockFile(lockFilePath(cwd), fs);
4717
5303
  const ctx = {
4718
5304
  config,
4719
5305
  cwd,
@@ -4726,10 +5312,18 @@ async function importWorkbook(input, deps = {}) {
4726
5312
  const summaries = [];
4727
5313
  for (const sheet of data.sheets) {
4728
5314
  try {
4729
- const { summary, lockEntries } = await runSheet(ctx, sheet, lock);
4730
- if (!dryRun) {
4731
- lock = updateLockLocale(lock, sheet.locale, lockEntries);
4732
- await writeLockFile(lockPath, lock, fs);
5315
+ let summary;
5316
+ if (dryRun) {
5317
+ summary = (await runSheet(ctx, sheet, lock)).summary;
5318
+ } else {
5319
+ summary = await withLocaleWriteLock(cwd, sheet.locale, fs, async () => {
5320
+ const result = await runSheet(ctx, sheet, lock);
5321
+ await updateLockFileLocale(cwd, fs, sheet.locale, {
5322
+ mode: "replace",
5323
+ entries: result.lockEntries
5324
+ });
5325
+ return result.summary;
5326
+ });
4733
5327
  }
4734
5328
  summaries.push(summary);
4735
5329
  } catch (error) {
@@ -4868,12 +5462,19 @@ exports.SdkError = SdkError;
4868
5462
  exports.check = check;
4869
5463
  exports.defineConfig = defineConfig;
4870
5464
  exports.diff = diff;
5465
+ exports.diffLocaleSnapshots = diffLocaleSnapshots;
5466
+ exports.editEntry = editEntry;
4871
5467
  exports.exportWorkbook = exportWorkbook;
4872
5468
  exports.importWorkbook = importWorkbook;
5469
+ exports.keyIntegrity = keyIntegrity;
5470
+ exports.keyValue = keyValue;
4873
5471
  exports.loadConfig = loadConfig;
4874
5472
  exports.loadConfigWithMeta = loadConfigWithMeta;
4875
5473
  exports.loadLockFile = loadLockFile;
4876
5474
  exports.lockState = lockState;
5475
+ exports.readLocaleFileSnapshot = readLocaleFileSnapshot;
5476
+ exports.retranslateEntry = retranslateEntry;
5477
+ exports.runStatus = runStatus;
4877
5478
  exports.scaffoldingMetadata = scaffoldingMetadata;
4878
5479
  exports.translate = translate2;
4879
5480
  exports.verbatraConfigSchema = verbatraConfigSchema;