scribe-cms 0.0.14 → 0.0.16

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.
@@ -2547,6 +2547,13 @@ function defaultLocalizationPrompt(localeName, locale) {
2547
2547
  "Write as if a native speaker authored it for the target market."
2548
2548
  ].join(" ");
2549
2549
  }
2550
+ function buildLocalizationPrompt(promptOverride, localeName, locale) {
2551
+ const localeDirective = defaultLocalizationPrompt(localeName, locale);
2552
+ if (!promptOverride) return localeDirective;
2553
+ return `${promptOverride}
2554
+
2555
+ ${localeDirective}`;
2556
+ }
2550
2557
  var TASK_FRAMING = "You are localizing the content below into a target language specified at the end of this prompt.";
2551
2558
  function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
2552
2559
  if (!hasFrontmatter) {
@@ -2554,9 +2561,20 @@ function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
2554
2561
  }
2555
2562
  return slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).";
2556
2563
  }
2564
+ function buildRetryContextSection(previousError) {
2565
+ return [
2566
+ "",
2567
+ "## Previous attempt",
2568
+ `A previous attempt at this translation was rejected with the following validation errors: ${previousError}. Produce a corrected translation that fixes these issues while re-checking every schema constraint (required fields, array minimums, maximum lengths).`
2569
+ ];
2570
+ }
2557
2571
  function buildPageTranslationPrompt(input) {
2558
2572
  const localeName = LOCALE_NAMES[input.targetLocale] ?? input.targetLocale;
2559
- const localizationPrompt = input.resolved.promptOverride ?? defaultLocalizationPrompt(localeName, input.targetLocale);
2573
+ const localizationPrompt = buildLocalizationPrompt(
2574
+ input.resolved.promptOverride,
2575
+ localeName,
2576
+ input.targetLocale
2577
+ );
2560
2578
  const prefix = [
2561
2579
  TASK_FRAMING,
2562
2580
  "",
@@ -2584,7 +2602,10 @@ function buildPageTranslationPrompt(input) {
2584
2602
  buildOutputFormatLine(
2585
2603
  Object.keys(input.translatableFrontmatter).length > 0,
2586
2604
  input.slugStrategy
2587
- )
2605
+ ),
2606
+ // Retry context is locale-specific suffix material: it MUST stay after the
2607
+ // EN body so the cacheable prefix is byte-identical with and without it.
2608
+ ...input.previousError ? buildRetryContextSection(input.previousError) : []
2588
2609
  ];
2589
2610
  return [...prefix, ...suffix].join("\n");
2590
2611
  }
@@ -2804,7 +2825,8 @@ function prepareTranslation(config, item, options, startedAt) {
2804
2825
  contextLabel: resolveContextLabel(enDoc, item.enSlug),
2805
2826
  translatableFrontmatter: payload.frontmatter,
2806
2827
  enBody: payload.body,
2807
- slugStrategy: type.slugStrategy
2828
+ slugStrategy: type.slugStrategy,
2829
+ previousError: item.previousError
2808
2830
  });
2809
2831
  const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
