@happyvertical/smrt-languages 0.37.2 → 0.37.3

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.
@@ -1,284 +0,0 @@
1
- import { getAI } from "@happyvertical/ai";
2
- import { createLogger } from "@happyvertical/logger";
3
- import { getPackageConfig } from "@happyvertical/smrt-config";
4
- import { smrt, SmrtObject } from "@happyvertical/smrt-core";
5
- import { FeatureResolver } from "@happyvertical/smrt-features";
6
- import { SmrtJobCollection } from "@happyvertical/smrt-jobs";
7
- import { definePrompt, resolvePrompt } from "@happyvertical/smrt-prompts";
8
- import { n as normalizeLocale, L as LanguageOverrideCollection, b as buildTenantGlossary, c as computeSourceHash, i as invalidateLanguageCache, d as buildTranslationJobId, a as LanguageRegistry } from "./language-registry-CgsuwQo6.js";
9
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
- var __decorateClass = (decorators, target, key, kind) => {
11
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
12
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
13
- if (decorator = decorators[i])
14
- result = decorator(result) || result;
15
- return result;
16
- };
17
- const logger = createLogger({ level: "info" });
18
- const AUTO_TRANSLATE_FEATURE_KEY = "smrt-languages.auto_translate";
19
- const TRANSLATION_PROMPT_KEY = "smrt-languages.translation";
20
- const PLACEHOLDER_EXAMPLE = '"{name}"';
21
- const RESPONSE_SHAPE_EXAMPLE = '{"translation": "Hola, {name}"}';
22
- definePrompt({
23
- key: TRANSLATION_PROMPT_KEY,
24
- template: 'You translate user-facing application strings from {sourceLocale} to {targetLocale}.\nPreserve any placeholders such as {placeholderExample} exactly as they appear, including the braces. Do not add markup or commentary.\nMatch the organization glossary where applicable:\n{glossary}\nString to translate: "{template}"\nReply with a JSON object shaped like {responseShapeExample} where the translation field is your translated string.',
25
- editable: {
26
- template: true,
27
- profile: true,
28
- model: true,
29
- params: true
30
- }
31
- });
32
- let LanguageTranslationTask = class extends SmrtObject {
33
- /**
34
- * Job-runner entrypoint. Reads the translation payload, calls
35
- * `@happyvertical/ai`, and upserts a `LanguageOverride` row.
36
- */
37
- async execute(args) {
38
- if (!args || !args.key || !args.targetLocale || !args.sourceTemplate) {
39
- throw new Error(
40
- "LanguageTranslationTask.execute requires a translation payload"
41
- );
42
- }
43
- const config = getLanguagesConfig();
44
- const targetLocale = normalizeLocale(args.targetLocale);
45
- const sourceLocale = normalizeLocale(args.sourceLocale);
46
- if (Array.isArray(config.supportedLocales) && config.supportedLocales.length > 0 && !config.supportedLocales.map(normalizeLocale).includes(targetLocale)) {
47
- return { skipped: "not_supported" };
48
- }
49
- if (await isAutoTranslateDisabled(args.tenantId ?? null, this.options)) {
50
- return { skipped: "feature_disabled" };
51
- }
52
- const overrides = await LanguageOverrideCollection.create(this.options);
53
- const existing = await overrides.getAppOverride(args.key, targetLocale);
54
- if (existing?.auto_generated && existing.source_hash === args.sourceHash) {
55
- return { skipped: "stale", template: existing.template };
56
- }
57
- if (existing && !existing.auto_generated) {
58
- return { skipped: "stale", template: existing.template };
59
- }
60
- if (args.tenantId && typeof config.translationBudgetPerTenantPerDay === "number") {
61
- const used = await countTodayTenantTranslations(
62
- this.options,
63
- args.tenantId
64
- );
65
- if (used >= config.translationBudgetPerTenantPerDay) {
66
- return { skipped: "budget" };
67
- }
68
- }
69
- let glossary = "";
70
- if (args.tenantId) {
71
- const tenantOverrides = await overrides.listTenantOverrides(
72
- args.tenantId
73
- );
74
- glossary = buildTenantGlossary(tenantOverrides, {
75
- sourceLocale,
76
- targetLocale,
77
- max: 25
78
- });
79
- }
80
- if (!glossary) {
81
- glossary = "(no organization glossary)";
82
- }
83
- const prompt = await resolvePrompt(TRANSLATION_PROMPT_KEY, {
84
- db: this.options.db,
85
- tenantId: args.tenantId ?? null,
86
- variables: {
87
- sourceLocale,
88
- targetLocale,
89
- template: args.sourceTemplate,
90
- glossary,
91
- placeholderExample: PLACEHOLDER_EXAMPLE,
92
- responseShapeExample: RESPONSE_SHAPE_EXAMPLE
93
- }
94
- });
95
- const promptAi = prompt.ai ?? {};
96
- const mergedAiConfig = { ...promptAi };
97
- if (args.model) mergedAiConfig.model = args.model;
98
- const ai = await getAI(mergedAiConfig);
99
- const message = await ai.message(prompt.text, {
100
- ...mergedAiConfig,
101
- responseFormat: { type: "json_object" }
102
- });
103
- const translated = parseTranslationResponse(message);
104
- if (!translated) {
105
- throw new Error(
106
- `LanguageTranslationTask: AI returned no usable translation for "${args.key}" → ${targetLocale}`
107
- );
108
- }
109
- if (containsObviousMarkupLeak(translated)) {
110
- throw new Error(
111
- `LanguageTranslationTask: refusing to persist suspicious translation for "${args.key}"`
112
- );
113
- }
114
- const aiModel = args.model ?? promptAi.model ?? null;
115
- const sourceHash = args.sourceHash || computeSourceHash(args.sourceTemplate);
116
- if (existing) {
117
- existing.template = translated;
118
- existing.auto_generated = true;
119
- existing.source_hash = sourceHash;
120
- existing.ai_model = aiModel;
121
- existing.reviewed_at = null;
122
- existing.reviewed_by = null;
123
- await existing.save();
124
- } else {
125
- await overrides.create({
126
- key: args.key,
127
- locale: targetLocale,
128
- tenantId: null,
129
- template: translated,
130
- auto_generated: true,
131
- source_hash: sourceHash,
132
- ai_model: aiModel,
133
- reviewed_at: null,
134
- reviewed_by: null
135
- });
136
- }
137
- invalidateLanguageCache(args.key, targetLocale, null, this.db);
138
- return { template: translated };
139
- }
140
- };
141
- LanguageTranslationTask = __decorateClass([
142
- smrt({
143
- api: { include: [] },
144
- cli: { include: [] },
145
- mcp: { include: [] }
146
- })
147
- ], LanguageTranslationTask);
148
- async function enqueueTranslationJob(options) {
149
- const targetLocale = normalizeLocale(options.targetLocale);
150
- const sourceLocale = normalizeLocale(options.sourceLocale ?? "en");
151
- const dedupId = buildTranslationJobId(options.key, targetLocale);
152
- const definition = LanguageRegistry.get(options.key, sourceLocale);
153
- if (!definition) {
154
- return { id: dedupId, status: "skipped" };
155
- }
156
- const config = getLanguagesConfig();
157
- if (Array.isArray(config.supportedLocales) && config.supportedLocales.length > 0 && !config.supportedLocales.map(normalizeLocale).includes(targetLocale)) {
158
- return { id: dedupId, status: "skipped" };
159
- }
160
- const jobs = await SmrtJobCollection.create({ db: options.db });
161
- if (!options.force) {
162
- const existing = await findPendingTranslationJob(
163
- jobs,
164
- options.key,
165
- targetLocale
166
- );
167
- if (existing) {
168
- return { id: dedupId, status: "duplicate" };
169
- }
170
- }
171
- const payload = {
172
- key: options.key,
173
- sourceLocale,
174
- sourceTemplate: definition.template,
175
- sourceHash: definition.sourceHash,
176
- targetLocale,
177
- tenantId: options.tenantId ?? null
178
- };
179
- const job = await jobs.create({
180
- queue: "languages",
181
- objectType: "LanguageTranslationTask",
182
- objectId: null,
183
- method: "execute",
184
- tenantId: options.tenantId ?? null,
185
- args: { ...payload, _dedupId: dedupId },
186
- runAt: /* @__PURE__ */ new Date(),
187
- priority: 25
188
- });
189
- await job.save();
190
- return { id: dedupId, status: "enqueued" };
191
- }
192
- const DEFAULT_DEDUP_SCAN_LIMIT = 500;
193
- async function findPendingTranslationJob(jobs, key, targetLocale) {
194
- const dedupId = buildTranslationJobId(key, targetLocale);
195
- const config = getLanguagesConfig();
196
- const scanLimit = typeof config.dedupScanLimit === "number" ? config.dedupScanLimit : DEFAULT_DEDUP_SCAN_LIMIT;
197
- const rows = await jobs.list({
198
- where: {
199
- objectType: "LanguageTranslationTask",
200
- method: "execute",
201
- status: ["pending", "running"],
202
- queue: "languages"
203
- },
204
- orderBy: "createdAt DESC",
205
- limit: scanLimit
206
- });
207
- for (const row of rows) {
208
- const args = row.args ?? {};
209
- if (args._dedupId === dedupId) return row;
210
- if (args.key === key && typeof args.targetLocale === "string" && normalizeLocale(args.targetLocale) === targetLocale) {
211
- return row;
212
- }
213
- }
214
- if (rows.length === scanLimit) {
215
- logger.warn(
216
- `[smrt-languages] translation-job dedup scan hit its limit (${scanLimit}); raise packages.languages.dedupScanLimit if duplicate jobs appear`
217
- );
218
- }
219
- return null;
220
- }
221
- async function countTodayTenantTranslations(options, tenantId) {
222
- const jobs = await SmrtJobCollection.create({ db: options.db });
223
- const since = /* @__PURE__ */ new Date();
224
- since.setUTCHours(0, 0, 0, 0);
225
- const rows = await jobs.list({
226
- where: {
227
- objectType: "LanguageTranslationTask",
228
- method: "execute",
229
- tenantId,
230
- "createdAt >=": since.toISOString()
231
- }
232
- });
233
- return rows.length;
234
- }
235
- function getLanguagesConfig() {
236
- return getPackageConfig("languages", {
237
- defaultLocale: "en",
238
- overrides: {}
239
- });
240
- }
241
- async function isAutoTranslateDisabled(tenantId, options) {
242
- try {
243
- const resolver = new FeatureResolver(options);
244
- const enabled = await resolver.isEnabled(AUTO_TRANSLATE_FEATURE_KEY, {
245
- tenantId: tenantId ?? void 0
246
- });
247
- return enabled === false;
248
- } catch {
249
- return false;
250
- }
251
- }
252
- function parseTranslationResponse(message) {
253
- if (!message) return null;
254
- let parseFailed = false;
255
- let parsed;
256
- try {
257
- parsed = JSON.parse(message);
258
- } catch {
259
- parseFailed = true;
260
- }
261
- if (!parseFailed) {
262
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.translation === "string") {
263
- const cleaned = parsed.translation.trim();
264
- return cleaned.length > 0 ? cleaned : null;
265
- }
266
- if (typeof parsed === "string") {
267
- const cleaned = parsed.trim();
268
- return cleaned.length > 0 ? cleaned : null;
269
- }
270
- return null;
271
- }
272
- const trimmed = String(message).trim();
273
- return trimmed.length > 0 ? trimmed : null;
274
- }
275
- function containsObviousMarkupLeak(value) {
276
- return /<\/?(script|html|body|iframe|system)/i.test(value);
277
- }
278
- export {
279
- AUTO_TRANSLATE_FEATURE_KEY,
280
- LanguageTranslationTask,
281
- TRANSLATION_PROMPT_KEY,
282
- enqueueTranslationJob
283
- };
284
- //# sourceMappingURL=translation-job-BMzCflao.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"translation-job-BMzCflao.js","sources":["../../src/translation-job.ts"],"sourcesContent":["import { type GetAIOptions, getAI } from '@happyvertical/ai';\nimport { createLogger } from '@happyvertical/logger';\nimport { getPackageConfig } from '@happyvertical/smrt-config';\nimport {\n type SmrtClassOptions,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { FeatureResolver } from '@happyvertical/smrt-features';\nimport { SmrtJobCollection } from '@happyvertical/smrt-jobs';\nimport { definePrompt, resolvePrompt } from '@happyvertical/smrt-prompts';\nimport { invalidateLanguageCache } from './cache.js';\nimport { LanguageOverrideCollection } from './collections/LanguageOverrideCollection.js';\nimport { buildTenantGlossary } from './glossary.js';\nimport { LanguageRegistry } from './language-registry.js';\nimport type { LanguagesPackageConfig, TranslationJobPayload } from './types.js';\nimport {\n buildTranslationJobId,\n computeSourceHash,\n normalizeLocale,\n} from './utils.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/** Feature flag key honored as the kill-switch for AI auto-translation. */\nexport const AUTO_TRANSLATE_FEATURE_KEY = 'smrt-languages.auto_translate';\n\n/**\n * Prompt key registered with `smrt-prompts` so ops can tune the AI translation\n * style without redeploying. The prompt itself is referenced by the job\n * handler when calling `@happyvertical/ai`.\n */\nexport const TRANSLATION_PROMPT_KEY = 'smrt-languages.translation';\n\n/**\n * Brace-containing example values that must reach the model verbatim. The\n * prompt renderer treats every `{...}` as a variable, so anything we want the\n * model to literally see (the placeholder syntax we're asking it to preserve,\n * the JSON shape we're asking it to return) is injected via a variable rather\n * than being baked into the static template.\n */\nconst PLACEHOLDER_EXAMPLE = '\"{name}\"';\nconst RESPONSE_SHAPE_EXAMPLE = '{\"translation\": \"Hola, {name}\"}';\n\ndefinePrompt({\n key: TRANSLATION_PROMPT_KEY,\n template:\n 'You translate user-facing application strings from {sourceLocale} to {targetLocale}.\\n' +\n 'Preserve any placeholders such as {placeholderExample} exactly as they appear, including the braces. Do not add markup or commentary.\\n' +\n 'Match the organization glossary where applicable:\\n{glossary}\\n' +\n 'String to translate: \"{template}\"\\n' +\n 'Reply with a JSON object shaped like {responseShapeExample} where the translation field is your translated string.',\n editable: {\n template: true,\n profile: true,\n model: true,\n params: true,\n },\n});\n\n/**\n * SmrtObject that owns the translation job's `execute` method. The TaskRunner\n * resolves jobs by `objectType` so the registered class name needs to match\n * what `enqueueTranslationJob` writes.\n */\n@smrt({\n api: { include: [] },\n cli: { include: [] },\n mcp: { include: [] },\n})\nexport class LanguageTranslationTask extends SmrtObject {\n /**\n * Job-runner entrypoint. Reads the translation payload, calls\n * `@happyvertical/ai`, and upserts a `LanguageOverride` row.\n */\n async execute(args: TranslationJobPayload): Promise<{\n skipped?: 'feature_disabled' | 'not_supported' | 'budget' | 'stale';\n template?: string;\n }> {\n if (!args || !args.key || !args.targetLocale || !args.sourceTemplate) {\n throw new Error(\n 'LanguageTranslationTask.execute requires a translation payload',\n );\n }\n\n const config = getLanguagesConfig();\n const targetLocale = normalizeLocale(args.targetLocale);\n const sourceLocale = normalizeLocale(args.sourceLocale);\n\n // Locale allowlist (cheap pre-check before any AI / DB work).\n if (\n Array.isArray(config.supportedLocales) &&\n config.supportedLocales.length > 0 &&\n !config.supportedLocales.map(normalizeLocale).includes(targetLocale)\n ) {\n return { skipped: 'not_supported' };\n }\n\n // Feature flag kill switch (best-effort — never blocks if unavailable).\n if (await isAutoTranslateDisabled(args.tenantId ?? null, this.options)) {\n return { skipped: 'feature_disabled' };\n }\n\n const overrides = await LanguageOverrideCollection.create(this.options);\n\n // Source-hash gate: if a translation already exists with the same source,\n // do nothing. Only re-translate when the source changed.\n const existing = await overrides.getAppOverride(args.key, targetLocale);\n if (existing?.auto_generated && existing.source_hash === args.sourceHash) {\n return { skipped: 'stale', template: existing.template };\n }\n if (existing && !existing.auto_generated) {\n // Human-edited rows are never overwritten.\n return { skipped: 'stale', template: existing.template };\n }\n\n // Per-tenant daily budget.\n if (\n args.tenantId &&\n typeof config.translationBudgetPerTenantPerDay === 'number'\n ) {\n const used = await countTodayTenantTranslations(\n this.options,\n args.tenantId,\n );\n if (used >= config.translationBudgetPerTenantPerDay) {\n return { skipped: 'budget' };\n }\n }\n\n // Build glossary from tenant overrides (no-op when no tenant context).\n let glossary = '';\n if (args.tenantId) {\n const tenantOverrides = await overrides.listTenantOverrides(\n args.tenantId,\n );\n glossary = buildTenantGlossary(tenantOverrides, {\n sourceLocale,\n targetLocale,\n max: 25,\n });\n }\n if (!glossary) {\n glossary = '(no organization glossary)';\n }\n\n // Pass the task's DB into resolvePrompt so any app/tenant-level prompt\n // overrides stored in `_smrt_prompt_overrides` are honored — that's the\n // whole reason we register the translation prompt with smrt-prompts in\n // the first place.\n const prompt = await resolvePrompt(TRANSLATION_PROMPT_KEY, {\n db: this.options.db,\n tenantId: args.tenantId ?? null,\n variables: {\n sourceLocale,\n targetLocale,\n template: args.sourceTemplate,\n glossary,\n placeholderExample: PLACEHOLDER_EXAMPLE,\n responseShapeExample: RESPONSE_SHAPE_EXAMPLE,\n },\n });\n\n // Single merged AI config: per-job model override (`args.model`) wins\n // over the prompt's `ai.model`, and the same merge feeds both `getAI()`\n // (client construction) and `ai.message()` (request options) so the\n // override actually takes effect end-to-end.\n const promptAi = (prompt.ai ?? {}) as Record<string, unknown>;\n const mergedAiConfig: Record<string, unknown> = { ...promptAi };\n if (args.model) mergedAiConfig.model = args.model;\n const ai = await getAI(mergedAiConfig as GetAIOptions);\n\n const message = await ai.message(prompt.text, {\n ...mergedAiConfig,\n responseFormat: { type: 'json_object' },\n });\n\n const translated = parseTranslationResponse(message);\n if (!translated) {\n throw new Error(\n `LanguageTranslationTask: AI returned no usable translation for \"${args.key}\" → ${targetLocale}`,\n );\n }\n\n if (containsObviousMarkupLeak(translated)) {\n throw new Error(\n `LanguageTranslationTask: refusing to persist suspicious translation for \"${args.key}\"`,\n );\n }\n\n // Persist the model that actually produced the translation, not just the\n // prompt's default — `args.model` overrides the prompt's `ai.model`.\n const aiModel =\n (args.model as string | undefined) ??\n (promptAi.model as string | undefined) ??\n null;\n const sourceHash =\n args.sourceHash || computeSourceHash(args.sourceTemplate);\n\n if (existing) {\n existing.template = translated;\n existing.auto_generated = true;\n existing.source_hash = sourceHash;\n existing.ai_model = aiModel;\n existing.reviewed_at = null;\n existing.reviewed_by = null;\n await existing.save();\n } else {\n // Use the collection's create() so the new row is initialized against\n // the same DB the task is running on. Constructing `new LanguageOverride`\n // directly leaves it un-initialized and `.save()` then trips on the\n // \"Database accessed before initialization\" guard.\n await overrides.create({\n key: args.key,\n locale: targetLocale,\n tenantId: null,\n template: translated,\n auto_generated: true,\n source_hash: sourceHash,\n ai_model: aiModel,\n reviewed_at: null,\n reviewed_by: null,\n });\n }\n\n invalidateLanguageCache(args.key, targetLocale, null, this.db);\n return { template: translated };\n }\n}\n\ninterface EnqueueTranslationOptions {\n key: string;\n targetLocale: string;\n sourceLocale?: string;\n tenantId?: string | null;\n db: SmrtClassOptions['db'];\n /** When true, skip the dedup check and force a fresh job. */\n force?: boolean;\n}\n\n/**\n * Enqueue a translation job for `(key, targetLocale)`, deduplicated against\n * any already-pending job for the same target. Returns the (possibly existing)\n * job's deterministic ID.\n */\nexport async function enqueueTranslationJob(\n options: EnqueueTranslationOptions,\n): Promise<{ id: string; status: 'enqueued' | 'duplicate' | 'skipped' }> {\n const targetLocale = normalizeLocale(options.targetLocale);\n const sourceLocale = normalizeLocale(options.sourceLocale ?? 'en');\n const dedupId = buildTranslationJobId(options.key, targetLocale);\n\n const definition = LanguageRegistry.get(options.key, sourceLocale);\n if (!definition) {\n return { id: dedupId, status: 'skipped' };\n }\n\n const config = getLanguagesConfig();\n if (\n Array.isArray(config.supportedLocales) &&\n config.supportedLocales.length > 0 &&\n !config.supportedLocales.map(normalizeLocale).includes(targetLocale)\n ) {\n return { id: dedupId, status: 'skipped' };\n }\n\n const jobs = await SmrtJobCollection.create({ db: options.db });\n\n if (!options.force) {\n const existing = await findPendingTranslationJob(\n jobs,\n options.key,\n targetLocale,\n );\n if (existing) {\n return { id: dedupId, status: 'duplicate' };\n }\n }\n\n const payload: TranslationJobPayload = {\n key: options.key,\n sourceLocale,\n sourceTemplate: definition.template,\n sourceHash: definition.sourceHash,\n targetLocale,\n tenantId: options.tenantId ?? null,\n };\n\n // Stamp tenantId on the SmrtJob row so the per-tenant daily budget query\n // (which filters by `tenantId`) actually counts this job. SmrtJob.save()\n // will fall back to `getTenantId()` from AsyncLocalStorage when undefined,\n // but we may be enqueueing from outside a tenant context (e.g. resolver\n // miss with an explicit tenantId option), so set it explicitly here.\n const job = await jobs.create({\n queue: 'languages',\n objectType: 'LanguageTranslationTask',\n objectId: null,\n method: 'execute',\n tenantId: options.tenantId ?? null,\n args: { ...payload, _dedupId: dedupId },\n runAt: new Date(),\n priority: 25,\n });\n await job.save();\n\n return { id: dedupId, status: 'enqueued' };\n}\n\n/**\n * In-memory match cap for the dedup scan. We pull the most recently-queued\n * language jobs and check them in JS because querying inside a JSON column\n * portably across SQLite/Postgres is fiddly. If the working queue is deeper\n * than this, the scan truncates — when that happens we log a warning instead\n * of silently letting duplicate jobs slip through, and the handler's\n * source-hash gate (`execute()`) still prevents redundant AI calls. Tune via\n * `packages.languages.dedupScanLimit` if your steady-state pending queue\n * regularly exceeds the default.\n */\nconst DEFAULT_DEDUP_SCAN_LIMIT = 500;\n\nasync function findPendingTranslationJob(\n jobs: SmrtJobCollection,\n key: string,\n targetLocale: string,\n): Promise<unknown | null> {\n const dedupId = buildTranslationJobId(key, targetLocale);\n const config = getLanguagesConfig();\n const scanLimit =\n typeof (config as { dedupScanLimit?: number }).dedupScanLimit === 'number'\n ? (config as { dedupScanLimit: number }).dedupScanLimit\n : DEFAULT_DEDUP_SCAN_LIMIT;\n\n const rows = await jobs.list({\n where: {\n objectType: 'LanguageTranslationTask',\n method: 'execute',\n status: ['pending', 'running'],\n queue: 'languages',\n },\n orderBy: 'createdAt DESC',\n limit: scanLimit,\n });\n\n for (const row of rows) {\n const args = (row as { args?: Record<string, unknown> }).args ?? {};\n if (args._dedupId === dedupId) return row;\n if (\n args.key === key &&\n typeof args.targetLocale === 'string' &&\n normalizeLocale(args.targetLocale as string) === targetLocale\n ) {\n return row;\n }\n }\n\n if (rows.length === scanLimit) {\n // Don't crash; the handler's source-hash gate is the durable safeguard.\n // But surface a warning so operators notice when the queue depth has\n // outgrown the in-memory match window.\n logger.warn(\n `[smrt-languages] translation-job dedup scan hit its limit (${scanLimit}); raise packages.languages.dedupScanLimit if duplicate jobs appear`,\n );\n }\n return null;\n}\n\nasync function countTodayTenantTranslations(\n options: SmrtClassOptions,\n tenantId: string,\n): Promise<number> {\n // SmrtCollection.list() encodes operators into the where-clause key (e.g.\n // `'createdAt >='`), not as `{ op, value }` objects — using the wrong shape\n // either throws or matches nothing, and a swallowed failure here silently\n // disables the budget. Let the call propagate so configuration mistakes\n // surface immediately rather than as a missed throttle in production.\n const jobs = await SmrtJobCollection.create({ db: options.db });\n const since = new Date();\n since.setUTCHours(0, 0, 0, 0);\n const rows = await jobs.list({\n where: {\n objectType: 'LanguageTranslationTask',\n method: 'execute',\n tenantId,\n 'createdAt >=': since.toISOString(),\n },\n });\n return rows.length;\n}\n\nfunction getLanguagesConfig(): LanguagesPackageConfig {\n return getPackageConfig<LanguagesPackageConfig>('languages', {\n defaultLocale: 'en',\n overrides: {},\n });\n}\n\nasync function isAutoTranslateDisabled(\n tenantId: string | null,\n options: SmrtClassOptions,\n): Promise<boolean> {\n try {\n const resolver = new FeatureResolver(options);\n const enabled = await resolver.isEnabled(AUTO_TRANSLATE_FEATURE_KEY, {\n tenantId: tenantId ?? undefined,\n });\n return enabled === false;\n } catch {\n // If features package can't resolve (no definition synced, etc.), default\n // to enabled — operators opt out by registering the flag.\n return false;\n }\n}\n\nfunction parseTranslationResponse(\n message: string | null | undefined,\n): string | null {\n if (!message) return null;\n\n // We requested `responseFormat: json_object` so the model is supposed to\n // return a JSON object with a `translation` string. If parsing succeeds but\n // the shape is wrong (e.g. `{\"text\":\"Hola\"}`), we MUST NOT fall through to\n // the bare-string path — that would persist the whole JSON blob as the\n // translation. Only the parse-failed branch may try to use the raw message\n // as a literal string (some providers occasionally return prose despite\n // the format hint).\n let parseFailed = false;\n let parsed: unknown;\n try {\n parsed = JSON.parse(message);\n } catch {\n parseFailed = true;\n }\n\n if (!parseFailed) {\n if (\n parsed &&\n typeof parsed === 'object' &&\n !Array.isArray(parsed) &&\n typeof (parsed as { translation?: unknown }).translation === 'string'\n ) {\n const cleaned = (parsed as { translation: string }).translation.trim();\n return cleaned.length > 0 ? cleaned : null;\n }\n if (typeof parsed === 'string') {\n const cleaned = parsed.trim();\n return cleaned.length > 0 ? cleaned : null;\n }\n return null;\n }\n\n const trimmed = String(message).trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\nfunction containsObviousMarkupLeak(value: string): boolean {\n // Cheap defense: refuse responses that look like raw HTML/system tags. We\n // can tighten this in v1.1 once we have telemetry on real failure modes.\n return /<\\/?(script|html|body|iframe|system)/i.test(value);\n}\n"],"names":[],"mappings":"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"}
package/dist/jobs.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"jobs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
package/dist/types.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}