scribe-cms 0.0.14 → 0.0.15

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.
@@ -2554,6 +2554,13 @@ function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
2554
2554
  }
2555
2555
  return slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).";
2556
2556
  }
2557
+ function buildRetryContextSection(previousError) {
2558
+ return [
2559
+ "",
2560
+ "## Previous attempt",
2561
+ `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).`
2562
+ ];
2563
+ }
2557
2564
  function buildPageTranslationPrompt(input) {
2558
2565
  const localeName = LOCALE_NAMES[input.targetLocale] ?? input.targetLocale;
2559
2566
  const localizationPrompt = input.resolved.promptOverride ?? defaultLocalizationPrompt(localeName, input.targetLocale);
@@ -2584,7 +2591,10 @@ function buildPageTranslationPrompt(input) {
2584
2591
  buildOutputFormatLine(
2585
2592
  Object.keys(input.translatableFrontmatter).length > 0,
2586
2593
  input.slugStrategy
2587
- )
2594
+ ),
2595
+ // Retry context is locale-specific suffix material: it MUST stay after the
2596
+ // EN body so the cacheable prefix is byte-identical with and without it.
2597
+ ...input.previousError ? buildRetryContextSection(input.previousError) : []
2588
2598
  ];
2589
2599
  return [...prefix, ...suffix].join("\n");
2590
2600
  }
@@ -2804,7 +2814,8 @@ function prepareTranslation(config, item, options, startedAt) {
2804
2814
  contextLabel: resolveContextLabel(enDoc, item.enSlug),
2805
2815
  translatableFrontmatter: payload.frontmatter,
2806
2816
  enBody: payload.body,
2807
- slugStrategy: type.slugStrategy
2817
+ slugStrategy: type.slugStrategy,
2818
+ previousError: item.previousError
2808
2819
  });
2809
2820
  const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
2810
2821
  return {
@@ -3216,6 +3227,82 @@ async function runPool(items, concurrency, worker) {
3216
3227
  function labelForItem(item) {
3217
3228
  return `${item.contentType}/${item.enSlug}@${item.locale}`;
3218
3229
  }
3230
+ function buildRetryWorklist(failed, originalByKey) {
3231
+ return failed.map((result) => {
3232
+ const key = translationItemKey(result);
3233
+ const original = originalByKey.get(key);
3234
+ return {
3235
+ contentType: result.contentType,
3236
+ enSlug: result.enSlug,
3237
+ locale: result.locale,
3238
+ reason: original?.reason ?? "missing",
3239
+ currentEnHash: original?.currentEnHash ?? "",
3240
+ storedEnHash: original?.storedEnHash,
3241
+ previousError: result.error ?? "Translation failed"
3242
+ };
3243
+ });
3244
+ }
3245
+ async function runRetryRound(config, retryItems, options) {
3246
+ const startedAt = Date.now();
3247
+ const translateOne = options.translateOne ?? ((item) => translatePage(config, item, { model: options.model, force: true }));
3248
+ if (options.mode === "direct") {
3249
+ const active = /* @__PURE__ */ new Set();
3250
+ await runPool(retryItems, options.concurrency, async (item) => {
3251
+ const label = labelForItem(item);
3252
+ active.add(label);
3253
+ options.onProgress?.({ type: "item-start", item, active: [...active] });
3254
+ const result = await translateOne(item);
3255
+ active.delete(label);
3256
+ options.onResult(result);
3257
+ });
3258
+ return;
3259
+ }
3260
+ const entries = [];
3261
+ for (const item of retryItems) {
3262
+ const itemStartedAt = Date.now();
3263
+ try {
3264
+ const outcome = prepareTranslation(config, item, { model: options.model, force: true }, itemStartedAt);
3265
+ if (outcome.status === "done") {
3266
+ options.onResult(outcome.result);
3267
+ } else {
3268
+ entries.push({
3269
+ apiModel: resolveGeminiModelId(displayModelFor(outcome.prepared)),
3270
+ prompt: outcome.prepared.prompt,
3271
+ prepared: outcome.prepared
3272
+ });
3273
+ }
3274
+ } catch (error) {
3275
+ options.onResult(failedResultForItem(item, error, itemStartedAt));
3276
+ }
3277
+ }
3278
+ const plans = planBatchJobs(entries);
3279
+ const submitted = await Promise.all(
3280
+ plans.map(async (plan, planIndex) => {
3281
+ try {
3282
+ return await submitBatchJobPlan(config, {
3283
+ plan,
3284
+ displayModel: normalizeGeminiDisplayName(plan.apiModel),
3285
+ jobIndex: planIndex,
3286
+ jobCount: plans.length,
3287
+ onProgress: options.onProgress
3288
+ });
3289
+ } catch (error) {
3290
+ for (const entry of plan.entries) {
3291
+ options.onResult(failedResultForItem(entry.prepared.item, error, startedAt));
3292
+ }
3293
+ return void 0;
3294
+ }
3295
+ })
3296
+ );
3297
+ const jobsToPoll = submitted.filter((row) => row !== void 0);
3298
+ if (jobsToPoll.length > 0) {
3299
+ await pollAndIngestBatchJobs(config, jobsToPoll, {
3300
+ jobCount: jobsToPoll.length,
3301
+ onProgress: options.onProgress,
3302
+ onResult: options.onResult
3303
+ });
3304
+ }
3305
+ }
3219
3306
  async function translatePage(config, item, options = {}) {
3220
3307
  const startedAt = Date.now();
3221
3308
  let outcome;
@@ -3388,9 +3475,40 @@ async function translateWorklist(config, items, options = {}) {
3388
3475
  });
3389
3476
  }
3390
3477
  }
