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.
package/dist/cli/index.js CHANGED
@@ -2530,6 +2530,13 @@ function defaultLocalizationPrompt(localeName, locale) {
2530
2530
  "Write as if a native speaker authored it for the target market."
2531
2531
  ].join(" ");
2532
2532
  }
2533
+ function buildLocalizationPrompt(promptOverride, localeName, locale) {
2534
+ const localeDirective = defaultLocalizationPrompt(localeName, locale);
2535
+ if (!promptOverride) return localeDirective;
2536
+ return `${promptOverride}
2537
+
2538
+ ${localeDirective}`;
2539
+ }
2533
2540
  var TASK_FRAMING = "You are localizing the content below into a target language specified at the end of this prompt.";
2534
2541
  function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
2535
2542
  if (!hasFrontmatter) {
@@ -2537,9 +2544,20 @@ function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
2537
2544
  }
2538
2545
  return slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).";
2539
2546
  }
2547
+ function buildRetryContextSection(previousError) {
2548
+ return [
2549
+ "",
2550
+ "## Previous attempt",
2551
+ `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).`
2552
+ ];
2553
+ }
2540
2554
  function buildPageTranslationPrompt(input) {
2541
2555
  const localeName = LOCALE_NAMES[input.targetLocale] ?? input.targetLocale;
2542
- const localizationPrompt = input.resolved.promptOverride ?? defaultLocalizationPrompt(localeName, input.targetLocale);
2556
+ const localizationPrompt = buildLocalizationPrompt(
2557
+ input.resolved.promptOverride,
2558
+ localeName,
2559
+ input.targetLocale
2560
+ );
2543
2561
  const prefix = [
2544
2562
  TASK_FRAMING,
2545
2563
  "",
@@ -2567,7 +2585,10 @@ function buildPageTranslationPrompt(input) {
2567
2585
  buildOutputFormatLine(
2568
2586
  Object.keys(input.translatableFrontmatter).length > 0,
2569
2587
  input.slugStrategy
2570
- )
2588
+ ),
2589
+ // Retry context is locale-specific suffix material: it MUST stay after the
2590
+ // EN body so the cacheable prefix is byte-identical with and without it.
2591
+ ...input.previousError ? buildRetryContextSection(input.previousError) : []
2571
2592
  ];
2572
2593
  return [...prefix, ...suffix].join("\n");
2573
2594
  }
@@ -2787,7 +2808,8 @@ function prepareTranslation(config, item, options, startedAt) {
2787
2808
  contextLabel: resolveContextLabel(enDoc, item.enSlug),
2788
2809
  translatableFrontmatter: payload.frontmatter,
2789
2810
  enBody: payload.body,
2790
- slugStrategy: type.slugStrategy
2811
+ slugStrategy: type.slugStrategy,
2812
+ previousError: item.previousError
2791
2813
  });
2792
2814
  const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
