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.
package/dist/cli/index.js CHANGED
@@ -2537,6 +2537,13 @@ function buildOutputFormatLine(hasFrontmatter, slugStrategy) {
2537
2537
  }
2538
2538
  return slugStrategy === "localized" ? "`frontmatter` (object with translated frontmatter fields), `body` (string, full MDX body), `slug` (string)." : "`frontmatter` (object), `body` (string).";
2539
2539
  }
2540
+ function buildRetryContextSection(previousError) {
2541
+ return [
2542
+ "",
2543
+ "## Previous attempt",
2544
+ `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).`
2545
+ ];
2546
+ }
2540
2547
  function buildPageTranslationPrompt(input) {
2541
2548
  const localeName = LOCALE_NAMES[input.targetLocale] ?? input.targetLocale;
2542
2549
  const localizationPrompt = input.resolved.promptOverride ?? defaultLocalizationPrompt(localeName, input.targetLocale);
@@ -2567,7 +2574,10 @@ function buildPageTranslationPrompt(input) {
2567
2574
  buildOutputFormatLine(
2568
2575
  Object.keys(input.translatableFrontmatter).length > 0,
2569
2576
  input.slugStrategy
2570
- )
2577
+ ),
2578
+ // Retry context is locale-specific suffix material: it MUST stay after the
2579
+ // EN body so the cacheable prefix is byte-identical with and without it.
2580
+ ...input.previousError ? buildRetryContextSection(input.previousError) : []
2571
2581
  ];
2572
2582
  return [...prefix, ...suffix].join("\n");
2573
2583
  }
@@ -2787,7 +2797,8 @@ function prepareTranslation(config, item, options, startedAt) {
2787
2797
  contextLabel: resolveContextLabel(enDoc, item.enSlug),
2788
2798
  translatableFrontmatter: payload.frontmatter,
2789
2799
  enBody: payload.body,
2790
- slugStrategy: type.slugStrategy
2800
+ slugStrategy: type.slugStrategy,
2801
+ previousError: item.previousError
2791
2802
  });
2792
2803
  const responseSchema = buildGeminiResponseSchema(type.schema, type.slugStrategy);
2793
2804
  return {
@@ -3199,6 +3210,82 @@ async function runPool(items, concurrency, worker) {
3199
3210
  function labelForItem(item) {
3200
3211
  return `${item.contentType}/${item.enSlug}@${item.locale}`;
3201
3212
  }
3213
+ function buildRetryWorklist(failed, originalByKey) {
3214
+ return failed.map((result) => {
3215
+ const key = translationItemKey(result);
3216
+ const original = originalByKey.get(key);
3217
+ return {
3218
+ contentType: result.contentType,
3219
+ enSlug: result.enSlug,
3220
+ locale: result.locale,
3221
+ reason: original?.reason ?? "missing",
3222
+ currentEnHash: original?.currentEnHash ?? "",
3223
+ storedEnHash: original?.storedEnHash,
3224
+ previousError: result.error ?? "Translation failed"
3225
+ };
3226
+ });
3227
+ }
3228
+ async function runRetryRound(config, retryItems, options) {
3229
+ const startedAt = Date.now();
3230
+ const translateOne = options.translateOne ?? ((item) => translatePage(config, item, { model: options.model, force: true }));
3231
+ if (options.mode === "direct") {
3232
+ const active = /* @__PURE__ */ new Set();
3233
+ await runPool(retryItems, options.concurrency, async (item) => {
3234
+ const label = labelForItem(item);
3235
+ active.add(label);
3236
+ options.onProgress?.({ type: "item-start", item, active: [...active] });
3237
+ const result = await translateOne(item);
3238
+ active.delete(label);
3239
+ options.onResult(result);
3240
+ });
3241
+ return;
3242
+ }
3243
+ const entries = [];
3244
+ for (const item of retryItems) {
3245
+ const itemStartedAt = Date.now();
3246
+ try {
3247
+ const outcome = prepareTranslation(config, item, { model: options.model, force: true }, itemStartedAt);
3248
+ if (outcome.status === "done") {
3249
+ options.onResult(outcome.result);
3250
+ } else {
3251
+ entries.push({
3252
+ apiModel: resolveGeminiModelId(displayModelFor(outcome.prepared)),
3253
+ prompt: outcome.prepared.prompt,
3254
+ prepared: outcome.prepared
3255
+ });
3256
+ }
3257
+ } catch (error) {
3258
+ options.onResult(failedResultForItem(item, error, itemStartedAt));
3259
+ }
3260
+ }
3261
+ const plans = planBatchJobs(entries);
3262
+ const submitted = await Promise.all(
3263
+ plans.map(async (plan, planIndex) => {
3264
+ try {
3265
+ return await submitBatchJobPlan(config, {
3266
+ plan,
3267
+ displayModel: normalizeGeminiDisplayName(plan.apiModel),
3268
+ jobIndex: planIndex,
3269
+ jobCount: plans.length,
3270
+ onProgress: options.onProgress
3271
+ });
3272
+ } catch (error) {
3273
+ for (const entry of plan.entries) {
3274
+ options.onResult(failedResultForItem(entry.prepared.item, error, startedAt));
3275
+ }
3276
+ return void 0;
3277
+ }
3278
+ })
3279
+ );
3280
+ const jobsToPoll = submitted.filter((row) => row !== void 0);
3281
+ if (jobsToPoll.length > 0) {
3282
+ await pollAndIngestBatchJobs(config, jobsToPoll, {
3283
+ jobCount: jobsToPoll.length,
3284
+ onProgress: options.onProgress,
3285
+ onResult: options.onResult
3286
+ });
3287
+ }
3288
+ }
3202
3289
  async function translatePage(config, item, options = {}) {
3203
3290
  const startedAt = Date.now();
3204
3291
  let outcome;
@@ -3371,9 +3458,40 @@ async function translateWorklist(config, items, options = {}) {
3371
3458
  });
3372
3459
  }
3373
3460
  }
