{"version":3,"file":"reviewReport.mjs","names":[],"sources":["../../../src/docReview/reviewReport.ts"],"sourcesContent":["import * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, colorizeNumber } from '@intlayer/config/logger';\nimport { buildAlignmentPlan } from './pipeline';\n\n/**\n * The kind of change detected for a block when reviewing a translation against\n * its base document.\n *\n * - `review`: the base block changed and its translation must be updated.\n * - `insert_new`: the base block has no translation yet and must be added.\n * - `delete`: the target block no longer exists in the base and should be removed.\n */\nexport type ReviewBlockAction = 'review' | 'insert_new' | 'delete';\n\n/** A 1-based, inclusive line range within a document. */\nexport type LineRange = {\n  start: number;\n  end: number;\n};\n\n/**\n * A single block that diverges between the base document and its translation.\n *\n * This is the unit an external translator (AI client, human, or agent) needs to\n * act on: it carries the base content to translate, the existing translation to\n * update, and where each lives so the change can be located in the file.\n */\nexport type ReviewReportBlock = {\n  /** What should happen to this block. */\n  action: ReviewBlockAction;\n  /** Line range of the block in the base document (omitted for pure deletions). */\n  baseLineRange?: LineRange;\n  /** Line range of the block in the target document (omitted for new insertions). */\n  targetLineRange?: LineRange;\n  /** Raw markdown of the base block (omitted for pure deletions). */\n  baseContent?: string;\n  /** Existing translation of the block (omitted for new insertions). */\n  targetContent?: string;\n};\n\nexport type ReviewReportSummary = {\n  /** Number of blocks reused as-is (unchanged translation). */\n  reuse: number;\n  /** Number of blocks whose translation must be updated. */\n  review: number;\n  /** Number of blocks missing a translation. */\n  insertNew: number;\n  /** Number of stale target blocks to delete. */\n  delete: number;\n};\n\nexport type ReviewReport = {\n  /** Only the blocks that diverge (review, insert_new, delete). */\n  blocks: ReviewReportBlock[];\n  /** Counts per action over the whole document, including reused blocks. */\n  summary: ReviewReportSummary;\n};\n\nexport type BuildReviewReportInput = {\n  /** The base (source) document, used as the translation reference. */\n  baseText: string;\n  /** The existing target (translated) document, possibly empty. */\n  targetText: string;\n  /**\n   * 1-based line numbers that changed in the base document.\n   *\n   * When omitted the whole document is compared, and only inserted and deleted\n   * blocks are reported (no aligned block is flagged for review since there is\n   * no way to know which ones changed). An empty array means nothing changed,\n   * and no block at all is reported.\n   */\n  changedLines?: number[];\n};\n\n/**\n * Compare a base markdown document with its translation and report only the\n * blocks that need attention, with their line ranges and content.\n *\n * Reusable across the CLI (`doc review --log`), the backend (comparing two\n * translation contents stored in database), and agents that generate the\n * missing translations.\n *\n * @param input - The base/target texts and optional changed lines.\n * @returns The divergent blocks and a per-action summary.\n */\nexport const buildReviewReport = ({\n  baseText,\n  targetText,\n  changedLines,\n}: BuildReviewReportInput): ReviewReport => {\n  const { baseBlocks, targetBlocks, plan } = buildAlignmentPlan({\n    baseText,\n    targetText,\n    changedLines,\n  });\n\n  const summary: ReviewReportSummary = {\n    reuse: 0,\n    review: 0,\n    insertNew: 0,\n    delete: 0,\n  };\n\n  const blocks: ReviewReportBlock[] = [];\n\n  for (const [actionIndex, action] of plan.actions.entries()) {\n    if (action.kind === 'reuse') {\n      summary.reuse += 1;\n      continue;\n    }\n\n    if (action.kind === 'review') {\n      summary.review += 1;\n      const baseBlock = baseBlocks[action.baseIndex];\n\n      if (!baseBlock) continue;\n\n      const targetBlock =\n        action.targetIndex !== null ? targetBlocks[action.targetIndex] : null;\n\n      blocks.push({\n        action: 'review',\n        baseLineRange: { start: baseBlock.lineStart, end: baseBlock.lineEnd },\n        targetLineRange: targetBlock\n          ? { start: targetBlock.lineStart, end: targetBlock.lineEnd }\n          : undefined,\n        baseContent: baseBlock.content,\n        targetContent: targetBlock?.content,\n      });\n      continue;\n    }\n\n    if (action.kind === 'insert_new') {\n      summary.insertNew += 1;\n      const baseBlock = baseBlocks[action.baseIndex];\n\n      if (!baseBlock) continue;\n\n      // Find the target line after which the new block should be inserted:\n      // scan backwards for the last action that produced a target block.\n      let insertAfterLine: number | undefined;\n      for (let prevIndex = actionIndex - 1; prevIndex >= 0; prevIndex--) {\n        const prevAction = plan.actions[prevIndex];\n        if (!prevAction) continue;\n\n        let targetIdx: number | null = null;\n        if (prevAction.kind === 'reuse') targetIdx = prevAction.targetIndex;\n        else if (\n          prevAction.kind === 'review' &&\n          prevAction.targetIndex !== null\n        )\n          targetIdx = prevAction.targetIndex;\n        else if (prevAction.kind === 'delete')\n          targetIdx = prevAction.targetIndex;\n\n        if (targetIdx !== null) {\n          const prevTargetBlock = targetBlocks[targetIdx];\n          if (prevTargetBlock) {\n            insertAfterLine = prevTargetBlock.lineEnd + 1;\n            break;\n          }\n        }\n      }\n\n      blocks.push({\n        action: 'insert_new',\n        baseLineRange: { start: baseBlock.lineStart, end: baseBlock.lineEnd },\n        targetLineRange:\n          insertAfterLine !== undefined\n            ? { start: insertAfterLine, end: insertAfterLine }\n            : undefined,\n        baseContent: baseBlock.content,\n      });\n      continue;\n    }\n\n    if (action.kind === 'delete') {\n      summary.delete += 1;\n      const targetBlock = targetBlocks[action.targetIndex];\n\n      if (!targetBlock) continue;\n\n      blocks.push({\n        action: 'delete',\n        targetLineRange: {\n          start: targetBlock.lineStart,\n          end: targetBlock.lineEnd,\n        },\n        targetContent: targetBlock.content,\n      });\n    }\n  }\n\n  return { blocks, summary };\n};\n\n/** Render a line range as a blue colored string, or an orange dash when absent. */\nconst colorizeLineRange = (range?: LineRange): string => {\n  if (!range) return colorize('—', ANSIColors.ORANGE);\n  const rangeStr =\n    range.start === range.end\n      ? `L${range.start}`\n      : `L${range.start}-${range.end}`;\n  return colorize(rangeStr, ANSIColors.BLUE);\n};\n\n/**\n * Colorize a count with green when zero (nothing to do) and a caller-supplied\n * non-zero color (orange for warnings, red for destructive actions).\n */\nconst colorizeCount = (\n  count: number,\n  nonZeroColor: (typeof ANSIColors)[keyof typeof ANSIColors]\n): string =>\n  colorizeNumber(count, {\n    zero: ANSIColors.GREEN,\n    one: nonZeroColor,\n    two: nonZeroColor,\n    few: nonZeroColor,\n    many: nonZeroColor,\n    other: nonZeroColor,\n  });\n\n/**\n * Render a {@link ReviewReport} as a human and agent readable log.\n *\n * Each divergent block is printed with its action, the base/target line ranges,\n * the base content to translate, and the existing translation to update.\n *\n * @param report - The report to format.\n * @param options - Optional labels for the base and target locales.\n * @returns A multi-line string describing every block that needs attention.\n */\nexport const formatReviewReport = (\n  report: ReviewReport,\n  options?: { baseLabel?: string; targetLabel?: string }\n): string => {\n  const baseLabel = options?.baseLabel ?? 'base';\n  const targetLabel = options?.targetLabel ?? 'target';\n\n  const { summary, blocks } = report;\n\n  // Build the summary header piece-by-piece so each count uses its own color.\n  const header = [\n    colorize('Review report: ', ANSIColors.ORANGE),\n    colorizeCount(blocks.length, ANSIColors.ORANGE),\n    colorize(' block(s) need attention (review=', ANSIColors.ORANGE),\n    colorizeCount(summary.review, ANSIColors.ORANGE),\n    colorize(', new=', ANSIColors.ORANGE),\n    colorizeCount(summary.insertNew, ANSIColors.ORANGE),\n    colorize(', delete=', ANSIColors.ORANGE),\n    colorizeCount(summary.delete, ANSIColors.RED),\n    colorize(', reuse=', ANSIColors.ORANGE),\n    colorizeNumber(summary.reuse),\n    colorize(').', ANSIColors.ORANGE),\n  ].join('');\n\n  if (blocks.length === 0) {\n    return `${header}\\n${colorize('No changes needed.', ANSIColors.GREEN)}`;\n  }\n\n  const sections = blocks.map((block, index) => {\n    const lines: string[] = [];\n\n    // Block header: each token colored individually.\n    const blockHeader = [\n      colorize('--- Block ', ANSIColors.ORANGE),\n      colorizeNumber(index + 1),\n      colorize('/', ANSIColors.ORANGE),\n      colorizeNumber(blocks.length),\n      colorize(` [${block.action}] `, ANSIColors.ORANGE),\n      baseLabel,\n      colorize(' ', ANSIColors.ORANGE),\n      colorizeLineRange(block.baseLineRange),\n      colorize(' → ', ANSIColors.ORANGE),\n      targetLabel,\n      colorize(' ', ANSIColors.ORANGE),\n      colorizeLineRange(block.targetLineRange),\n      colorize(' ---', ANSIColors.ORANGE),\n    ].join('');\n\n    lines.push(blockHeader);\n\n    if (block.baseContent !== undefined) {\n      lines.push(colorize(`[${baseLabel}]`, ANSIColors.BEIGE));\n      lines.push(colorize(block.baseContent.trimEnd(), ANSIColors.GREY));\n    }\n\n    if (block.targetContent !== undefined) {\n      lines.push(colorize(`[${targetLabel}]`, ANSIColors.BEIGE));\n      lines.push(colorize(block.targetContent.trimEnd(), ANSIColors.GREY));\n    } else if (block.action === 'insert_new') {\n      lines.push(colorize(`[${targetLabel}]`, ANSIColors.BEIGE));\n      lines.push(colorize('(missing — to be translated)', ANSIColors.ORANGE));\n    }\n\n    return lines.join('\\n');\n  });\n\n  const editingNote = colorize(\n    'Tip: start editing from the last block — working bottom-up keeps earlier line numbers accurate.',\n    ANSIColors.GREY\n  );\n\n  return [header, '', ...sections, '', editingNote].join('\\n');\n};\n"],"mappings":"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"}