@verbatra/sdk 0.4.4 → 0.5.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -4,6 +4,8 @@ var fs = require('fs');
4
4
  var path = require('path');
5
5
  var cosmiconfig = require('cosmiconfig');
6
6
  var cosmiconfigTypescriptLoader = require('cosmiconfig-typescript-loader');
7
+ var crypto = require('crypto');
8
+ var promises = require('fs/promises');
7
9
  var zod = require('zod');
8
10
  var Anthropic = require('@anthropic-ai/sdk');
9
11
  var deepl = require('deepl-node');
@@ -11,8 +13,6 @@ var module$1 = require('module');
11
13
  var log = require('loglevel');
12
14
  var genai = require('@google/genai');
13
15
  var OpenAI = require('openai');
14
- var crypto = require('crypto');
15
- var promises = require('fs/promises');
16
16
  var icuMessageformatParser = require('@formatjs/icu-messageformat-parser');
17
17
  var xmldom = require('@xmldom/xmldom');
18
18
  var yaml = require('yaml');
@@ -68,6 +68,155 @@ var SdkError = class extends Error {
68
68
  this.code = code;
69
69
  }
70
70
  };
71
+ async function readBoundedUtf8(handle, size) {
72
+ const buffer = Buffer.allocUnsafe(size);
73
+ let offset = 0;
74
+ while (offset < size) {
75
+ const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
76
+ if (bytesRead === 0) {
77
+ break;
78
+ }
79
+ offset += bytesRead;
80
+ }
81
+ return buffer.toString("utf8", 0, offset);
82
+ }
83
+ async function readBounded(path, maxBytes) {
84
+ let handle;
85
+ try {
86
+ handle = await promises.open(path, "r");
87
+ } catch {
88
+ return { kind: "missing" };
89
+ }
90
+ try {
91
+ const info = await handle.stat();
92
+ if (!info.isFile()) {
93
+ return { kind: "missing" };
94
+ }
95
+ if (info.size > maxBytes) {
96
+ return { kind: "too-large" };
97
+ }
98
+ return { kind: "ok", content: await readBoundedUtf8(handle, info.size) };
99
+ } finally {
100
+ await handle.close();
101
+ }
102
+ }
103
+ async function readBoundedBytesInto(handle, size) {
104
+ const buffer = Buffer.allocUnsafeSlow(size);
105
+ let offset = 0;
106
+ while (offset < size) {
107
+ const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
108
+ if (bytesRead === 0) {
109
+ break;
110
+ }
111
+ offset += bytesRead;
112
+ }
113
+ return new Uint8Array(buffer.buffer, buffer.byteOffset, offset);
114
+ }
115
+ async function readBoundedBytes(path, maxBytes) {
116
+ let handle;
117
+ try {
118
+ handle = await promises.open(path, "r");
119
+ } catch {
120
+ return { kind: "missing" };
121
+ }
122
+ try {
123
+ const info = await handle.stat();
124
+ if (!info.isFile()) {
125
+ return { kind: "missing" };
126
+ }
127
+ if (info.size > maxBytes) {
128
+ return { kind: "too-large" };
129
+ }
130
+ return { kind: "ok", bytes: await readBoundedBytesInto(handle, info.size) };
131
+ } finally {
132
+ await handle.close();
133
+ }
134
+ }
135
+ function tempFileName(path$1) {
136
+ return path.join(path.dirname(path$1), `.${path.basename(path$1)}.tmp-${process.pid}-${Date.now()}-${crypto.randomUUID()}`);
137
+ }
138
+ async function atomicWrite(path, data) {
139
+ const tmp = tempFileName(path);
140
+ await (typeof data === "string" ? promises.writeFile(tmp, data, "utf8") : promises.writeFile(tmp, data));
141
+ try {
142
+ await promises.rename(tmp, path);
143
+ } catch (error) {
144
+ await promises.rm(tmp, { force: true });
145
+ throw error;
146
+ }
147
+ }
148
+ var defaultFs = {
149
+ async fileExists(path) {
150
+ try {
151
+ await promises.access(path);
152
+ return true;
153
+ } catch {
154
+ return false;
155
+ }
156
+ },
157
+ readFileBounded: (path, maxBytes) => readBounded(path, maxBytes),
158
+ readBytesBounded: (path, maxBytes) => readBoundedBytes(path, maxBytes),
159
+ writeFile: (path, data) => atomicWrite(path, data),
160
+ writeBytes: (path, data) => atomicWrite(path, data)
161
+ };
162
+ var MAX_GLOSSARY_FILE_BYTES = 1024 * 1024;
163
+ var glossaryRecordSchema = zod.z.record(zod.z.string(), zod.z.string());
164
+ var BOM = "\uFEFF";
165
+ var REPLACEMENT_CHARACTER = "\uFFFD";
166
+ var NUL = "\0";
167
+ function stripBom(content) {
168
+ return content.startsWith(BOM) ? content.slice(BOM.length) : content;
169
+ }
170
+ function looksLikeInvalidEncoding(content) {
171
+ return content.startsWith(REPLACEMENT_CHARACTER) || content.includes(NUL);
172
+ }
173
+ async function readGlossaryFile(resolvedPath, fs) {
174
+ const read = await fs.readFileBounded(resolvedPath, MAX_GLOSSARY_FILE_BYTES);
175
+ if (read.kind === "missing") {
176
+ throw new SdkError(
177
+ "CONFIG_INVALID",
178
+ `The glossary file at ${resolvedPath} was not found or could not be read.`
179
+ );
180
+ }
181
+ if (read.kind === "too-large") {
182
+ throw new SdkError(
183
+ "CONFIG_INVALID",
184
+ `The glossary file at ${resolvedPath} exceeds the maximum allowed size of ${MAX_GLOSSARY_FILE_BYTES} bytes.`
185
+ );
186
+ }
187
+ const content = stripBom(read.content);
188
+ if (looksLikeInvalidEncoding(content)) {
189
+ throw new SdkError(
190
+ "CONFIG_INVALID",
191
+ `The glossary file at ${resolvedPath} must be UTF-8 encoded.`
192
+ );
193
+ }
194
+ let parsed;
195
+ try {
196
+ parsed = JSON.parse(content);
197
+ } catch {
198
+ throw new SdkError("CONFIG_INVALID", `The glossary file at ${resolvedPath} is not valid JSON.`);
199
+ }
200
+ const result = glossaryRecordSchema.safeParse(parsed);
201
+ if (!result.success) {
202
+ throw new SdkError(
203
+ "CONFIG_INVALID",
204
+ `The glossary file at ${resolvedPath} must contain a flat object of string keys to string values.`
205
+ );
206
+ }
207
+ return result.data;
208
+ }
209
+ async function resolveGlossary(glossary, baseDir, fs) {
210
+ if (glossary === void 0) {
211
+ return { glossary: void 0, provenance: { source: "none" } };
212
+ }
213
+ if (typeof glossary === "string") {
214
+ const resolvedPath = path.resolve(baseDir, glossary);
215
+ const record = await readGlossaryFile(resolvedPath, fs);
216
+ return { glossary: record, provenance: { source: "file", path: resolvedPath } };
217
+ }
218
+ return { glossary, provenance: { source: "inline" } };
219
+ }
71
220
  var FNV_OFFSET_BASIS = 14695981039346656037n;