3391
- const results = collector.assemble();
3478
+ const mainResults = collector.assemble();
3479
+ const failed = mainResults.filter((result) => result.failed);
3480
+ if (failed.length === 0) {
3481
+ const totals2 = summarizeResults(mainResults, Date.now() - startedAt);
3482
+ options.onProgress?.({ type: "done", results: mainResults, totals: totals2 });
3483
+ return mainResults;
3484
+ }
3485
+ options.onProgress?.({ type: "retry-start", failed });
3486
+ const originalByKey = new Map(
3487
+ items.map((item) => [translationItemKey(item), item])
3488
+ );
3489
+ const retryItems = buildRetryWorklist(failed, originalByKey);
3490
+ const retryByKey = /* @__PURE__ */ new Map();
3491
+ await runRetryRound(config, retryItems, {
3492
+ model: options.model,
3493
+ force: options.force,
3494
+ concurrency,
3495
+ mode,
3496
+ onProgress: options.onProgress,
3497
+ onResult: (result) => {
3498
+ retryByKey.set(translationItemKey(result), result);
3499
+ options.onProgress?.({ type: "item-done", result });
3500
+ }
3501
+ });
3502
+ const results = mainResults.map((result) => {
3503
+ if (!result.failed) return result;
3504
+ return retryByKey.get(translationItemKey(result)) ?? result;
3505
+ });
3506
+ const retriedTranslated = failed.filter((original) => {
3507
+ const retried = retryByKey.get(translationItemKey(original));
3508
+ return retried !== void 0 && !retried.failed && !retried.skipped;
3509
+ }).length;
3392
3510
  const totals = summarizeResults(results, Date.now() - startedAt);
3393
- options.onProgress?.({ type: "done", results, totals });
3511
+ options.onProgress?.({ type: "done", results, totals, retriedTranslated });
3394
3512
  return results;
3395
3513
  }
3396
3514
  function countPendingItems(config, jobs) {
@@ -4212,10 +4330,11 @@ function detailForResult(result) {
4212
4330
  if (result.error) parts.push(red(result.error));
4213
4331
  return parts.length > 0 ? dim(parts.join(" \xB7 ")) : "";
4214
4332
  }
4215
- function formatSummaryLine(totals, dryRun) {
4333
+ function formatSummaryLine(totals, dryRun, retriedTranslated = 0) {
4216
4334
  const action = dryRun ? "Would translate" : "Translated";
4335
+ const base = totals.translated - retriedTranslated;
4217
4336
  const parts = [
4218
- `${action} ${totals.translated}`,
4337
+ retriedTranslated > 0 ? `${action} ${base} +${retriedTranslated} on retry` : `${action} ${totals.translated}`,
4219
4338
  `skipped ${totals.skipped}`
4220
4339
  ];
4221
4340
  if (totals.failed > 0) parts.push(red(`failed ${totals.failed}`));
@@ -4264,8 +4383,18 @@ function createTranslateProgressReporter(options = {}) {
4264
4383
  }
4265
4384
  return;
4266
4385
  }
4386
+ if (event.type === "retry-start") {
4387
+ for (const result of event.failed) {
4388
+ console.log(`${labelForResult(result)}: ${result.error ?? "failed"}`);
4389
+ }
4390
+ console.log(`Retrying ${event.failed.length} failed items with validation context...`);
4391
+ return;
4392
+ }
4267
4393
  if (event.type === "done") {
4268
- console.log(formatSummaryLine(event.totals, dryRun));
4394
+ console.log(formatSummaryLine(event.totals, dryRun, event.retriedTranslated));
4395
+ for (const result of event.results.filter((entry) => entry.failed)) {
4396
+ console.error(`${labelForResult(result)}: ${result.error ?? "failed"}`);
4397
+ }
4269
4398
  }
4270
4399
  },
4271
4400
  finish() {
@@ -4432,12 +4561,25 @@ function createTranslateProgressReporter(options = {}) {
4432
4561
  render();
4433
4562
  break;
4434
4563
  }
4564
+ case "retry-start": {
4565
+ total += event.failed.length;
4566
+ printPersistent([
4567
+ ...event.failed.map(
4568
+ (result) => red(`${labelForResult(result)}: ${result.error ?? "failed"}`)
4569
+ ),
4570
+ cyan(`Retrying ${event.failed.length} failed items with validation context...`)
4571
+ ]);
4572
+ render();
4573
+ break;
4574
+ }
4435
4575
  case "done":
4436
4576
  showCursor();
4437
4577
  if (renderedLines > 0) {
4438
4578
  process.stdout.write(`\x1B[${renderedLines}A`);
4439
4579
  }
4440
- process.stdout.write("\x1B[2K" + green("Done") + " \xB7 " + formatSummaryLine(event.totals, dryRun) + "\n");
4580
+ process.stdout.write(
4581
+ "\x1B[2K" + green("Done") + " \xB7 " + formatSummaryLine(event.totals, dryRun, event.retriedTranslated) + "\n"
4582
+ );
4441
4583
  renderedLines = 0;
4442
4584
  for (const result of event.results.filter((entry) => entry.failed)) {
4443
4585
  process.stdout.write("\x1B[2K" + red(`${labelForResult(result)}: ${result.error ?? "failed"}`) + "\n");