2810
2832
  return {
@@ -3216,6 +3238,82 @@ async function runPool(items, concurrency, worker) {
3216
3238
  function labelForItem(item) {
3217
3239
  return `${item.contentType}/${item.enSlug}@${item.locale}`;
3218
3240
  }
3241
+ function buildRetryWorklist(failed, originalByKey) {
3242
+ return failed.map((result) => {
3243
+ const key = translationItemKey(result);
3244
+ const original = originalByKey.get(key);
3245
+ return {
3246
+ contentType: result.contentType,
3247
+ enSlug: result.enSlug,
3248
+ locale: result.locale,
3249
+ reason: original?.reason ?? "missing",
3250
+ currentEnHash: original?.currentEnHash ?? "",
3251
+ storedEnHash: original?.storedEnHash,
3252
+ previousError: result.error ?? "Translation failed"
3253
+ };
3254
+ });
3255
+ }
3256
+ async function runRetryRound(config, retryItems, options) {
3257
+ const startedAt = Date.now();
3258
+ const translateOne = options.translateOne ?? ((item) => translatePage(config, item, { model: options.model, force: true }));
3259
+ if (options.mode === "direct") {
3260
+ const active = /* @__PURE__ */ new Set();
3261
+ await runPool(retryItems, options.concurrency, async (item) => {
3262
+ const label = labelForItem(item);
3263
+ active.add(label);
3264
+ options.onProgress?.({ type: "item-start", item, active: [...active] });
3265
+ const result = await translateOne(item);
3266
+ active.delete(label);
3267
+ options.onResult(result);
3268
+ });
3269
+ return;
3270
+ }
3271
+ const entries = [];
3272
+ for (const item of retryItems) {
3273
+ const itemStartedAt = Date.now();
3274
+ try {
3275
+ const outcome = prepareTranslation(config, item, { model: options.model, force: true }, itemStartedAt);
3276
+ if (outcome.status === "done") {
3277
+ options.onResult(outcome.result);
3278
+ } else {
3279
+ entries.push({
3280
+ apiModel: resolveGeminiModelId(displayModelFor(outcome.prepared)),
3281
+ prompt: outcome.prepared.prompt,
3282
+ prepared: outcome.prepared
3283
+ });
3284
+ }
3285
+ } catch (error) {
3286
+ options.onResult(failedResultForItem(item, error, itemStartedAt));
3287
+ }
3288
+ }
3289
+ const plans = planBatchJobs(entries);
3290
+ const submitted = await Promise.all(
3291
+ plans.map(async (plan, planIndex) => {
3292
+ try {
3293
+ return await submitBatchJobPlan(config, {
3294
+ plan,
3295
+ displayModel: normalizeGeminiDisplayName(plan.apiModel),
3296
+ jobIndex: planIndex,
3297
+ jobCount: plans.length,
3298
+ onProgress: options.onProgress
3299
+ });
3300
+ } catch (error) {
3301
+ for (const entry of plan.entries) {
3302
+ options.onResult(failedResultForItem(entry.prepared.item, error, startedAt));
3303
+ }
3304
+ return void 0;
3305
+ }
3306
+ })
3307
+ );
3308
+ const jobsToPoll = submitted.filter((row) => row !== void 0);
3309
+ if (jobsToPoll.length > 0) {
3310
+ await pollAndIngestBatchJobs(config, jobsToPoll, {
3311
+ jobCount: jobsToPoll.length,
3312
+ onProgress: options.onProgress,
3313
+ onResult: options.onResult
3314
+ });
3315
+ }
3316
+ }
3219
3317
  async function translatePage(config, item, options = {}) {
3220
3318
  const startedAt = Date.now();
3221
3319
  let outcome;
@@ -3388,9 +3486,40 @@ async function translateWorklist(config, items, options = {}) {
3388
3486
  });
3389
3487
  }
3390
3488
  }
3391
- const results = collector.assemble();
3489
+ const mainResults = collector.assemble();
3490
+ const failed = mainResults.filter((result) => result.failed);
3491
+ if (failed.length === 0) {
3492
+ const totals2 = summarizeResults(mainResults, Date.now() - startedAt);
3493
+ options.onProgress?.({ type: "done", results: mainResults, totals: totals2 });
3494
+ return mainResults;
3495
+ }
3496
+ options.onProgress?.({ type: "retry-start", failed });
3497
+ const originalByKey = new Map(
3498
+ items.map((item) => [translationItemKey(item), item])
3499
+ );
3500
+ const retryItems = buildRetryWorklist(failed, originalByKey);
3501
+ const retryByKey = /* @__PURE__ */ new Map();
3502
+ await runRetryRound(config, retryItems, {
3503
+ model: options.model,
3504
+ force: options.force,
3505
+ concurrency,
3506
+ mode,
3507
+ onProgress: options.onProgress,
3508
+ onResult: (result) => {
3509
+ retryByKey.set(translationItemKey(result), result);
3510
+ options.onProgress?.({ type: "item-done", result });
3511
+ }
3512
+ });
3513
+ const results = mainResults.map((result) => {
3514
+ if (!result.failed) return result;
3515
+ return retryByKey.get(translationItemKey(result)) ?? result;
3516
+ });
3517
+ const retriedTranslated = failed.filter((original) => {
3518
+ const retried = retryByKey.get(translationItemKey(original));
3519
+ return retried !== void 0 && !retried.failed && !retried.skipped;
3520
+ }).length;
3392
3521
  const totals = summarizeResults(results, Date.now() - startedAt);