72
221
  var FNV_PRIME = 1099511628211n;
73
222
  var U64_MASK = (1n << 64n) - 1n;
@@ -903,7 +1052,13 @@ var verbatraConfigSchema = zod.z.strictObject({
903
1052
  pattern: zod.z.string().min(1)
904
1053
  }),
905
1054
  provider: providerConfigSchema,
906
- glossary: zod.z.record(zod.z.string(), zod.z.string()).optional(),
1055
+ /**
1056
+ * A glossary of source terms to preferred target terms, either inline or as a path to a JSON file
1057
+ * carrying the same shape. The two forms are mutually exclusive by type; there is no merge or
1058
+ * precedence between them. A path is resolved and validated by the loader, not by this schema; see
1059
+ * `resolve-glossary.ts`.
1060
+ */
1061
+ glossary: zod.z.union([zod.z.record(zod.z.string(), zod.z.string()), zod.z.string().min(1)]).optional(),
907
1062
  tone: zod.z.enum(["formal", "informal", "neutral"]).optional(),
908
1063
  /**
909
1064
  * Opt-in orphan pruning, off by default. When true, keys present in a target file but absent from
@@ -957,7 +1112,7 @@ function formatIssues(error) {
957
1112
  return issue.code === "unrecognized_keys" ? `${base} (API keys are read from the environment, not the config)` : base;
958
1113
  }).join("; ");
959
1114
  }
960
- function validate(input) {
1115
+ function parseConfig(input) {
961
1116
  const parsed = verbatraConfigSchema.safeParse(input);
962
1117
  if (!parsed.success) {
963
1118
  throw new SdkError(
@@ -967,7 +1122,16 @@ function validate(input) {
967
1122
  }
968
1123
  return parsed.data;
969
1124
  }
970
- async function loadExplicit(explorer, configPath, cwd) {
1125
+ async function finalizeConfig(parsed, baseDir, fs) {
1126
+ const { glossary: glossaryInput, ...rest } = parsed;
1127
+ const resolved = await resolveGlossary(glossaryInput, baseDir, fs);
1128
+ const config = {
1129
+ ...rest,
1130
+ ...resolved.glossary !== void 0 ? { glossary: resolved.glossary } : {}
1131
+ };
1132
+ return { config, glossary: resolved.provenance };
1133
+ }
1134
+ async function loadExplicitWithMeta(explorer, configPath, cwd, fs$1) {
971
1135
  const resolved = path.resolve(cwd ?? process.cwd(), configPath);
972
1136
  if (!fs.existsSync(resolved)) {
973
1137
  throw new SdkError("CONFIG_NOT_FOUND", `No verbatra configuration file at ${resolved}.`);
@@ -979,18 +1143,23 @@ async function loadExplicit(explorer, configPath, cwd) {
979
1143
  const detail = error instanceof Error ? error.message : String(error);
980
1144
  throw new SdkError("CONFIG_INVALID", `Failed to load the verbatra configuration: ${detail}`);
981
1145
  }
982
- return validate(result?.config);
1146
+ const parsed = parseConfig(result?.config);
1147
+ const { config, glossary } = await finalizeConfig(parsed, path.dirname(resolved), fs$1);
1148
+ return { config, source: { kind: "explicit", filepath: resolved }, glossary };
983
1149
  }
984
- async function loadConfig(options = {}) {
1150
+ async function loadConfigWithMeta(options = {}) {
1151
+ const fs = options.fs ?? defaultFs;
985
1152
  if (options.configOverride !== void 0) {
986
- return validate(options.configOverride);
1153
+ const parsed2 = parseConfig(options.configOverride);
1154
+ const { config: config2, glossary: glossary2 } = await finalizeConfig(parsed2, options.cwd ?? process.cwd(), fs);
1155
+ return { config: config2, source: { kind: "override" }, glossary: glossary2 };
987
1156
  }
988
1157
  const explorer = cosmiconfig.cosmiconfig(MODULE_NAME, {
989
1158
  searchPlaces: SEARCH_PLACES,
990
1159
  loaders: { ".ts": cosmiconfigTypescriptLoader.TypeScriptLoader() }
991
1160
  });
992
1161
  if (options.configPath !== void 0) {
993
- return loadExplicit(explorer, options.configPath, options.cwd);
1162
+ return loadExplicitWithMeta(explorer, options.configPath, options.cwd, fs);
994
1163
  }
995
1164
  let result;
996
1165
  try {
@@ -1005,99 +1174,14 @@ async function loadConfig(options = {}) {
1005
1174
  "No verbatra configuration found. Create a verbatra.config.ts, a .verbatrarc.json, or a 'verbatra' property in package.json."
1006
1175
  );
1007
1176
  }
1008
- return validate(result.config);
1009
- }
1010
- async function readBoundedUtf8(handle, size) {
1011
- const buffer = Buffer.allocUnsafe(size);
1012
- let offset = 0;
1013
- while (offset < size) {
1014
- const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
1015
- if (bytesRead === 0) {
1016
- break;
1017
- }
1018
- offset += bytesRead;
1019
- }
1020
- return buffer.toString("utf8", 0, offset);
1021
- }
1022
- async function readBounded(path, maxBytes) {
1023
- let handle;
1024
- try {
1025
- handle = await promises.open(path, "r");
1026
- } catch {
1027
- return { kind: "missing" };
1028
- }
1029
- try {
1030
- const info = await handle.stat();
1031
- if (!info.isFile()) {
1032
- return { kind: "missing" };
1033
- }
1034
- if (info.size > maxBytes) {
1035
- return { kind: "too-large" };
1036
- }
1037
- return { kind: "ok", content: await readBoundedUtf8(handle, info.size) };
1038
- } finally {
1039
- await handle.close();
1040
- }
1041
- }
1042
- async function readBoundedBytesInto(handle, size) {
1043
- const buffer = Buffer.allocUnsafeSlow(size);
1044
- let offset = 0;
1045
- while (offset < size) {
1046
- const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
1047
- if (bytesRead === 0) {
1048
- break;
1049
- }
1050
- offset += bytesRead;
1051
- }
1052
- return new Uint8Array(buffer.buffer, buffer.byteOffset, offset);
1053
- }
1054
- async function readBoundedBytes(path, maxBytes) {
1055
- let handle;
1056
- try {
1057
- handle = await promises.open(path, "r");
1058
- } catch {
1059
- return { kind: "missing" };
1060
- }
1061
- try {
1062
- const info = await handle.stat();
1063
- if (!info.isFile()) {
1064
- return { kind: "missing" };
1065
- }
1066
- if (info.size > maxBytes) {
1067
- return { kind: "too-large" };
1068
- }
1069
- return { kind: "ok", bytes: await readBoundedBytesInto(handle, info.size) };
1070
- } finally {
1071
- await handle.close();
1072
- }
1073
- }
1074
- function tempFileName(path$1) {
1075
- return path.join(path.dirname(path$1), `.${path.basename(path$1)}.tmp-${process.pid}-${Date.now()}-${crypto.randomUUID()}`);
1177
+ const parsed = parseConfig(result.config);
1178
+ const { config, glossary } = await finalizeConfig(parsed, path.dirname(result.filepath), fs);
1179
+ return { config, source: { kind: "search", filepath: result.filepath }, glossary };
1076
1180
  }
1077
- async function atomicWrite(path, data) {
1078
- const tmp = tempFileName(path);
1079
- await (typeof data === "string" ? promises.writeFile(tmp, data, "utf8") : promises.writeFile(tmp, data));
1080
- try {
1081
- await promises.rename(tmp, path);
1082
- } catch (error) {
1083
- await promises.rm(tmp, { force: true });
1084
- throw error;
1085
- }
1181
+ async function loadConfig(options = {}) {
1182
+ const { config } = await loadConfigWithMeta(options);
1183
+ return config;
1086
1184
  }
1087
- var defaultFs = {
1088
- async fileExists(path) {
1089
- try {
1090
- await promises.access(path);
1091
- return true;
1092
- } catch {
1093
- return false;
1094
- }
1095
- },
1096
- readFileBounded: (path, maxBytes) => readBounded(path, maxBytes),
1097
- readBytesBounded: (path, maxBytes) => readBoundedBytes(path, maxBytes),
1098
- writeFile: (path, data) => atomicWrite(path, data),
1099
- writeBytes: (path, data) => atomicWrite(path, data)
1100
- };
1101
1185
  var LOCK_FILE_NAME = "verbatra.lock.json";
1102
1186
  var CURRENT_VERSION = 1;
1103
1187
  var EMPTY_LOCK = { version: CURRENT_VERSION, locales: {} };
@@ -2254,6 +2338,40 @@ async function diff(input, deps = {}) {
2254
2338
  return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
2255
2339
  }
2256
2340
 
2341
+ // src/flow/lock-state.ts
2342
+ function toLockLocaleState(locale, keyCount, diff2) {
2343
+ return {
2344
+ locale,
2345
+ keyCount,
2346
+ missing: diff2.missing.length,
2347
+ stale: diff2.changed.length,
2348
+ upToDate: diff2.unchanged.length
2349
+ };
2350
+ }
2351
+ async function lockState(input, deps = {}) {
2352
+ const config = input.config;
2353
+ const cwd = input.cwd ?? process.cwd();
2354
+ const fs = deps.fs ?? defaultFs;
2355
+ const locales = selectLocales(config, input.locales);
2356
+ const path = lockFilePath(cwd);
2357
+ const exists = await fs.fileExists(path);
2358
+ if (!exists) {
2359
+ return { exists: false };
2360
+ }
2361
+ const lock = await readLockFile(path, fs);
2362
+ const adapter = selectAdapter(config.format, deps.adapterRegistry);
2363
+ const source = await readSource(config, cwd, fs, adapter);
2364
+ const localeStates = await Promise.all(
2365
+ locales.map(async (locale) => {
2366
+ const target = await readTarget(cwd, config, adapter, fs, locale);
2367
+ const baseline = baselineFor(lock, locale);
2368
+ const diff2 = diffResources(source.resource, target, { baseline });
2369
+ return toLockLocaleState(locale, baseline.size, diff2);
2370
+ })
2371
+ );
2372
+ return { exists: true, version: lock.version, locales: localeStates };
2373
+ }
2374
+
2257
2375
  // src/selection/select-provider.ts
2258
2376
  function selectProvider(config, createProvider = buildProvider) {
2259
2377
  try {
@@ -3459,6 +3577,13 @@ async function importWorkbook(input, deps = {}) {
3459
3577
  return { dryRun, locales: summaries, succeeded, failed };
3460
3578
  }
3461
3579
 
3580
+ // src/lock/load-lock-file.ts
3581
+ async function loadLockFile(input = {}, deps = {}) {
3582
+ const cwd = input.cwd ?? process.cwd();
3583
+ const fs = deps.fs ?? defaultFs;
3584
+ return readLockFile(lockFilePath(cwd), fs);
3585
+ }
3586
+
3462
3587
  // src/scaffolding.ts
3463
3588
  var scaffoldingMetadata = {
3464
3589
  /** Provider id -> the environment variable its API key is read from. Owned by ai-providers. */
@@ -3575,6 +3700,7 @@ async function watch(input, deps = {}) {
3575
3700
  }
3576
3701
 
3577
3702
  exports.DEFAULT_WORKBOOK_PATH = DEFAULT_WORKBOOK_PATH;
3703
+ exports.LOCK_FILE_NAME = LOCK_FILE_NAME;
3578
3704
  exports.SdkError = SdkError;
3579
3705
  exports.check = check;
3580
3706
  exports.defineConfig = defineConfig;
@@ -3582,6 +3708,9 @@ exports.diff = diff;
3582
3708
  exports.exportWorkbook = exportWorkbook;
3583
3709
  exports.importWorkbook = importWorkbook;
3584
3710
  exports.loadConfig = loadConfig;
3711
+ exports.loadConfigWithMeta = loadConfigWithMeta;
3712
+ exports.loadLockFile = loadLockFile;
3713
+ exports.lockState = lockState;
3585
3714
  exports.scaffoldingMetadata = scaffoldingMetadata;
3586
3715
  exports.translate = translate2;
3587
3716
  exports.verbatraConfigSchema = verbatraConfigSchema;