2793
2815
  return {
@@ -3199,6 +3221,82 @@ async function runPool(items, concurrency, worker) {
3199
3221
  function labelForItem(item) {
3200
3222
  return `${item.contentType}/${item.enSlug}@${item.locale}`;
3201
3223
  }
3224
+ function buildRetryWorklist(failed, originalByKey) {
3225
+ return failed.map((result) => {
3226
+ const key = translationItemKey(result);
3227
+ const original = originalByKey.get(key);
3228
+ return {
3229
+ contentType: result.contentType,
3230
+ enSlug: result.enSlug,
3231
+ locale: result.locale,
3232
+ reason: original?.reason ?? "missing",
3233
+ currentEnHash: original?.currentEnHash ?? "",
3234
+ storedEnHash: original?.storedEnHash,
3235
+ previousError: result.error ?? "Translation failed"
3236
+ };
3237
+ });
3238
+ }
3239
+ async function runRetryRound(config, retryItems, options) {
3240
+ const startedAt = Date.now();
3241
+ const translateOne = options.translateOne ?? ((item) => translatePage(config, item, { model: options.model, force: true }));
3242
+ if (options.mode === "direct") {
3243
+ const active = /* @__PURE__ */ new Set();
3244
+ await runPool(retryItems, options.concurrency, async (item) => {
3245
+ const label = labelForItem(item);
3246
+ active.add(label);
3247
+ options.onProgress?.({ type: "item-start", item, active: [...active] });
3248
+ const result = await translateOne(item);
3249
+ active.delete(label);
3250
+ options.onResult(result);
3251
+ });
3252
+ return;
3253
+ }
3254
+ const entries = [];
3255
+ for (const item of retryItems) {
3256
+ const itemStartedAt = Date.now();
3257
+ try {
3258
+ const outcome = prepareTranslation(config, item, { model: options.model, force: true }, itemStartedAt);
3259
+ if (outcome.status === "done") {
3260
+ options.onResult(outcome.result);
3261
+ } else {
3262
+ entries.push({
3263
+ apiModel: resolveGeminiModelId(displayModelFor(outcome.prepared)),
3264
+ prompt: outcome.prepared.prompt,
3265
+ prepared: outcome.prepared
3266
+ });
3267
+ }
3268
+ } catch (error) {
3269
+ options.onResult(failedResultForItem(item, error, itemStartedAt));
3270
+ }
3271
+ }
3272
+ const plans = planBatchJobs(entries);
3273
+ const submitted = await Promise.all(
3274
+ plans.map(async (plan, planIndex) => {
3275
+ try {
3276
+ return await submitBatchJobPlan(config, {
3277
+ plan,
3278
+ displayModel: normalizeGeminiDisplayName(plan.apiModel),
3279
+ jobIndex: planIndex,
3280
+ jobCount: plans.length,
3281
+ onProgress: options.onProgress
3282
+ });
3283
+ } catch (error) {
3284
+ for (const entry of plan.entries) {
3285
+ options.onResult(failedResultForItem(entry.prepared.item, error, startedAt));
3286
+ }
3287
+ return void 0;
3288
+ }
3289
+ })
3290
+ );
3291
+ const jobsToPoll = submitted.filter((row) => row !== void 0);
3292
+ if (jobsToPoll.length > 0) {
3293
+ await pollAndIngestBatchJobs(config, jobsToPoll, {
3294
+ jobCount: jobsToPoll.length,
3295
+ onProgress: options.onProgress,
3296
+ onResult: options.onResult
3297
+ });
3298
+ }
3299
+ }
3202
3300
  async function translatePage(config, item, options = {}) {
3203
3301
  const startedAt = Date.now();
3204
3302
  let outcome;
@@ -3371,9 +3469,40 @@ async function translateWorklist(config, items, options = {}) {
3371
3469
  });
3372
3470
  }
3373
3471
  }
3374
- const results = collector.assemble();
3472
+ const mainResults = collector.assemble();
3473
+ const failed = mainResults.filter((result) => result.failed);
3474
+ if (failed.length === 0) {
3475
+ const totals2 = summarizeResults(mainResults, Date.now() - startedAt);
3476
+ options.onProgress?.({ type: "done", results: mainResults, totals: totals2 });
3477
+ return mainResults;
3478
+ }
3479
+ options.onProgress?.({ type: "retry-start", failed });
3480
+ const originalByKey = new Map(
3481
+ items.map((item) => [translationItemKey(item), item])
3482
+ );
3483
+ const retryItems = buildRetryWorklist(failed, originalByKey);
3484
+ const retryByKey = /* @__PURE__ */ new Map();
3485
+ await runRetryRound(config, retryItems, {
3486
+ model: options.model,
3487
+ force: options.force,
3488
+ concurrency,
3489
+ mode,
3490
+ onProgress: options.onProgress,
3491
+ onResult: (result) => {
3492
+ retryByKey.set(translationItemKey(result), result);
3493
+ options.onProgress?.({ type: "item-done", result });
3494
+ }
3495
+ });
3496
+ const results = mainResults.map((result) => {
3497
+ if (!result.failed) return result;
3498
+ return retryByKey.get(translationItemKey(result)) ?? result;
3499
+ });
3500
+ const retriedTranslated = failed.filter((original) => {
3501
+ const retried = retryByKey.get(translationItemKey(original));
3502
+ return retried !== void 0 && !retried.failed && !retried.skipped;
3503
+ }).length;
3375
3504
  const totals = summarizeResults(results, Date.now() - startedAt);
