{"version":3,"file":"rebuildDocument.mjs","names":[],"sources":["../../../src/docReview/rebuildDocument.ts"],"sourcesContent":["import type { AlignmentPlan, FingerprintedBlock } from './types';\n\n/**\n * A block that needs to be translated or re-translated by an external consumer\n * (an AI client, a human, or an agent).\n */\nexport type SegmentToReview = {\n  /** The base block to translate. */\n  baseBlock: FingerprintedBlock;\n  /** Existing target translation, or `null` when the block is new. */\n  targetBlockText: string | null;\n  /** Index of the originating action within {@link AlignmentPlan.actions}. */\n  actionIndex: number;\n};\n\nexport type RebuildInput = {\n  baseBlocks: FingerprintedBlock[];\n  targetBlocks: FingerprintedBlock[];\n  plan: AlignmentPlan;\n};\n\nexport type RebuildResult = {\n  segmentsToReview: SegmentToReview[];\n};\n\n/**\n * Analyze the alignment plan and return only the segments that need\n * review/translation. Does not generate output text - that is done by\n * {@link mergeReviewedSegments} once the translations are available.\n *\n * @param input - The base/target blocks and the alignment plan.\n * @returns The list of segments that require translation.\n */\nexport const identifySegmentsToReview = ({\n  baseBlocks,\n  targetBlocks,\n  plan,\n}: RebuildInput): RebuildResult => {\n  const segmentsToReview: SegmentToReview[] = [];\n\n  plan.actions.forEach((action, actionIndex) => {\n    if (action.kind === 'review') {\n      const baseBlock = baseBlocks[action.baseIndex];\n      const targetBlockText =\n        action.targetIndex !== null\n          ? targetBlocks[action.targetIndex].content\n          : null;\n\n      segmentsToReview.push({ baseBlock, targetBlockText, actionIndex });\n    } else if (action.kind === 'insert_new') {\n      const baseBlock = baseBlocks[action.baseIndex];\n\n      segmentsToReview.push({\n        baseBlock,\n        targetBlockText: null,\n        actionIndex,\n      });\n    }\n  });\n\n  return { segmentsToReview };\n};\n\n/** Markdown separates two blocks with a blank line. */\nconst endsWithBlockBoundary = (text: string): boolean =>\n  /\\n[ \\t]*\\n$/.test(text);\n\n/**\n * The newlines missing at the end of `text` for it to close a markdown block.\n *\n * @param text - The document built so far.\n * @returns `''`, `'\\n'` or `'\\n\\n'` depending on how the text already ends.\n */\nconst buildBlockSeparator = (text: string): string => {\n  if (text.length === 0 || endsWithBlockBoundary(text)) return '';\n\n  return text.endsWith('\\n') ? '\\n' : '\\n\\n';\n};\n\n/**\n * Merge reviewed translations back into the final document following the\n * alignment plan, reusing untouched target blocks as-is.\n *\n * @param plan - The alignment plan.\n * @param targetBlocks - Blocks of the existing target document.\n * @param reviewedSegments - Map of action index to its reviewed translation.\n * @returns The rebuilt target document.\n */\nexport const mergeReviewedSegments = (\n  plan: AlignmentPlan,\n  targetBlocks: FingerprintedBlock[],\n  reviewedSegments: Map<number, string>\n): string => {\n  let mergedText = '';\n  let previousPartWasGenerated = false;\n\n  /**\n   * Append one part, keeping a blank line around the content this run produced.\n   *\n   * Blocks own the blank lines that trail them, so two verbatim blocks already\n   * separate themselves and are appended untouched — a document with nothing to\n   * change merges back byte for byte. Generated content is the exception: the\n   * block it lands after may be the one that used to end the document, which\n   * carries no trailing blank line, and the two would be glued into a single\n   * markdown block (a heading swallowed by the paragraph above it).\n   */\n  const appendPart = (content: string, isGenerated: boolean): void => {\n    if (content.length === 0) return;\n\n    if (isGenerated || previousPartWasGenerated) {\n      mergedText += buildBlockSeparator(mergedText);\n    }\n\n    mergedText += content;\n    previousPartWasGenerated = isGenerated;\n  };\n\n  plan.actions.forEach((action, actionIndex) => {\n    if (action.kind === 'reuse') {\n      appendPart(targetBlocks[action.targetIndex]!.content, false);\n    } else if (action.kind === 'review' || action.kind === 'insert_new') {\n      const reviewedContent = reviewedSegments.get(actionIndex);\n\n      if (reviewedContent !== undefined) {\n        appendPart(reviewedContent, true);\n      } else {\n        // Fallback: if review failed, use existing or blank\n        if (action.kind === 'review' && action.targetIndex !== null) {\n          appendPart(targetBlocks[action.targetIndex]!.content, false);\n        } else {\n          appendPart('\\n', false);\n        }\n      }\n    } else if (action.kind === 'delete') {\n      const reviewedContent = reviewedSegments.get(actionIndex);\n      if (reviewedContent !== undefined) {\n        // Caller explicitly resolved this block: empty string = actually delete,\n        // non-empty string = replacement content.\n        appendPart(reviewedContent, true);\n      } else {\n        // Default: keep verbatim. A target block with no base counterpart may\n        // just be a section the aligner could not follow (reordering, split\n        // prose) — keeping it prevents accidental data loss in log/read-only mode.\n        appendPart(targetBlocks[action.targetIndex]!.content, false);\n      }\n    }\n  });\n\n  return mergedText;\n};\n"],"mappings":";;;;;;;;;AAiCA,MAAa,4BAA4B,EACvC,YACA,cACA,WACiC;CACjC,MAAM,mBAAsC,CAAC;CAE7C,KAAK,QAAQ,SAAS,QAAQ,gBAAgB;EAC5C,IAAI,OAAO,SAAS,UAAU;GAC5B,MAAM,YAAY,WAAW,OAAO;GACpC,MAAM,kBACJ,OAAO,gBAAgB,OACnB,aAAa,OAAO,YAAY,CAAC,UACjC;GAEN,iBAAiB,KAAK;IAAE;IAAW;IAAiB;GAAY,CAAC;EACnE,OAAO,IAAI,OAAO,SAAS,cAAc;GACvC,MAAM,YAAY,WAAW,OAAO;GAEpC,iBAAiB,KAAK;IACpB;IACA,iBAAiB;IACjB;GACF,CAAC;EACH;CACF,CAAC;CAED,OAAO,EAAE,iBAAiB;AAC5B;;AAGA,MAAM,yBAAyB,SAC7B,cAAc,KAAK,IAAI;;;;;;;AAQzB,MAAM,uBAAuB,SAAyB;CACpD,IAAI,KAAK,WAAW,KAAK,sBAAsB,IAAI,GAAG,OAAO;CAE7D,OAAO,KAAK,SAAS,IAAI,IAAI,OAAO;AACtC;;;;;;;;;;AAWA,MAAa,yBACX,MACA,cACA,qBACW;CACX,IAAI,aAAa;CACjB,IAAI,2BAA2B;;;;;;;;;;;CAY/B,MAAM,cAAc,SAAiB,gBAA+B;EAClE,IAAI,QAAQ,WAAW,GAAG;EAE1B,IAAI,eAAe,0BACjB,cAAc,oBAAoB,UAAU;EAG9C,cAAc;EACd,2BAA2B;CAC7B;CAEA,KAAK,QAAQ,SAAS,QAAQ,gBAAgB;EAC5C,IAAI,OAAO,SAAS,SAClB,WAAW,aAAa,OAAO,YAAY,CAAE,SAAS,KAAK;OACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,cAAc;GACnE,MAAM,kBAAkB,iBAAiB,IAAI,WAAW;GAExD,IAAI,oBAAoB,QACtB,WAAW,iBAAiB,IAAI;QAGhC,IAAI,OAAO,SAAS,YAAY,OAAO,gBAAgB,MACrD,WAAW,aAAa,OAAO,YAAY,CAAE,SAAS,KAAK;QAE3D,WAAW,MAAM,KAAK;EAG5B,OAAO,IAAI,OAAO,SAAS,UAAU;GACnC,MAAM,kBAAkB,iBAAiB,IAAI,WAAW;GACxD,IAAI,oBAAoB,QAGtB,WAAW,iBAAiB,IAAI;QAKhC,WAAW,aAAa,OAAO,YAAY,CAAE,SAAS,KAAK;EAE/D;CACF,CAAC;CAED,OAAO;AACT"}