3374
- const results = collector.assemble();
3461
+ const mainResults = collector.assemble();
3462
+ const failed = mainResults.filter((result) => result.failed);
3463
+ if (failed.length === 0) {
3464
+ const totals2 = summarizeResults(mainResults, Date.now() - startedAt);
3465
+ options.onProgress?.({ type: "done", results: mainResults, totals: totals2 });
3466
+ return mainResults;
3467
+ }
3468
+ options.onProgress?.({ type: "retry-start", failed });
3469
+ const originalByKey = new Map(
3470
+ items.map((item) => [translationItemKey(item), item])
3471
+ );
3472
+ const retryItems = buildRetryWorklist(failed, originalByKey);
3473
+ const retryByKey = /* @__PURE__ */ new Map();
3474
+ await runRetryRound(config, retryItems, {
3475
+ model: options.model,
3476
+ force: options.force,
3477
+ concurrency,
3478
+ mode,
3479
+ onProgress: options.onProgress,
3480
+ onResult: (result) => {
3481
+ retryByKey.set(translationItemKey(result), result);
3482
+ options.onProgress?.({ type: "item-done", result });
3483
+ }
3484
+ });
3485
+ const results = mainResults.map((result) => {
3486
+ if (!result.failed) return result;
3487
+ return retryByKey.get(translationItemKey(result)) ?? result;
3488
+ });
3489
+ const retriedTranslated = failed.filter((original) => {
3490
+ const retried = retryByKey.get(translationItemKey(original));
3491
+ return retried !== void 0 && !retried.failed && !retried.skipped;
3492
+ }).length;
3375
3493
  const totals = summarizeResults(results, Date.now() - startedAt);
3376
- options.onProgress?.({ type: "done", results, totals });
3494
+ options.onProgress?.({ type: "done", results, totals, retriedTranslated });
3377
3495
  return results;
3378
3496
  }
3379
3497
  function countPendingItems(config, jobs) {
@@ -4195,10 +4313,11 @@ function detailForResult(result) {
4195
4313
  if (result.error) parts.push(red(result.error));
4196
4314
  return parts.length > 0 ? dim(parts.join(" \xB7 ")) : "";
4197
4315
  }
4198
- function formatSummaryLine(totals, dryRun) {
4316
+ function formatSummaryLine(totals, dryRun, retriedTranslated = 0) {
4199
4317
  const action = dryRun ? "Would translate" : "Translated";
4318
+ const base = totals.translated - retriedTranslated;
4200
4319
  const parts = [
4201
- `${action} ${totals.translated}`,
4320
+ retriedTranslated > 0 ? `${action} ${base} +${retriedTranslated} on retry` : `${action} ${totals.translated}`,
4202
4321
  `skipped ${totals.skipped}`
4203
4322
  ];
4204
4323
  if (totals.failed > 0) parts.push(red(`failed ${totals.failed}`));
@@ -4247,8 +4366,18 @@ function createTranslateProgressReporter(options = {}) {
4247
4366
  }
4248
4367
  return;
4249
4368
  }
4369
+ if (event.type === "retry-start") {
4370
+ for (const result of event.failed) {
4371
+ console.log(`${labelForResult(result)}: ${result.error ?? "failed"}`);
4372
+ }
4373
+ console.log(`Retrying ${event.failed.length} failed items with validation context...`);
4374
+ return;
4375
+ }
4250
4376
  if (event.type === "done") {
4251
- console.log(formatSummaryLine(event.totals, dryRun));
4377
+ console.log(formatSummaryLine(event.totals, dryRun, event.retriedTranslated));
4378
+ for (const result of event.results.filter((entry) => entry.failed)) {
4379
+ console.error(`${labelForResult(result)}: ${result.error ?? "failed"}`);
4380
+ }
4252
4381
  }
4253
4382
  },
4254
4383
  finish() {
@@ -4415,12 +4544,25 @@ function createTranslateProgressReporter(options = {}) {
4415
4544
  render();
4416
4545
  break;
4417
4546
  }
4547
+ case "retry-start": {
4548
+ total += event.failed.length;
4549
+ printPersistent([
4550
+ ...event.failed.map(
4551
+ (result) => red(`${labelForResult(result)}: ${result.error ?? "failed"}`)
4552
+ ),
4553
+ cyan(`Retrying ${event.failed.length} failed items with validation context...`)
4554
+ ]);
4555
+ render();
4556
+ break;
4557
+ }
4418
4558
  case "done":
4419
4559
  showCursor();
4420
4560
  if (renderedLines > 0) {
4421
4561
  process.stdout.write(`\x1B[${renderedLines}A`);
4422
4562
  }
4423
- process.stdout.write("\x1B[2K" + green("Done") + " \xB7 " + formatSummaryLine(event.totals, dryRun) + "\n");
4563
+ process.stdout.write(
4564
+ "\x1B[2K" + green("Done") + " \xB7 " + formatSummaryLine(event.totals, dryRun, event.retriedTranslated) + "\n"
4565
+ );
4424
4566
  renderedLines = 0;
4425
4567
  for (const result of event.results.filter((entry) => entry.failed)) {
4426
4568
  process.stdout.write("\x1B[2K" + red(`${labelForResult(result)}: ${result.error ?? "failed"}`) + "\n");