3393
- options.onProgress?.({ type: "done", results, totals });
3522
+ options.onProgress?.({ type: "done", results, totals, retriedTranslated });
3394
3523
  return results;
3395
3524
  }
3396
3525
  function countPendingItems(config, jobs) {
@@ -4212,10 +4341,11 @@ function detailForResult(result) {
4212
4341
  if (result.error) parts.push(red(result.error));
4213
4342
  return parts.length > 0 ? dim(parts.join(" \xB7 ")) : "";
4214
4343
  }
4215
- function formatSummaryLine(totals, dryRun) {
4344
+ function formatSummaryLine(totals, dryRun, retriedTranslated = 0) {
4216
4345
  const action = dryRun ? "Would translate" : "Translated";
4346
+ const base = totals.translated - retriedTranslated;
4217
4347
  const parts = [
4218
- `${action} ${totals.translated}`,
4348
+ retriedTranslated > 0 ? `${action} ${base} +${retriedTranslated} on retry` : `${action} ${totals.translated}`,
4219
4349
  `skipped ${totals.skipped}`
4220
4350
  ];
4221
4351
  if (totals.failed > 0) parts.push(red(`failed ${totals.failed}`));
@@ -4264,8 +4394,18 @@ function createTranslateProgressReporter(options = {}) {
4264
4394
  }
4265
4395
  return;
4266
4396
  }
4397
+ if (event.type === "retry-start") {
4398
+ for (const result of event.failed) {
4399
+ console.log(`${labelForResult(result)}: ${result.error ?? "failed"}`);
4400
+ }
4401
+ console.log(`Retrying ${event.failed.length} failed items with validation context...`);
4402
+ return;
4403
+ }
4267
4404
  if (event.type === "done") {
4268
- console.log(formatSummaryLine(event.totals, dryRun));
4405
+ console.log(formatSummaryLine(event.totals, dryRun, event.retriedTranslated));
4406
+ for (const result of event.results.filter((entry) => entry.failed)) {
4407
+ console.error(`${labelForResult(result)}: ${result.error ?? "failed"}`);
4408
+ }
4269
4409
  }
4270
4410
  },
4271
4411
  finish() {
@@ -4432,12 +4572,25 @@ function createTranslateProgressReporter(options = {}) {
4432
4572
  render();
4433
4573
  break;
4434
4574
  }
4575
+ case "retry-start": {
4576
+ total += event.failed.length;
4577
+ printPersistent([
4578
+ ...event.failed.map(
4579
+ (result) => red(`${labelForResult(result)}: ${result.error ?? "failed"}`)
4580
+ ),
4581
+ cyan(`Retrying ${event.failed.length} failed items with validation context...`)
4582
+ ]);
4583
+ render();
4584
+ break;
4585
+ }
4435
4586
  case "done":
4436
4587
  showCursor();
4437
4588
  if (renderedLines > 0) {
4438
4589
  process.stdout.write(`\x1B[${renderedLines}A`);
4439
4590
  }
4440
- process.stdout.write("\x1B[2K" + green("Done") + " \xB7 " + formatSummaryLine(event.totals, dryRun) + "\n");
4591
+ process.stdout.write(
4592
+ "\x1B[2K" + green("Done") + " \xB7 " + formatSummaryLine(event.totals, dryRun, event.retriedTranslated) + "\n"
4593
+ );
4441
4594
  renderedLines = 0;
4442
4595
  for (const result of event.results.filter((entry) => entry.failed)) {
4443
4596
  process.stdout.write("\x1B[2K" + red(`${labelForResult(result)}: ${result.error ?? "failed"}`) + "\n");