{"version":3,"file":"pipeline.mjs","names":[],"sources":["../../../src/docReview/pipeline.ts"],"sourcesContent":["import { alignBaseAndTargetBlocks } from './alignBlocks';\nimport { fingerprintBlock } from './fingerprintBlock';\nimport { mapChangedLinesToBlocks } from './mapChangedLinesToBlocks';\nimport { normalizeBlock } from './normalizeBlock';\nimport {\n  identifySegmentsToReview,\n  mergeReviewedSegments,\n  type SegmentToReview,\n} from './rebuildDocument';\nimport { segmentDocument, segmentSections } from './segmentDocument';\nimport type {\n  AlignmentPlan,\n  Block,\n  FingerprintedBlock,\n  PlannedAction,\n} from './types';\n\nexport type BuildAlignmentPlanInput = {\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   * `undefined` means the changed lines are unknown (no git history available):\n   * every section is then compared block by block. An empty array means nothing\n   * changed, and every aligned block is reused.\n   */\n  changedLines: number[] | undefined;\n};\n\nexport type BuildAlignmentPlanOutput = {\n  baseBlocks: FingerprintedBlock[];\n  targetBlocks: FingerprintedBlock[];\n  plan: AlignmentPlan;\n  segmentsToReview: SegmentToReview[];\n};\n\n/** Fingerprint a list of already-segmented blocks (context = neighbours). */\nconst fingerprintBlockList = (blocks: Block[]): FingerprintedBlock[] => {\n  const normalized = blocks.map(normalizeBlock);\n\n  return normalized.map((block, index, array) =>\n    fingerprintBlock(block, array[index - 1] ?? null, array[index + 1] ?? null)\n  );\n};\n\n/**\n * Re-segment a section's own text into fine blocks, shifting their relative line\n * numbers back to absolute document positions so changed-line mapping and the\n * review report keep reporting real file coordinates.\n */\nconst fingerprintSectionFineBlocks = (section: Block): FingerprintedBlock[] => {\n  const lineOffset = section.lineStart - 1;\n\n  const fineBlocks = segmentDocument(section.content).map(\n    (block): Block => ({\n      ...block,\n      lineStart: block.lineStart + lineOffset,\n      lineEnd: block.lineEnd + lineOffset,\n    })\n  );\n\n  return fingerprintBlockList(fineBlocks);\n};\n\n/**\n * Build the block-aware alignment plan between a base document and its\n * translation, in two levels.\n *\n * 1. **Sections** (heading-anchored) are aligned first. Because a document and\n *    its translation share the same heading structure, this alignment is robust\n *    and never drops a section just because the prose was split into a different\n *    number of paragraphs.\n * 2. Only the sections **touched by a changed line** are then re-segmented into\n *    fine blocks (paragraphs, code fences) and aligned within the section, so a\n *    small edit re-translates only the affected paragraph(s) instead of the\n *    whole section. Within a changed section a target block with no base\n *    counterpart is **kept as-is** (reused) rather than deleted, so a translation\n *    that has extra paragraphs never loses content.\n *    When `changedLines` is `undefined` the changed lines are simply unknown, so\n *    **every** section is inspected instead of none: aligned blocks are still\n *    reused (there is no way to tell which translation went stale), but blocks\n *    living on one side only are reported as `insert_new` / `delete`. This is\n *    what makes a plain \"compare this document with its translation\" run — one\n *    with no git history behind it — report anything at all.\n *\n * Section-level insertions and deletions stay whole: a brand-new section is\n * translated as one unit, and a target section with no base counterpart is\n * reported as `delete` for visibility but never dropped by the merge (see\n * {@link mergeReviewedSegments}), so a review can never lose translated content.\n *\n * @param input - The base/target texts and optional changed lines.\n * @returns The (flattened) blocks, the plan, and the segments that need translation.\n */\nexport const buildAlignmentPlan = ({\n  baseText,\n  targetText,\n  changedLines,\n}: BuildAlignmentPlanInput): BuildAlignmentPlanOutput => {\n  // `undefined` means \"which lines changed is unknown\" (no git history to read),\n  // which is not the same as \"no line changed\": the former must still compare the\n  // two documents, the latter must reuse everything.\n  const hasChangedLineInformation = Array.isArray(changedLines);\n  const changedLineNumbers = changedLines ?? [];\n\n  const baseSections = fingerprintBlockList(segmentSections(baseText));\n  const targetSections = fingerprintBlockList(segmentSections(targetText));\n\n  const sectionAlignment = alignBaseAndTargetBlocks(\n    baseSections,\n    targetSections\n  );\n  const changedSectionIndexes = mapChangedLinesToBlocks(\n    baseSections,\n    changedLineNumbers\n  );\n\n  /**\n   * Whether a section must be opened and inspected block by block.\n   *\n   * Without changed-line information every section is inspected, so comparing a\n   * document with its translation from scratch still reports the blocks that\n   * exist on one side only. With changed lines, only the touched sections are.\n   */\n  const isSectionToInspect = (sectionIndex: number): boolean =>\n    !hasChangedLineInformation || changedSectionIndexes.has(sectionIndex);\n\n  // Flattened blocks the plan refers to by index. Reused/deleted/inserted\n  // sections contribute their whole-section block; changed sections contribute\n  // their fine sub-blocks.\n  const baseBlocks: FingerprintedBlock[] = [];\n  const targetBlocks: FingerprintedBlock[] = [];\n  const actions: PlannedAction[] = [];\n\n  const pushBaseBlock = (block: FingerprintedBlock): number =>\n    baseBlocks.push(block) - 1;\n  const pushTargetBlock = (block: FingerprintedBlock): number =>\n    targetBlocks.push(block) - 1;\n\n  for (const pair of sectionAlignment) {\n    // Section present only in the target → reported as `delete` for visibility,\n    // but kept verbatim by the merge (the aligner may simply have failed to\n    // follow a reordered section), never silently dropped.\n    if (pair.baseIndex === -1 && pair.targetIndex !== null) {\n      const targetIndex = pushTargetBlock(targetSections[pair.targetIndex]!);\n      actions.push({ kind: 'delete', targetIndex });\n      continue;\n    }\n\n    // Section present only in the base → brand new, translate as one unit.\n    if (pair.baseIndex >= 0 && pair.targetIndex === null) {\n      const baseIndex = pushBaseBlock(baseSections[pair.baseIndex]!);\n      actions.push({ kind: 'insert_new', baseIndex });\n      continue;\n    }\n\n    if (pair.baseIndex < 0 || pair.targetIndex === null) continue;\n\n    const baseSection = baseSections[pair.baseIndex]!;\n    const targetSection = targetSections[pair.targetIndex]!;\n\n    // Unchanged section → reuse the existing translation verbatim.\n    if (!isSectionToInspect(pair.baseIndex)) {\n      const baseIndex = pushBaseBlock(baseSection);\n      const targetIndex = pushTargetBlock(targetSection);\n      actions.push({ kind: 'reuse', baseIndex, targetIndex });\n      continue;\n    }\n\n    // Inspected section → align its fine blocks and review only what changed.\n    const baseFineBlocks = fingerprintSectionFineBlocks(baseSection);\n    const targetFineBlocks = fingerprintSectionFineBlocks(targetSection);\n    const fineAlignment = alignBaseAndTargetBlocks(\n      baseFineBlocks,\n      targetFineBlocks\n    );\n    const changedFineIndexes = mapChangedLinesToBlocks(\n      baseFineBlocks,\n      changedLineNumbers\n    );\n\n    for (const finePair of fineAlignment) {\n      // Target-only fine block. Its content is kept either way — `delete` is\n      // reported for visibility only and the merge keeps it verbatim (see\n      // {@link mergeReviewedSegments}).\n      // With changed lines the author's edit is known precisely, so an unmatched\n      // target block is most likely the translator's own prose split and stays\n      // silent. Without them the whole document is being compared, and such a\n      // block is exactly the divergence the report exists to surface.\n      if (finePair.baseIndex === -1 && finePair.targetIndex !== null) {\n        const targetIndex = pushTargetBlock(\n          targetFineBlocks[finePair.targetIndex]!\n        );\n        actions.push(\n          hasChangedLineInformation\n            ? { kind: 'reuse', baseIndex: -1, targetIndex }\n            : { kind: 'delete', targetIndex }\n        );\n        continue;\n      }\n\n      // Base-only fine block: a new paragraph inside the section, translate it.\n      if (finePair.baseIndex >= 0 && finePair.targetIndex === null) {\n        const baseIndex = pushBaseBlock(baseFineBlocks[finePair.baseIndex]!);\n        actions.push({ kind: 'insert_new', baseIndex });\n        continue;\n      }\n\n      if (finePair.baseIndex < 0 || finePair.targetIndex === null) continue;\n\n      const baseIndex = pushBaseBlock(baseFineBlocks[finePair.baseIndex]!);\n      const targetIndex = pushTargetBlock(\n        targetFineBlocks[finePair.targetIndex]!\n      );\n\n      actions.push(\n        changedFineIndexes.has(finePair.baseIndex)\n          ? { kind: 'review', baseIndex, targetIndex }\n          : { kind: 'reuse', baseIndex, targetIndex }\n      );\n    }\n  }\n\n  const plan = { actions };\n\n  const { segmentsToReview } = identifySegmentsToReview({\n    baseBlocks,\n    targetBlocks,\n    plan,\n  });\n\n  return { baseBlocks, targetBlocks, plan, segmentsToReview };\n};\n\nexport type { SegmentToReview };\nexport { mergeReviewedSegments };\n"],"mappings":"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"}