3376
- options.onProgress?.({ type: "done", results, totals });
3505
+ options.onProgress?.({ type: "done", results, totals, retriedTranslated });
3377
3506
  return results;
3378
3507
  }
3379
3508
  function countPendingItems(config, jobs) {
@@ -4195,10 +4324,11 @@ function detailForResult(result) {
4195
4324
  if (result.error) parts.push(red(result.error));
4196
4325
  return parts.length > 0 ? dim(parts.join(" \xB7 ")) : "";
4197
4326
  }
4198
- function formatSummaryLine(totals, dryRun) {
4327
+ function formatSummaryLine(totals, dryRun, retriedTranslated = 0) {
4199
4328
  const action = dryRun ? "Would translate" : "Translated";
4329
+ const base = totals.translated - retriedTranslated;
4200
4330
  const parts = [
4201
- `${action} ${totals.translated}`,
4331
+ retriedTranslated > 0 ? `${action} ${base} +${retriedTranslated} on retry` : `${action} ${totals.translated}`,
4202
4332
  `skipped ${totals.skipped}`
4203
4333
  ];
4204
4334
  if (totals.failed > 0) parts.push(red(`failed ${totals.failed}`));
@@ -4247,8 +4377,18 @@ function createTranslateProgressReporter(options = {}) {
4247
4377
  }
4248
4378
  return;
4249
4379
  }
4380
+ if (event.type === "retry-start") {
4381
+ for (const result of event.failed) {
4382
+ console.log(`${labelForResult(result)}: ${result.error ?? "failed"}`);
4383
+ }
4384
+ console.log(`Retrying ${event.failed.length} failed items with validation context...`);
4385
+ return;
4386
+ }
4250
4387
  if (event.type === "done") {
4251
- console.log(formatSummaryLine(event.totals, dryRun));
4388
+ console.log(formatSummaryLine(event.totals, dryRun, event.retriedTranslated));
4389
+ for (const result of event.results.filter((entry) => entry.failed)) {
4390
+ console.error(`${labelForResult(result)}: ${result.error ?? "failed"}`);
4391
+ }
4252
4392
  }
4253
4393
  },
4254
4394
  finish() {
@@ -4415,12 +4555,25 @@ function createTranslateProgressReporter(options = {}) {
4415
4555
  render();
4416
4556
  break;
4417
4557
  }
4558
+ case "retry-start": {
4559
+ total += event.failed.length;
4560
+ printPersistent([
4561
+ ...event.failed.map(
4562
+ (result) => red(`${labelForResult(result)}: ${result.error ?? "failed"}`)
4563
+ ),
4564
+ cyan(`Retrying ${event.failed.length} failed items with validation context...`)
4565
+ ]);
4566
+ render();
4567
+ break;
4568
+ }
4418
4569
  case "done":
4419
4570
  showCursor();
4420
4571
  if (renderedLines > 0) {
4421
4572
  process.stdout.write(`\x1B[${renderedLines}A`);
4422
4573
  }
4423
- process.stdout.write("\x1B[2K" + green("Done") + " \xB7 " + formatSummaryLine(event.totals, dryRun) + "\n");
4574
+ process.stdout.write(
4575
+ "\x1B[2K" + green("Done") + " \xB7 " + formatSummaryLine(event.totals, dryRun, event.retriedTranslated) + "\n"
4576
+ );
4424
4577
  renderedLines = 0;
4425
4578
  for (const result of event.results.filter((entry) => entry.failed)) {
4426
4579
  process.stdout.write("\x1B[2K" + red(`${labelForResult(result)}: ${result.error ?? "failed"}`) + "\n");