@intlayer/engine 9.1.2 → 9.2.0

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.
Files changed (38) hide show
  1. package/dist/assets/initConfig/templates/cjs.txt +4 -1
  2. package/dist/assets/initConfig/templates/json.txt +4 -1
  3. package/dist/assets/initConfig/templates/mjs.txt +4 -1
  4. package/dist/assets/initConfig/templates/ts.txt +4 -1
  5. package/dist/cjs/docReview/alignBlocks.cjs +57 -8
  6. package/dist/cjs/docReview/alignBlocks.cjs.map +1 -1
  7. package/dist/cjs/docReview/rebuildDocument.cjs +37 -9
  8. package/dist/cjs/docReview/rebuildDocument.cjs.map +1 -1
  9. package/dist/cjs/docReview/segmentDocument.cjs +19 -2
  10. package/dist/cjs/docReview/segmentDocument.cjs.map +1 -1
  11. package/dist/cjs/prepareIntlayer.cjs +7 -3
  12. package/dist/cjs/prepareIntlayer.cjs.map +1 -1
  13. package/dist/cjs/utils/runOnce.cjs +129 -50
  14. package/dist/cjs/utils/runOnce.cjs.map +1 -1
  15. package/dist/cjs/writeFileIfChanged.cjs +14 -7
  16. package/dist/cjs/writeFileIfChanged.cjs.map +1 -1
  17. package/dist/esm/docReview/alignBlocks.mjs +57 -8
  18. package/dist/esm/docReview/alignBlocks.mjs.map +1 -1
  19. package/dist/esm/docReview/rebuildDocument.mjs +37 -9
  20. package/dist/esm/docReview/rebuildDocument.mjs.map +1 -1
  21. package/dist/esm/docReview/segmentDocument.mjs +19 -2
  22. package/dist/esm/docReview/segmentDocument.mjs.map +1 -1
  23. package/dist/esm/prepareIntlayer.mjs +7 -3
  24. package/dist/esm/prepareIntlayer.mjs.map +1 -1
  25. package/dist/esm/utils/runOnce.mjs +129 -50
  26. package/dist/esm/utils/runOnce.mjs.map +1 -1
  27. package/dist/esm/writeFileIfChanged.mjs +15 -8
  28. package/dist/esm/writeFileIfChanged.mjs.map +1 -1
  29. package/dist/types/docReview/alignBlocks.d.ts +4 -2
  30. package/dist/types/docReview/alignBlocks.d.ts.map +1 -1
  31. package/dist/types/docReview/rebuildDocument.d.ts.map +1 -1
  32. package/dist/types/docReview/segmentDocument.d.ts.map +1 -1
  33. package/dist/types/docReview/types.d.ts +9 -0
  34. package/dist/types/docReview/types.d.ts.map +1 -1
  35. package/dist/types/utils/index.d.ts +2 -2
  36. package/dist/types/utils/runOnce.d.ts +30 -6
  37. package/dist/types/utils/runOnce.d.ts.map +1 -1
  38. package/package.json +10 -10
@@ -28,7 +28,10 @@ const config = {
28
28
  /**
29
29
  * Enables the Intlayer locale-routing proxy (middleware).
30
30
  * Handles locale detection, redirects and rewrites in dev, preview and SSR.
31
- * Default: true
31
+ * Leave unset for auto mode: dev and preview servers keep locale routing
32
+ * URL-driven by ignoring the stored locale. `true` forces the full
33
+ * behaviour everywhere, `false` disables the proxy.
34
+ * Default: undefined (auto)
32
35
  */
33
36
  enableProxy: false,
34
37
  },
@@ -23,7 +23,10 @@
23
23
  /**
24
24
  * Enables the Intlayer locale-routing proxy (middleware).
25
25
  * Handles locale detection, redirects and rewrites in dev, preview and SSR.
26
- * Default: true
26
+ * Leave unset for auto mode: dev and preview servers keep locale routing
27
+ * URL-driven by ignoring the stored locale. `true` forces the full
28
+ * behaviour everywhere, `false` disables the proxy.
29
+ * Default: undefined (auto)
27
30
  */
28
31
  "enableProxy": false
29
32
  },
@@ -28,7 +28,10 @@ const config = {
28
28
  /**
29
29
  * Enables the Intlayer locale-routing proxy (middleware).
30
30
  * Handles locale detection, redirects and rewrites in dev, preview and SSR.
31
- * Default: true
31
+ * Leave unset for auto mode: dev and preview servers keep locale routing
32
+ * URL-driven by ignoring the stored locale. `true` forces the full
33
+ * behaviour everywhere, `false` disables the proxy.
34
+ * Default: undefined (auto)
32
35
  */
33
36
  enableProxy: false,
34
37
  },
@@ -27,7 +27,10 @@ const config: IntlayerConfig = {
27
27
  /**
28
28
  * Enables the Intlayer locale-routing proxy (middleware).
29
29
  * Handles locale detection, redirects and rewrites in dev, preview and SSR.
30
- * Default: true
30
+ * Leave unset for auto mode: dev and preview servers keep locale routing
31
+ * URL-driven by ignoring the stored locale. `true` forces the full
32
+ * behaviour everywhere, `false` disables the proxy.
33
+ * Default: undefined (auto)
31
34
  */
32
35
  enableProxy: false,
33
36
  },
@@ -2,12 +2,54 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_docReview_computeSimilarity = require('./computeSimilarity.cjs');
3
3
 
4
4
  //#region src/docReview/alignBlocks.ts
5
+ /** Cost of leaving a block unaligned (an insertion or a deletion). */
6
+ const GAP_PENALTY = -2;
7
+ /**
8
+ * Score of a pair that structural evidence rules out.
9
+ *
10
+ * Strictly below the cost of two gaps (`2 * GAP_PENALTY`) so the aligner always
11
+ * prefers reporting an insertion plus a deletion over such a pair. Every other
12
+ * score is positive, which means that without an explicit veto the aligner would
13
+ * rather pair two unrelated blocks than leave them unaligned — and a bogus pair
14
+ * is planned as `reuse`, which keeps the stale translation verbatim *and*
15
+ * inserts the freshly translated base block next to it, producing the duplicated
16
+ * headings this penalty exists to prevent.
17
+ */
18
+ const STRUCTURAL_MISMATCH_PENALTY = GAP_PENALTY * 2 - 1;
19
+ /** Reward for two blocks opened by a heading of the very same depth. */
20
+ const HEADING_DEPTH_MATCH_BONUS = 3;
21
+ /** Minimum length ratio for the (small) "comparable size" reward. */
22
+ const COMPARABLE_LENGTH_RATIO = .75;
23
+ /**
24
+ * Body length from which a section is considered to carry content of its own.
25
+ *
26
+ * Kept above a single sentence so a translator's short lead-in paragraph is not
27
+ * mistaken for a whole section body.
28
+ */
29
+ const SUBSTANTIAL_BODY_LENGTH = 80;
30
+ /**
31
+ * Length of the body a block carries below its opening heading.
32
+ *
33
+ * A section that only holds its heading (because subsections carry all of its
34
+ * content) is structurally different from one that holds a full body, and that
35
+ * difference survives translation.
36
+ *
37
+ * @param block - The block to measure.
38
+ * @returns The trimmed length of everything below the opening heading line.
39
+ */
40
+ const measureBodyLength = (block) => {
41
+ if (block.headingDepth === null) return block.content.trim().length;
42
+ const [, ...bodyLines] = block.content.split("\n");
43
+ return bodyLines.join("\n").trim().length;
44
+ };
5
45
  /**
6
46
  * Align the blocks of a base document with the blocks of its translation using a
7
- * Needleman–Wunsch global alignment over anchor similarity and block type.
47
+ * Needleman–Wunsch global alignment over heading depth, anchor similarity and
48
+ * block type.
8
49
  *
9
50
  * Because prose differs across languages, the score is weighted toward the
10
- * structural anchor (digits and symbols) rather than the words themselves.
51
+ * structural signals — heading depth first, then the anchor (digits and symbols)
52
+ * — rather than the words themselves.
11
53
  *
12
54
  * @param baseBlocks - Blocks of the base (source) document.
13
55
  * @param targetBlocks - Blocks of the target (translated) document.
@@ -18,26 +60,33 @@ const alignBaseAndTargetBlocks = (baseBlocks, targetBlocks) => {
18
60
  const targetLength = targetBlocks.length;
19
61
  const scoreMatrix = Array.from({ length: baseLength + 1 }, () => Array.from({ length: targetLength + 1 }, () => 0));
20
62
  const traceMatrix = Array.from({ length: baseLength + 1 }, () => Array.from({ length: targetLength + 1 }, () => "diagonal"));
21
- const gapPenalty = -2;
22
63
  const computeMatchScore = (baseIndex, targetIndex) => {
23
64
  const baseBlock = baseBlocks[baseIndex];
24
65
  const targetBlock = targetBlocks[targetIndex];
66
+ const hasComparableHeadingDepths = baseBlock.headingDepth !== null && targetBlock.headingDepth !== null;
67
+ if (hasComparableHeadingDepths && baseBlock.headingDepth !== targetBlock.headingDepth) return STRUCTURAL_MISMATCH_PENALTY;
68
+ const baseBodyLength = measureBodyLength(baseBlock);
69
+ const targetBodyLength = measureBodyLength(targetBlock);
70
+ if (baseBodyLength === 0 && targetBodyLength >= SUBSTANTIAL_BODY_LENGTH || targetBodyLength === 0 && baseBodyLength >= SUBSTANTIAL_BODY_LENGTH) return STRUCTURAL_MISMATCH_PENALTY;
71
+ const lengthRatio = Math.min(baseBlock.content.length, targetBlock.content.length) / Math.max(baseBlock.content.length, targetBlock.content.length);
72
+ const headingDepthBonus = hasComparableHeadingDepths ? HEADING_DEPTH_MATCH_BONUS : 0;
25
73
  const typeBonus = baseBlock.type === targetBlock.type ? 2 : 0;
26
74
  const anchorSimilarity = require_docReview_computeSimilarity.computeJaccardSimilarity(baseBlock.anchorText, targetBlock.anchorText, 3);
27
- return typeBonus + (Math.min(baseBlock.content.length, targetBlock.content.length) / Math.max(baseBlock.content.length, targetBlock.content.length) > .75 ? 1 : 0) + anchorSimilarity * 8;
75
+ const lengthBonus = lengthRatio > COMPARABLE_LENGTH_RATIO ? 1 : 0;
76
+ return headingDepthBonus + typeBonus + lengthBonus + anchorSimilarity * 8;
28
77
  };
29
78
  for (let i = 1; i <= baseLength; i += 1) {
30
- scoreMatrix[i][0] = scoreMatrix[i - 1][0] + gapPenalty;
79
+ scoreMatrix[i][0] = scoreMatrix[i - 1][0] + GAP_PENALTY;
31
80
  traceMatrix[i][0] = "up";
32
81
  }
33
82
  for (let j = 1; j <= targetLength; j += 1) {
34
- scoreMatrix[0][j] = scoreMatrix[0][j - 1] + gapPenalty;
83
+ scoreMatrix[0][j] = scoreMatrix[0][j - 1] + GAP_PENALTY;
35
84
  traceMatrix[0][j] = "left";
36
85
  }
37
86
  for (let i = 1; i <= baseLength; i += 1) for (let j = 1; j <= targetLength; j += 1) {
38
87
  const match = scoreMatrix[i - 1][j - 1] + computeMatchScore(i - 1, j - 1);
39
- const deleteGap = scoreMatrix[i - 1][j] + gapPenalty;
40
- const insertGap = scoreMatrix[i][j - 1] + gapPenalty;
88
+ const deleteGap = scoreMatrix[i - 1][j] + GAP_PENALTY;
89
+ const insertGap = scoreMatrix[i][j - 1] + GAP_PENALTY;
41
90
  const best = Math.max(match, deleteGap, insertGap);
42
91
  scoreMatrix[i][j] = best;
43
92
  traceMatrix[i][j] = best === match ? "diagonal" : best === deleteGap ? "up" : "left";
@@ -1 +1 @@
1
- {"version":3,"file":"alignBlocks.cjs","names":["computeJaccardSimilarity"],"sources":["../../../src/docReview/alignBlocks.ts"],"sourcesContent":["import { computeJaccardSimilarity } from './computeSimilarity';\nimport type { AlignmentPair, FingerprintedBlock } from './types';\n\n/**\n * Align the blocks of a base document with the blocks of its translation using a\n * Needleman–Wunsch global alignment over anchor similarity and block type.\n *\n * Because prose differs across languages, the score is weighted toward the\n * structural anchor (digits and symbols) rather than the words themselves.\n *\n * @param baseBlocks - Blocks of the base (source) document.\n * @param targetBlocks - Blocks of the target (translated) document.\n * @returns The ordered list of alignment pairs, including insertions and deletions.\n */\nexport const alignBaseAndTargetBlocks = (\n baseBlocks: FingerprintedBlock[],\n targetBlocks: FingerprintedBlock[]\n): AlignmentPair[] => {\n const baseLength = baseBlocks.length;\n const targetLength = targetBlocks.length;\n\n const scoreMatrix: number[][] = Array.from({ length: baseLength + 1 }, () =>\n Array.from({ length: targetLength + 1 }, () => 0)\n );\n const traceMatrix: ('diagonal' | 'up' | 'left')[][] = Array.from(\n { length: baseLength + 1 },\n () => Array.from({ length: targetLength + 1 }, () => 'diagonal')\n );\n\n const gapPenalty = -2;\n\n const computeMatchScore = (\n baseIndex: number,\n targetIndex: number\n ): number => {\n const baseBlock = baseBlocks[baseIndex];\n const targetBlock = targetBlocks[targetIndex];\n const typeBonus = baseBlock.type === targetBlock.type ? 2 : 0;\n const anchorSimilarity = computeJaccardSimilarity(\n baseBlock.anchorText,\n targetBlock.anchorText,\n 3\n );\n const lengthRatio =\n Math.min(baseBlock.content.length, targetBlock.content.length) /\n Math.max(baseBlock.content.length, targetBlock.content.length);\n const lengthBonus = lengthRatio > 0.75 ? 1 : 0;\n return typeBonus + lengthBonus + anchorSimilarity * 8; // weighted toward anchor similarity\n };\n\n // initialize first row and column\n for (let i = 1; i <= baseLength; i += 1) {\n scoreMatrix[i][0] = scoreMatrix[i - 1][0] + gapPenalty;\n traceMatrix[i][0] = 'up';\n }\n for (let j = 1; j <= targetLength; j += 1) {\n scoreMatrix[0][j] = scoreMatrix[0][j - 1] + gapPenalty;\n traceMatrix[0][j] = 'left';\n }\n\n // fill\n for (let i = 1; i <= baseLength; i += 1) {\n for (let j = 1; j <= targetLength; j += 1) {\n const match = scoreMatrix[i - 1][j - 1] + computeMatchScore(i - 1, j - 1);\n const deleteGap = scoreMatrix[i - 1][j] + gapPenalty;\n const insertGap = scoreMatrix[i][j - 1] + gapPenalty;\n\n const best = Math.max(match, deleteGap, insertGap);\n scoreMatrix[i][j] = best;\n traceMatrix[i][j] =\n best === match ? 'diagonal' : best === deleteGap ? 'up' : 'left';\n }\n }\n\n // traceback\n const result: AlignmentPair[] = [];\n let i = baseLength;\n let j = targetLength;\n while (i > 0 || j > 0) {\n if (i > 0 && j > 0 && traceMatrix[i][j] === 'diagonal') {\n const baseIndex = i - 1;\n const targetIndex = j - 1;\n const similarityScore = computeJaccardSimilarity(\n baseBlocks[baseIndex].anchorText,\n targetBlocks[targetIndex].anchorText,\n 3\n );\n result.unshift({ baseIndex, targetIndex, similarityScore });\n i -= 1;\n j -= 1;\n } else if (i > 0 && (j === 0 || traceMatrix[i][j] === 'up')) {\n result.unshift({\n baseIndex: i - 1,\n targetIndex: null,\n similarityScore: 0,\n });\n i -= 1;\n } else if (j > 0 && (i === 0 || traceMatrix[i][j] === 'left')) {\n // target block has no corresponding base block (deleted)\n result.unshift({\n baseIndex: -1,\n targetIndex: j - 1,\n similarityScore: 0,\n });\n j -= 1;\n }\n }\n return result;\n};\n"],"mappings":";;;;;;;;;;;;;;;AAcA,MAAa,4BACX,YACA,iBACoB;CACpB,MAAM,aAAa,WAAW;CAC9B,MAAM,eAAe,aAAa;CAElC,MAAM,cAA0B,MAAM,KAAK,EAAE,QAAQ,aAAa,EAAE,SAClE,MAAM,KAAK,EAAE,QAAQ,eAAe,EAAE,SAAS,CAAC,CAClD;CACA,MAAM,cAAgD,MAAM,KAC1D,EAAE,QAAQ,aAAa,EAAE,SACnB,MAAM,KAAK,EAAE,QAAQ,eAAe,EAAE,SAAS,UAAU,CACjE;CAEA,MAAM,aAAa;CAEnB,MAAM,qBACJ,WACA,gBACW;EACX,MAAM,YAAY,WAAW;EAC7B,MAAM,cAAc,aAAa;EACjC,MAAM,YAAY,UAAU,SAAS,YAAY,OAAO,IAAI;EAC5D,MAAM,mBAAmBA,6DACvB,UAAU,YACV,YAAY,YACZ,CACF;EAKA,OAAO,aAHL,KAAK,IAAI,UAAU,QAAQ,QAAQ,YAAY,QAAQ,MAAM,IAC7D,KAAK,IAAI,UAAU,QAAQ,QAAQ,YAAY,QAAQ,MAAM,IAC7B,MAAO,IAAI,KACZ,mBAAmB;CACtD;CAGA,KAAK,IAAI,IAAI,GAAG,KAAK,YAAY,KAAK,GAAG;EACvC,YAAY,EAAE,CAAC,KAAK,YAAY,IAAI,EAAE,CAAC,KAAK;EAC5C,YAAY,EAAE,CAAC,KAAK;CACtB;CACA,KAAK,IAAI,IAAI,GAAG,KAAK,cAAc,KAAK,GAAG;EACzC,YAAY,EAAE,CAAC,KAAK,YAAY,EAAE,CAAC,IAAI,KAAK;EAC5C,YAAY,EAAE,CAAC,KAAK;CACtB;CAGA,KAAK,IAAI,IAAI,GAAG,KAAK,YAAY,KAAK,GACpC,KAAK,IAAI,IAAI,GAAG,KAAK,cAAc,KAAK,GAAG;EACzC,MAAM,QAAQ,YAAY,IAAI,EAAE,CAAC,IAAI,KAAK,kBAAkB,IAAI,GAAG,IAAI,CAAC;EACxE,MAAM,YAAY,YAAY,IAAI,EAAE,CAAC,KAAK;EAC1C,MAAM,YAAY,YAAY,EAAE,CAAC,IAAI,KAAK;EAE1C,MAAM,OAAO,KAAK,IAAI,OAAO,WAAW,SAAS;EACjD,YAAY,EAAE,CAAC,KAAK;EACpB,YAAY,EAAE,CAAC,KACb,SAAS,QAAQ,aAAa,SAAS,YAAY,OAAO;CAC9D;CAIF,MAAM,SAA0B,CAAC;CACjC,IAAI,IAAI;CACR,IAAI,IAAI;CACR,OAAO,IAAI,KAAK,IAAI,GAClB,IAAI,IAAI,KAAK,IAAI,KAAK,YAAY,EAAE,CAAC,OAAO,YAAY;EACtD,MAAM,YAAY,IAAI;EACtB,MAAM,cAAc,IAAI;EACxB,MAAM,kBAAkBA,6DACtB,WAAW,UAAU,CAAC,YACtB,aAAa,YAAY,CAAC,YAC1B,CACF;EACA,OAAO,QAAQ;GAAE;GAAW;GAAa;EAAgB,CAAC;EAC1D,KAAK;EACL,KAAK;CACP,OAAO,IAAI,IAAI,MAAM,MAAM,KAAK,YAAY,EAAE,CAAC,OAAO,OAAO;EAC3D,OAAO,QAAQ;GACb,WAAW,IAAI;GACf,aAAa;GACb,iBAAiB;EACnB,CAAC;EACD,KAAK;CACP,OAAO,IAAI,IAAI,MAAM,MAAM,KAAK,YAAY,EAAE,CAAC,OAAO,SAAS;EAE7D,OAAO,QAAQ;GACb,WAAW;GACX,aAAa,IAAI;GACjB,iBAAiB;EACnB,CAAC;EACD,KAAK;CACP;CAEF,OAAO;AACT"}
1
+ {"version":3,"file":"alignBlocks.cjs","names":["computeJaccardSimilarity"],"sources":["../../../src/docReview/alignBlocks.ts"],"sourcesContent":["import { computeJaccardSimilarity } from './computeSimilarity';\nimport type { AlignmentPair, FingerprintedBlock } from './types';\n\n/** Cost of leaving a block unaligned (an insertion or a deletion). */\nconst GAP_PENALTY = -2;\n\n/**\n * Score of a pair that structural evidence rules out.\n *\n * Strictly below the cost of two gaps (`2 * GAP_PENALTY`) so the aligner always\n * prefers reporting an insertion plus a deletion over such a pair. Every other\n * score is positive, which means that without an explicit veto the aligner would\n * rather pair two unrelated blocks than leave them unaligned — and a bogus pair\n * is planned as `reuse`, which keeps the stale translation verbatim *and*\n * inserts the freshly translated base block next to it, producing the duplicated\n * headings this penalty exists to prevent.\n */\nconst STRUCTURAL_MISMATCH_PENALTY = GAP_PENALTY * 2 - 1;\n\n/** Reward for two blocks opened by a heading of the very same depth. */\nconst HEADING_DEPTH_MATCH_BONUS = 3;\n\n/** Minimum length ratio for the (small) \"comparable size\" reward. */\nconst COMPARABLE_LENGTH_RATIO = 0.75;\n\n/**\n * Body length from which a section is considered to carry content of its own.\n *\n * Kept above a single sentence so a translator's short lead-in paragraph is not\n * mistaken for a whole section body.\n */\nconst SUBSTANTIAL_BODY_LENGTH = 80;\n\n/**\n * Length of the body a block carries below its opening heading.\n *\n * A section that only holds its heading (because subsections carry all of its\n * content) is structurally different from one that holds a full body, and that\n * difference survives translation.\n *\n * @param block - The block to measure.\n * @returns The trimmed length of everything below the opening heading line.\n */\nconst measureBodyLength = (block: FingerprintedBlock): number => {\n if (block.headingDepth === null) return block.content.trim().length;\n\n const [, ...bodyLines] = block.content.split('\\n');\n\n return bodyLines.join('\\n').trim().length;\n};\n\n/**\n * Align the blocks of a base document with the blocks of its translation using a\n * Needleman–Wunsch global alignment over heading depth, anchor similarity and\n * block type.\n *\n * Because prose differs across languages, the score is weighted toward the\n * structural signals — heading depth first, then the anchor (digits and symbols)\n * — rather than the words themselves.\n *\n * @param baseBlocks - Blocks of the base (source) document.\n * @param targetBlocks - Blocks of the target (translated) document.\n * @returns The ordered list of alignment pairs, including insertions and deletions.\n */\nexport const alignBaseAndTargetBlocks = (\n baseBlocks: FingerprintedBlock[],\n targetBlocks: FingerprintedBlock[]\n): AlignmentPair[] => {\n const baseLength = baseBlocks.length;\n const targetLength = targetBlocks.length;\n\n const scoreMatrix: number[][] = Array.from({ length: baseLength + 1 }, () =>\n Array.from({ length: targetLength + 1 }, () => 0)\n );\n const traceMatrix: ('diagonal' | 'up' | 'left')[][] = Array.from(\n { length: baseLength + 1 },\n () => Array.from({ length: targetLength + 1 }, () => 'diagonal')\n );\n\n const computeMatchScore = (\n baseIndex: number,\n targetIndex: number\n ): number => {\n const baseBlock = baseBlocks[baseIndex]!;\n const targetBlock = targetBlocks[targetIndex]!;\n\n // Translating a document never changes its heading depths, so two headings\n // of different depths cannot be counterparts, however similar their content.\n const hasComparableHeadingDepths =\n baseBlock.headingDepth !== null && targetBlock.headingDepth !== null;\n\n if (\n hasComparableHeadingDepths &&\n baseBlock.headingDepth !== targetBlock.headingDepth\n ) {\n return STRUCTURAL_MISMATCH_PENALTY;\n }\n\n // A section reduced to its bare heading (its content living in subsections)\n // is not the translation of a section holding a full body. Pairing them\n // reuses that whole body while the base heading is translated again right\n // next to it — which is exactly how a duplicated heading appears.\n const baseBodyLength = measureBodyLength(baseBlock);\n const targetBodyLength = measureBodyLength(targetBlock);\n const isBodyPresenceMismatched =\n (baseBodyLength === 0 && targetBodyLength >= SUBSTANTIAL_BODY_LENGTH) ||\n (targetBodyLength === 0 && baseBodyLength >= SUBSTANTIAL_BODY_LENGTH);\n\n if (isBodyPresenceMismatched) return STRUCTURAL_MISMATCH_PENALTY;\n\n const lengthRatio =\n Math.min(baseBlock.content.length, targetBlock.content.length) /\n Math.max(baseBlock.content.length, targetBlock.content.length);\n\n const headingDepthBonus = hasComparableHeadingDepths\n ? HEADING_DEPTH_MATCH_BONUS\n : 0;\n const typeBonus = baseBlock.type === targetBlock.type ? 2 : 0;\n const anchorSimilarity = computeJaccardSimilarity(\n baseBlock.anchorText,\n targetBlock.anchorText,\n 3\n );\n const lengthBonus = lengthRatio > COMPARABLE_LENGTH_RATIO ? 1 : 0;\n\n // weighted toward the structural signals (heading depth, then anchor)\n return headingDepthBonus + typeBonus + lengthBonus + anchorSimilarity * 8;\n };\n\n // initialize first row and column\n for (let i = 1; i <= baseLength; i += 1) {\n scoreMatrix[i][0] = scoreMatrix[i - 1][0] + GAP_PENALTY;\n traceMatrix[i][0] = 'up';\n }\n for (let j = 1; j <= targetLength; j += 1) {\n scoreMatrix[0][j] = scoreMatrix[0][j - 1] + GAP_PENALTY;\n traceMatrix[0][j] = 'left';\n }\n\n // fill\n for (let i = 1; i <= baseLength; i += 1) {\n for (let j = 1; j <= targetLength; j += 1) {\n const match = scoreMatrix[i - 1][j - 1] + computeMatchScore(i - 1, j - 1);\n const deleteGap = scoreMatrix[i - 1][j] + GAP_PENALTY;\n const insertGap = scoreMatrix[i][j - 1] + GAP_PENALTY;\n\n const best = Math.max(match, deleteGap, insertGap);\n scoreMatrix[i][j] = best;\n traceMatrix[i][j] =\n best === match ? 'diagonal' : best === deleteGap ? 'up' : 'left';\n }\n }\n\n // traceback\n const result: AlignmentPair[] = [];\n let i = baseLength;\n let j = targetLength;\n while (i > 0 || j > 0) {\n if (i > 0 && j > 0 && traceMatrix[i][j] === 'diagonal') {\n const baseIndex = i - 1;\n const targetIndex = j - 1;\n const similarityScore = computeJaccardSimilarity(\n baseBlocks[baseIndex].anchorText,\n targetBlocks[targetIndex].anchorText,\n 3\n );\n result.unshift({ baseIndex, targetIndex, similarityScore });\n i -= 1;\n j -= 1;\n } else if (i > 0 && (j === 0 || traceMatrix[i][j] === 'up')) {\n result.unshift({\n baseIndex: i - 1,\n targetIndex: null,\n similarityScore: 0,\n });\n i -= 1;\n } else if (j > 0 && (i === 0 || traceMatrix[i][j] === 'left')) {\n // target block has no corresponding base block (deleted)\n result.unshift({\n baseIndex: -1,\n targetIndex: j - 1,\n similarityScore: 0,\n });\n j -= 1;\n }\n }\n return result;\n};\n"],"mappings":";;;;;AAIA,MAAM,cAAc;;;;;;;;;;;;AAapB,MAAM,8BAA8B,cAAc,IAAI;;AAGtD,MAAM,4BAA4B;;AAGlC,MAAM,0BAA0B;;;;;;;AAQhC,MAAM,0BAA0B;;;;;;;;;;;AAYhC,MAAM,qBAAqB,UAAsC;CAC/D,IAAI,MAAM,iBAAiB,MAAM,OAAO,MAAM,QAAQ,KAAK,CAAC,CAAC;CAE7D,MAAM,GAAG,GAAG,aAAa,MAAM,QAAQ,MAAM,IAAI;CAEjD,OAAO,UAAU,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC;AACrC;;;;;;;;;;;;;;AAeA,MAAa,4BACX,YACA,iBACoB;CACpB,MAAM,aAAa,WAAW;CAC9B,MAAM,eAAe,aAAa;CAElC,MAAM,cAA0B,MAAM,KAAK,EAAE,QAAQ,aAAa,EAAE,SAClE,MAAM,KAAK,EAAE,QAAQ,eAAe,EAAE,SAAS,CAAC,CAClD;CACA,MAAM,cAAgD,MAAM,KAC1D,EAAE,QAAQ,aAAa,EAAE,SACnB,MAAM,KAAK,EAAE,QAAQ,eAAe,EAAE,SAAS,UAAU,CACjE;CAEA,MAAM,qBACJ,WACA,gBACW;EACX,MAAM,YAAY,WAAW;EAC7B,MAAM,cAAc,aAAa;EAIjC,MAAM,6BACJ,UAAU,iBAAiB,QAAQ,YAAY,iBAAiB;EAElE,IACE,8BACA,UAAU,iBAAiB,YAAY,cAEvC,OAAO;EAOT,MAAM,iBAAiB,kBAAkB,SAAS;EAClD,MAAM,mBAAmB,kBAAkB,WAAW;EAKtD,IAHG,mBAAmB,KAAK,oBAAoB,2BAC5C,qBAAqB,KAAK,kBAAkB,yBAEjB,OAAO;EAErC,MAAM,cACJ,KAAK,IAAI,UAAU,QAAQ,QAAQ,YAAY,QAAQ,MAAM,IAC7D,KAAK,IAAI,UAAU,QAAQ,QAAQ,YAAY,QAAQ,MAAM;EAE/D,MAAM,oBAAoB,6BACtB,4BACA;EACJ,MAAM,YAAY,UAAU,SAAS,YAAY,OAAO,IAAI;EAC5D,MAAM,mBAAmBA,6DACvB,UAAU,YACV,YAAY,YACZ,CACF;EACA,MAAM,cAAc,cAAc,0BAA0B,IAAI;EAGhE,OAAO,oBAAoB,YAAY,cAAc,mBAAmB;CAC1E;CAGA,KAAK,IAAI,IAAI,GAAG,KAAK,YAAY,KAAK,GAAG;EACvC,YAAY,EAAE,CAAC,KAAK,YAAY,IAAI,EAAE,CAAC,KAAK;EAC5C,YAAY,EAAE,CAAC,KAAK;CACtB;CACA,KAAK,IAAI,IAAI,GAAG,KAAK,cAAc,KAAK,GAAG;EACzC,YAAY,EAAE,CAAC,KAAK,YAAY,EAAE,CAAC,IAAI,KAAK;EAC5C,YAAY,EAAE,CAAC,KAAK;CACtB;CAGA,KAAK,IAAI,IAAI,GAAG,KAAK,YAAY,KAAK,GACpC,KAAK,IAAI,IAAI,GAAG,KAAK,cAAc,KAAK,GAAG;EACzC,MAAM,QAAQ,YAAY,IAAI,EAAE,CAAC,IAAI,KAAK,kBAAkB,IAAI,GAAG,IAAI,CAAC;EACxE,MAAM,YAAY,YAAY,IAAI,EAAE,CAAC,KAAK;EAC1C,MAAM,YAAY,YAAY,EAAE,CAAC,IAAI,KAAK;EAE1C,MAAM,OAAO,KAAK,IAAI,OAAO,WAAW,SAAS;EACjD,YAAY,EAAE,CAAC,KAAK;EACpB,YAAY,EAAE,CAAC,KACb,SAAS,QAAQ,aAAa,SAAS,YAAY,OAAO;CAC9D;CAIF,MAAM,SAA0B,CAAC;CACjC,IAAI,IAAI;CACR,IAAI,IAAI;CACR,OAAO,IAAI,KAAK,IAAI,GAClB,IAAI,IAAI,KAAK,IAAI,KAAK,YAAY,EAAE,CAAC,OAAO,YAAY;EACtD,MAAM,YAAY,IAAI;EACtB,MAAM,cAAc,IAAI;EACxB,MAAM,kBAAkBA,6DACtB,WAAW,UAAU,CAAC,YACtB,aAAa,YAAY,CAAC,YAC1B,CACF;EACA,OAAO,QAAQ;GAAE;GAAW;GAAa;EAAgB,CAAC;EAC1D,KAAK;EACL,KAAK;CACP,OAAO,IAAI,IAAI,MAAM,MAAM,KAAK,YAAY,EAAE,CAAC,OAAO,OAAO;EAC3D,OAAO,QAAQ;GACb,WAAW,IAAI;GACf,aAAa;GACb,iBAAiB;EACnB,CAAC;EACD,KAAK;CACP,OAAO,IAAI,IAAI,MAAM,MAAM,KAAK,YAAY,EAAE,CAAC,OAAO,SAAS;EAE7D,OAAO,QAAQ;GACb,WAAW;GACX,aAAa,IAAI;GACjB,iBAAiB;EACnB,CAAC;EACD,KAAK;CACP;CAEF,OAAO;AACT"}
@@ -31,6 +31,18 @@ const identifySegmentsToReview = ({ baseBlocks, targetBlocks, plan }) => {
31
31
  });
32
32
  return { segmentsToReview };
33
33
  };
34
+ /** Markdown separates two blocks with a blank line. */
35
+ const endsWithBlockBoundary = (text) => /\n[ \t]*\n$/.test(text);
36
+ /**
37
+ * The newlines missing at the end of `text` for it to close a markdown block.
38
+ *
39
+ * @param text - The document built so far.
40
+ * @returns `''`, `'\n'` or `'\n\n'` depending on how the text already ends.
41
+ */
42
+ const buildBlockSeparator = (text) => {
43
+ if (text.length === 0 || endsWithBlockBoundary(text)) return "";
44
+ return text.endsWith("\n") ? "\n" : "\n\n";
45
+ };
34
46
  /**
35
47
  * Merge reviewed translations back into the final document following the
36
48
  * alignment plan, reusing untouched target blocks as-is.
@@ -41,22 +53,38 @@ const identifySegmentsToReview = ({ baseBlocks, targetBlocks, plan }) => {
41
53
  * @returns The rebuilt target document.
42
54
  */
43
55
  const mergeReviewedSegments = (plan, targetBlocks, reviewedSegments) => {
44
- const outputParts = [];
56
+ let mergedText = "";
57
+ let previousPartWasGenerated = false;
58
+ /**
59
+ * Append one part, keeping a blank line around the content this run produced.
60
+ *
61
+ * Blocks own the blank lines that trail them, so two verbatim blocks already
62
+ * separate themselves and are appended untouched — a document with nothing to
63
+ * change merges back byte for byte. Generated content is the exception: the
64
+ * block it lands after may be the one that used to end the document, which
65
+ * carries no trailing blank line, and the two would be glued into a single
66
+ * markdown block (a heading swallowed by the paragraph above it).
67
+ */
68
+ const appendPart = (content, isGenerated) => {
69
+ if (content.length === 0) return;
70
+ if (isGenerated || previousPartWasGenerated) mergedText += buildBlockSeparator(mergedText);
71
+ mergedText += content;
72
+ previousPartWasGenerated = isGenerated;
73
+ };
45
74
  plan.actions.forEach((action, actionIndex) => {
46
- if (action.kind === "reuse") outputParts.push(targetBlocks[action.targetIndex].content);
75
+ if (action.kind === "reuse") appendPart(targetBlocks[action.targetIndex].content, false);
47
76
  else if (action.kind === "review" || action.kind === "insert_new") {
48
77
  const reviewedContent = reviewedSegments.get(actionIndex);
49
- if (reviewedContent !== void 0) outputParts.push(reviewedContent);
50
- else if (action.kind === "review" && action.targetIndex !== null) outputParts.push(targetBlocks[action.targetIndex].content);
51
- else outputParts.push("\n");
78
+ if (reviewedContent !== void 0) appendPart(reviewedContent, true);
79
+ else if (action.kind === "review" && action.targetIndex !== null) appendPart(targetBlocks[action.targetIndex].content, false);
80
+ else appendPart("\n", false);
52
81
  } else if (action.kind === "delete") {
53
82
  const reviewedContent = reviewedSegments.get(actionIndex);
54
- if (reviewedContent !== void 0) {
55
- if (reviewedContent) outputParts.push(reviewedContent);
56
- } else outputParts.push(targetBlocks[action.targetIndex].content);
83
+ if (reviewedContent !== void 0) appendPart(reviewedContent, true);
84
+ else appendPart(targetBlocks[action.targetIndex].content, false);
57
85
  }
58
86
  });
59
- return outputParts.join("");
87
+ return mergedText;
60
88
  };
61
89
 
62
90
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"rebuildDocument.cjs","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/**\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 const outputParts: string[] = [];\n\n plan.actions.forEach((action, actionIndex) => {\n if (action.kind === 'reuse') {\n outputParts.push(targetBlocks[action.targetIndex].content);\n } else if (action.kind === 'review' || action.kind === 'insert_new') {\n const reviewedContent = reviewedSegments.get(actionIndex);\n\n if (reviewedContent !== undefined) {\n outputParts.push(reviewedContent);\n } else {\n // Fallback: if review failed, use existing or blank\n if (action.kind === 'review' && action.targetIndex !== null) {\n outputParts.push(targetBlocks[action.targetIndex].content);\n } else {\n outputParts.push('\\n');\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 if (reviewedContent) outputParts.push(reviewedContent);\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 outputParts.push(targetBlocks[action.targetIndex].content);\n }\n }\n });\n\n return outputParts.join('');\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;;;;;;;;;;AAWA,MAAa,yBACX,MACA,cACA,qBACW;CACX,MAAM,cAAwB,CAAC;CAE/B,KAAK,QAAQ,SAAS,QAAQ,gBAAgB;EAC5C,IAAI,OAAO,SAAS,SAClB,YAAY,KAAK,aAAa,OAAO,YAAY,CAAC,OAAO;OACpD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,cAAc;GACnE,MAAM,kBAAkB,iBAAiB,IAAI,WAAW;GAExD,IAAI,oBAAoB,QACtB,YAAY,KAAK,eAAe;QAGhC,IAAI,OAAO,SAAS,YAAY,OAAO,gBAAgB,MACrD,YAAY,KAAK,aAAa,OAAO,YAAY,CAAC,OAAO;QAEzD,YAAY,KAAK,IAAI;EAG3B,OAAO,IAAI,OAAO,SAAS,UAAU;GACnC,MAAM,kBAAkB,iBAAiB,IAAI,WAAW;GACxD,IAAI,oBAAoB,QAGtB;QAAI,iBAAiB,YAAY,KAAK,eAAe;GAAC,OAKtD,YAAY,KAAK,aAAa,OAAO,YAAY,CAAC,OAAO;EAE7D;CACF,CAAC;CAED,OAAO,YAAY,KAAK,EAAE;AAC5B"}
1
+ {"version":3,"file":"rebuildDocument.cjs","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"}
@@ -1,9 +1,19 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
 
3
3
  //#region src/docReview/segmentDocument.ts
4
+ const HEADING_PATTERN = /^\s*(#{1,6})\s+/;
4
5
  const isBlankLine = (line) => line.trim().length === 0;
5
6
  const isFencedCodeDelimiter = (line) => /^\s*```/.test(line);
6
- const isHeading = (line) => /^\s*#{1,6}\s+/.test(line);
7
+ /**
8
+ * Read the depth of an ATX markdown heading (`#` → 1, `######` → 6).
9
+ *
10
+ * @param line - The line to inspect.
11
+ * @returns The heading depth, or `null` when the line is not a heading.
12
+ */
13
+ const parseHeadingDepth = (line) => {
14
+ return HEADING_PATTERN.exec(line)?.[1]?.length ?? null;
15
+ };
16
+ const isHeading = (line) => parseHeadingDepth(line) !== null;
7
17
  const isFrontmatterDelimiter = (line) => /^\s*---\s*$/.test(line);
8
18
  /**
9
19
  * Split a markdown document into fine-grained blocks.
@@ -37,6 +47,7 @@ const segmentDocument = (text) => {
37
47
  if (index < lineCount) index += 1;
38
48
  units.push({
39
49
  type: "unknown",
50
+ headingDepth: null,
40
51
  startIndex,
41
52
  endIndex: index - 1
42
53
  });
@@ -49,14 +60,17 @@ const segmentDocument = (text) => {
49
60
  if (index < lineCount) index += 1;
50
61
  units.push({
51
62
  type: "code_block",
63
+ headingDepth: null,
52
64
  startIndex,
53
65
  endIndex: index - 1
54
66
  });
55
67
  continue;
56
68
  }
57
- if (isHeading(currentLine)) {
69
+ const headingDepth = parseHeadingDepth(currentLine);
70
+ if (headingDepth !== null) {
58
71
  units.push({
59
72
  type: "heading",
73
+ headingDepth,
60
74
  startIndex: index,
61
75
  endIndex: index
62
76
  });
@@ -67,6 +81,7 @@ const segmentDocument = (text) => {
67
81
  while (index < lineCount && !isBlankLine(lines[index]) && !isHeading(lines[index]) && !isFencedCodeDelimiter(lines[index])) index += 1;
68
82
  units.push({
69
83
  type: "paragraph",
84
+ headingDepth: null,
70
85
  startIndex,
71
86
  endIndex: index - 1
72
87
  });
@@ -80,6 +95,7 @@ const segmentDocument = (text) => {
80
95
  return {
81
96
  type: unit.type,
82
97
  content,
98
+ headingDepth: unit.headingDepth,
83
99
  lineStart: blockStartIndex + 1,
84
100
  lineEnd: blockEndIndex + 1
85
101
  };
@@ -114,6 +130,7 @@ const segmentSections = (text) => {
114
130
  sections.push({
115
131
  type: firstBlock.type,
116
132
  content: currentBlocks.map((block) => block.content).join(""),
133
+ headingDepth: firstBlock.headingDepth,
117
134
  lineStart: firstBlock.lineStart,
118
135
  lineEnd: lastBlock.lineEnd
119
136
  });
@@ -1 +1 @@
1
- {"version":3,"file":"segmentDocument.cjs","names":[],"sources":["../../../src/docReview/segmentDocument.ts"],"sourcesContent":["import type { Block, BlockType } from './types';\n\nconst isBlankLine = (line: string): boolean => line.trim().length === 0;\nconst isFencedCodeDelimiter = (line: string): boolean => /^\\s*```/.test(line);\nconst isHeading = (line: string): boolean => /^\\s*#{1,6}\\s+/.test(line);\nconst isFrontmatterDelimiter = (line: string): boolean =>\n /^\\s*---\\s*$/.test(line);\n\n/**\n * A content unit (heading, paragraph, code block or frontmatter) spanning a\n * 0-based, inclusive line range. Blank-line runs are not units of their own;\n * they are folded into the preceding unit as trailing separators (see\n * {@link segmentDocument}) so that the concatenation of every block's content\n * reproduces the document byte-for-byte.\n */\ntype ContentUnit = {\n type: BlockType;\n startIndex: number;\n endIndex: number;\n};\n\n/**\n * Split a markdown document into fine-grained blocks.\n *\n * Boundaries are drawn at headings, blank lines (paragraph breaks) and fenced\n * code blocks, while frontmatter and the inside of code fences are kept intact.\n * Each blank-line run is appended to the block that precedes it, so the blocks\n * form an exact partition of the document: concatenating every `content` (in\n * order) yields the original text unchanged. This is what lets the block-aware\n * review re-translate only the paragraphs/snippets that actually changed instead\n * of the whole heading section.\n *\n * @param text - The full markdown document.\n * @returns The ordered list of blocks with their 1-based line ranges.\n */\nexport const segmentDocument = (text: string): Block[] => {\n const lines = text.split('\\n');\n const lineCount = lines.length;\n\n // 1. Tokenize into content units, skipping blank-line runs (folded in below).\n const units: ContentUnit[] = [];\n let index = 0;\n\n while (index < lineCount) {\n const currentLine = lines[index];\n\n if (isBlankLine(currentLine)) {\n index += 1;\n continue;\n }\n\n // Frontmatter: only when it opens the document.\n if (units.length === 0 && isFrontmatterDelimiter(currentLine)) {\n const startIndex = index;\n index += 1;\n while (index < lineCount && !isFrontmatterDelimiter(lines[index])) {\n index += 1;\n }\n // Include the closing delimiter when present.\n if (index < lineCount) index += 1;\n units.push({ type: 'unknown', startIndex, endIndex: index - 1 });\n continue;\n }\n\n // Fenced code block: consumed whole so inner blank lines and `#` lines are\n // never treated as boundaries.\n if (isFencedCodeDelimiter(currentLine)) {\n const startIndex = index;\n index += 1;\n while (index < lineCount && !isFencedCodeDelimiter(lines[index])) {\n index += 1;\n }\n // Include the closing fence when present.\n if (index < lineCount) index += 1;\n units.push({ type: 'code_block', startIndex, endIndex: index - 1 });\n continue;\n }\n\n // Heading: a single self-contained line.\n if (isHeading(currentLine)) {\n units.push({ type: 'heading', startIndex: index, endIndex: index });\n index += 1;\n continue;\n }\n\n // Paragraph: a run of consecutive lines until a blank line, a heading or a\n // code fence. Tables and tight lists stay together (no blank line between\n // their rows/items).\n const startIndex = index;\n while (\n index < lineCount &&\n !isBlankLine(lines[index]) &&\n !isHeading(lines[index]) &&\n !isFencedCodeDelimiter(lines[index])\n ) {\n index += 1;\n }\n units.push({ type: 'paragraph', startIndex, endIndex: index - 1 });\n }\n\n if (units.length === 0) return [];\n\n // 2. Turn each unit into a block whose line range extends to just before the\n // next unit, so the trailing blank-line run is owned by it. The first block\n // also absorbs any leading blank lines, and the last block runs to EOF.\n return units.map((unit, unitIndex): Block => {\n const blockStartIndex = unitIndex === 0 ? 0 : unit.startIndex;\n const blockEndIndex =\n unitIndex === units.length - 1\n ? lineCount - 1\n : units[unitIndex + 1].startIndex - 1;\n\n const blockLines = lines.slice(blockStartIndex, blockEndIndex + 1);\n // Re-append the boundary newline dropped by `split` for every block but the\n // one ending at EOF, so concatenating all blocks rebuilds the document.\n const content =\n blockEndIndex < lineCount - 1\n ? `${blockLines.join('\\n')}\\n`\n : blockLines.join('\\n');\n\n return {\n type: unit.type,\n content,\n lineStart: blockStartIndex + 1,\n lineEnd: blockEndIndex + 1,\n };\n });\n};\n\n/**\n * Split a markdown document into coarse, heading-anchored sections.\n *\n * Built by grouping the fine blocks of {@link segmentDocument}: frontmatter and\n * each heading open a new section, and the following non-heading blocks are\n * folded into it. Because it only concatenates adjacent fine blocks, the result\n * is still an exact partition of the document (sections concatenate back to the\n * source unchanged).\n *\n * Sections are the robust alignment unit between a base document and its\n * translation — both share the same heading structure, so they align almost\n * perfectly and a translation that splits its prose into a different number of\n * paragraphs never causes a section to be dropped. Fine-grained review happens\n * within a section once it is known to have changed.\n *\n * @param text - The full markdown document.\n * @returns The ordered list of sections with their 1-based line ranges.\n */\nexport const segmentSections = (text: string): Block[] => {\n const fineBlocks = segmentDocument(text);\n const sections: Block[] = [];\n let currentBlocks: Block[] = [];\n\n const flushSection = (): void => {\n if (currentBlocks.length === 0) return;\n\n const [firstBlock] = currentBlocks;\n const lastBlock = currentBlocks[currentBlocks.length - 1];\n\n sections.push({\n type: firstBlock.type,\n content: currentBlocks.map((block) => block.content).join(''),\n lineStart: firstBlock.lineStart,\n lineEnd: lastBlock.lineEnd,\n });\n currentBlocks = [];\n };\n\n for (const block of fineBlocks) {\n // Frontmatter (a leading `unknown` block) and every heading open a section.\n const opensSection =\n block.type === 'heading' ||\n (block.type === 'unknown' && sections.length === 0);\n\n if (opensSection) flushSection();\n currentBlocks.push(block);\n if (block.type === 'unknown' && sections.length === 0) flushSection();\n }\n\n flushSection();\n\n return sections;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"segmentDocument.cjs","names":[],"sources":["../../../src/docReview/segmentDocument.ts"],"sourcesContent":["import type { Block, BlockType } from './types';\n\nconst HEADING_PATTERN = /^\\s*(#{1,6})\\s+/;\n\nconst isBlankLine = (line: string): boolean => line.trim().length === 0;\nconst isFencedCodeDelimiter = (line: string): boolean => /^\\s*```/.test(line);\n\n/**\n * Read the depth of an ATX markdown heading (`#` → 1, `######` → 6).\n *\n * @param line - The line to inspect.\n * @returns The heading depth, or `null` when the line is not a heading.\n */\nconst parseHeadingDepth = (line: string): number | null => {\n const match = HEADING_PATTERN.exec(line);\n\n return match?.[1]?.length ?? null;\n};\n\nconst isHeading = (line: string): boolean => parseHeadingDepth(line) !== null;\nconst isFrontmatterDelimiter = (line: string): boolean =>\n /^\\s*---\\s*$/.test(line);\n\n/**\n * A content unit (heading, paragraph, code block or frontmatter) spanning a\n * 0-based, inclusive line range. Blank-line runs are not units of their own;\n * they are folded into the preceding unit as trailing separators (see\n * {@link segmentDocument}) so that the concatenation of every block's content\n * reproduces the document byte-for-byte.\n */\ntype ContentUnit = {\n type: BlockType;\n /** Depth of the ATX heading opening the unit, `null` when it is not a heading. */\n headingDepth: number | null;\n startIndex: number;\n endIndex: number;\n};\n\n/**\n * Split a markdown document into fine-grained blocks.\n *\n * Boundaries are drawn at headings, blank lines (paragraph breaks) and fenced\n * code blocks, while frontmatter and the inside of code fences are kept intact.\n * Each blank-line run is appended to the block that precedes it, so the blocks\n * form an exact partition of the document: concatenating every `content` (in\n * order) yields the original text unchanged. This is what lets the block-aware\n * review re-translate only the paragraphs/snippets that actually changed instead\n * of the whole heading section.\n *\n * @param text - The full markdown document.\n * @returns The ordered list of blocks with their 1-based line ranges.\n */\nexport const segmentDocument = (text: string): Block[] => {\n const lines = text.split('\\n');\n const lineCount = lines.length;\n\n // 1. Tokenize into content units, skipping blank-line runs (folded in below).\n const units: ContentUnit[] = [];\n let index = 0;\n\n while (index < lineCount) {\n const currentLine = lines[index];\n\n if (isBlankLine(currentLine)) {\n index += 1;\n continue;\n }\n\n // Frontmatter: only when it opens the document.\n if (units.length === 0 && isFrontmatterDelimiter(currentLine)) {\n const startIndex = index;\n index += 1;\n while (index < lineCount && !isFrontmatterDelimiter(lines[index])) {\n index += 1;\n }\n // Include the closing delimiter when present.\n if (index < lineCount) index += 1;\n units.push({\n type: 'unknown',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n continue;\n }\n\n // Fenced code block: consumed whole so inner blank lines and `#` lines are\n // never treated as boundaries.\n if (isFencedCodeDelimiter(currentLine)) {\n const startIndex = index;\n index += 1;\n while (index < lineCount && !isFencedCodeDelimiter(lines[index])) {\n index += 1;\n }\n // Include the closing fence when present.\n if (index < lineCount) index += 1;\n units.push({\n type: 'code_block',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n continue;\n }\n\n // Heading: a single self-contained line.\n const headingDepth = parseHeadingDepth(currentLine);\n\n if (headingDepth !== null) {\n units.push({\n type: 'heading',\n headingDepth,\n startIndex: index,\n endIndex: index,\n });\n index += 1;\n continue;\n }\n\n // Paragraph: a run of consecutive lines until a blank line, a heading or a\n // code fence. Tables and tight lists stay together (no blank line between\n // their rows/items).\n const startIndex = index;\n while (\n index < lineCount &&\n !isBlankLine(lines[index]) &&\n !isHeading(lines[index]) &&\n !isFencedCodeDelimiter(lines[index])\n ) {\n index += 1;\n }\n units.push({\n type: 'paragraph',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n }\n\n if (units.length === 0) return [];\n\n // 2. Turn each unit into a block whose line range extends to just before the\n // next unit, so the trailing blank-line run is owned by it. The first block\n // also absorbs any leading blank lines, and the last block runs to EOF.\n return units.map((unit, unitIndex): Block => {\n const blockStartIndex = unitIndex === 0 ? 0 : unit.startIndex;\n const blockEndIndex =\n unitIndex === units.length - 1\n ? lineCount - 1\n : units[unitIndex + 1].startIndex - 1;\n\n const blockLines = lines.slice(blockStartIndex, blockEndIndex + 1);\n // Re-append the boundary newline dropped by `split` for every block but the\n // one ending at EOF, so concatenating all blocks rebuilds the document.\n const content =\n blockEndIndex < lineCount - 1\n ? `${blockLines.join('\\n')}\\n`\n : blockLines.join('\\n');\n\n return {\n type: unit.type,\n content,\n headingDepth: unit.headingDepth,\n lineStart: blockStartIndex + 1,\n lineEnd: blockEndIndex + 1,\n };\n });\n};\n\n/**\n * Split a markdown document into coarse, heading-anchored sections.\n *\n * Built by grouping the fine blocks of {@link segmentDocument}: frontmatter and\n * each heading open a new section, and the following non-heading blocks are\n * folded into it. Because it only concatenates adjacent fine blocks, the result\n * is still an exact partition of the document (sections concatenate back to the\n * source unchanged).\n *\n * Sections are the robust alignment unit between a base document and its\n * translation — both share the same heading structure, so they align almost\n * perfectly and a translation that splits its prose into a different number of\n * paragraphs never causes a section to be dropped. Fine-grained review happens\n * within a section once it is known to have changed.\n *\n * @param text - The full markdown document.\n * @returns The ordered list of sections with their 1-based line ranges.\n */\nexport const segmentSections = (text: string): Block[] => {\n const fineBlocks = segmentDocument(text);\n const sections: Block[] = [];\n let currentBlocks: Block[] = [];\n\n const flushSection = (): void => {\n if (currentBlocks.length === 0) return;\n\n const [firstBlock] = currentBlocks;\n const lastBlock = currentBlocks[currentBlocks.length - 1];\n\n sections.push({\n type: firstBlock.type,\n content: currentBlocks.map((block) => block.content).join(''),\n // A section is identified by the heading that opens it, so it inherits its\n // depth — this is what keeps a `##` section from aligning with a `###` one.\n headingDepth: firstBlock.headingDepth,\n lineStart: firstBlock.lineStart,\n lineEnd: lastBlock.lineEnd,\n });\n currentBlocks = [];\n };\n\n for (const block of fineBlocks) {\n // Frontmatter (a leading `unknown` block) and every heading open a section.\n const opensSection =\n block.type === 'heading' ||\n (block.type === 'unknown' && sections.length === 0);\n\n if (opensSection) flushSection();\n currentBlocks.push(block);\n if (block.type === 'unknown' && sections.length === 0) flushSection();\n }\n\n flushSection();\n\n return sections;\n};\n"],"mappings":";;;AAEA,MAAM,kBAAkB;AAExB,MAAM,eAAe,SAA0B,KAAK,KAAK,CAAC,CAAC,WAAW;AACtE,MAAM,yBAAyB,SAA0B,UAAU,KAAK,IAAI;;;;;;;AAQ5E,MAAM,qBAAqB,SAAgC;CAGzD,OAFc,gBAAgB,KAAK,IAExB,CAAC,GAAG,EAAE,EAAE,UAAU;AAC/B;AAEA,MAAM,aAAa,SAA0B,kBAAkB,IAAI,MAAM;AACzE,MAAM,0BAA0B,SAC9B,cAAc,KAAK,IAAI;;;;;;;;;;;;;;;AA+BzB,MAAa,mBAAmB,SAA0B;CACxD,MAAM,QAAQ,KAAK,MAAM,IAAI;CAC7B,MAAM,YAAY,MAAM;CAGxB,MAAM,QAAuB,CAAC;CAC9B,IAAI,QAAQ;CAEZ,OAAO,QAAQ,WAAW;EACxB,MAAM,cAAc,MAAM;EAE1B,IAAI,YAAY,WAAW,GAAG;GAC5B,SAAS;GACT;EACF;EAGA,IAAI,MAAM,WAAW,KAAK,uBAAuB,WAAW,GAAG;GAC7D,MAAM,aAAa;GACnB,SAAS;GACT,OAAO,QAAQ,aAAa,CAAC,uBAAuB,MAAM,MAAM,GAC9D,SAAS;GAGX,IAAI,QAAQ,WAAW,SAAS;GAChC,MAAM,KAAK;IACT,MAAM;IACN,cAAc;IACd;IACA,UAAU,QAAQ;GACpB,CAAC;GACD;EACF;EAIA,IAAI,sBAAsB,WAAW,GAAG;GACtC,MAAM,aAAa;GACnB,SAAS;GACT,OAAO,QAAQ,aAAa,CAAC,sBAAsB,MAAM,MAAM,GAC7D,SAAS;GAGX,IAAI,QAAQ,WAAW,SAAS;GAChC,MAAM,KAAK;IACT,MAAM;IACN,cAAc;IACd;IACA,UAAU,QAAQ;GACpB,CAAC;GACD;EACF;EAGA,MAAM,eAAe,kBAAkB,WAAW;EAElD,IAAI,iBAAiB,MAAM;GACzB,MAAM,KAAK;IACT,MAAM;IACN;IACA,YAAY;IACZ,UAAU;GACZ,CAAC;GACD,SAAS;GACT;EACF;EAKA,MAAM,aAAa;EACnB,OACE,QAAQ,aACR,CAAC,YAAY,MAAM,MAAM,KACzB,CAAC,UAAU,MAAM,MAAM,KACvB,CAAC,sBAAsB,MAAM,MAAM,GAEnC,SAAS;EAEX,MAAM,KAAK;GACT,MAAM;GACN,cAAc;GACd;GACA,UAAU,QAAQ;EACpB,CAAC;CACH;CAEA,IAAI,MAAM,WAAW,GAAG,OAAO,CAAC;CAKhC,OAAO,MAAM,KAAK,MAAM,cAAqB;EAC3C,MAAM,kBAAkB,cAAc,IAAI,IAAI,KAAK;EACnD,MAAM,gBACJ,cAAc,MAAM,SAAS,IACzB,YAAY,IACZ,MAAM,YAAY,EAAE,CAAC,aAAa;EAExC,MAAM,aAAa,MAAM,MAAM,iBAAiB,gBAAgB,CAAC;EAGjE,MAAM,UACJ,gBAAgB,YAAY,IACxB,GAAG,WAAW,KAAK,IAAI,EAAE,MACzB,WAAW,KAAK,IAAI;EAE1B,OAAO;GACL,MAAM,KAAK;GACX;GACA,cAAc,KAAK;GACnB,WAAW,kBAAkB;GAC7B,SAAS,gBAAgB;EAC3B;CACF,CAAC;AACH;;;;;;;;;;;;;;;;;;;AAoBA,MAAa,mBAAmB,SAA0B;CACxD,MAAM,aAAa,gBAAgB,IAAI;CACvC,MAAM,WAAoB,CAAC;CAC3B,IAAI,gBAAyB,CAAC;CAE9B,MAAM,qBAA2B;EAC/B,IAAI,cAAc,WAAW,GAAG;EAEhC,MAAM,CAAC,cAAc;EACrB,MAAM,YAAY,cAAc,cAAc,SAAS;EAEvD,SAAS,KAAK;GACZ,MAAM,WAAW;GACjB,SAAS,cAAc,KAAK,UAAU,MAAM,OAAO,CAAC,CAAC,KAAK,EAAE;GAG5D,cAAc,WAAW;GACzB,WAAW,WAAW;GACtB,SAAS,UAAU;EACrB,CAAC;EACD,gBAAgB,CAAC;CACnB;CAEA,KAAK,MAAM,SAAS,YAAY;EAM9B,IAHE,MAAM,SAAS,aACd,MAAM,SAAS,aAAa,SAAS,WAAW,GAEjC,aAAa;EAC/B,cAAc,KAAK,KAAK;EACxB,IAAI,MAAM,SAAS,aAAa,SAAS,WAAW,GAAG,aAAa;CACtE;CAEA,aAAa;CAEb,OAAO;AACT"}
@@ -31,7 +31,7 @@ const prepareIntlayer = async (configuration, options) => {
31
31
  const sentinelPath = (0, node_path.join)(configuration.system.cacheDir, "intlayer-prepared.lock");
32
32
  const versionCache = (0, _intlayer_config_utils.cacheDisk)(configuration, ["intlayer-version"]);
33
33
  const intlayerCacheVersion = await versionCache.get();
34
- const isCorrectVersion = Boolean(intlayerCacheVersion && intlayerCacheVersion === _intlayer_config_package_json.default.version);
34
+ const isCorrectVersion = Boolean(intlayerCacheVersion === void 0 || intlayerCacheVersion === _intlayer_config_package_json.default.version);
35
35
  const isConfigSimilar = await require_writeConfiguration_index.isCachedConfigurationUpToDate(configuration);
36
36
  const dictionariesWithStats = await require_listDictionariesPath.listDictionariesWithStats(configuration);
37
37
  let isDictionaryChanged = false;
@@ -45,8 +45,12 @@ const prepareIntlayer = async (configuration, options) => {
45
45
  forceRun: !isCorrectVersion || !isConfigSimilar || isDictionaryChanged || hasPluginLoadDictionaries,
46
46
  ...options ?? {}
47
47
  };
48
- await require_utils_runOnce.runOnce(sentinelPath, async () => {
49
- if (clean || !isCorrectVersion) await require_cleanOutputDir.cleanOutputDir(configuration);
48
+ await require_utils_runOnce.runOnce(sentinelPath, async ({ renewLock }) => {
49
+ const isCorrectVersionUnderLock = await versionCache.get() === _intlayer_config_package_json.default.version;
50
+ if (clean || !isCorrectVersionUnderLock) {
51
+ await require_cleanOutputDir.cleanOutputDir(configuration);
52
+ await renewLock();
53
+ }
50
54
  await versionCache.set(_intlayer_config_package_json.default.version);
51
55
  const preparationStartMs = Date.now();
52
56
  appLogger(["Preparing Intlayer", (0, _intlayer_config_logger.colorize)(`(v${_intlayer_config_package_json.default.version})`, _intlayer_config_colors.GREY_DARK)]);
@@ -1 +1 @@
1
- {"version":3,"file":"prepareIntlayer.cjs","names":["packageJson","isCachedConfigurationUpToDate","listDictionariesWithStats","runOnce","cleanOutputDir","ANSIColors","writeConfiguration","loadDictionaries","buildDictionary","writeRemoteDictionary","createTypes","createDictionaryEntryPoint","createModuleAugmentation"],"sources":["../../src/prepareIntlayer.ts"],"sourcesContent":["import { stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport packageJson from '@intlayer/config/package.json' with { type: 'json' };\nimport { cacheDisk } from '@intlayer/config/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { buildDictionary } from './buildIntlayerDictionary/buildIntlayerDictionary';\nimport { writeRemoteDictionary } from './buildIntlayerDictionary/writeRemoteDictionary';\nimport { cleanOutputDir } from './cleanOutputDir';\nimport { createDictionaryEntryPoint } from './createDictionaryEntryPoint/createDictionaryEntryPoint';\nimport { createModuleAugmentation, createTypes } from './createType/index';\nimport { listDictionariesWithStats } from './listDictionariesPath';\nimport { loadDictionaries } from './loadDictionaries/loadDictionaries';\nimport { runOnce } from './utils/runOnce';\nimport {\n isCachedConfigurationUpToDate,\n writeConfiguration,\n} from './writeConfiguration';\n\ntype PrepareIntlayerOptions = {\n clean?: boolean;\n env?: 'prod' | 'dev';\n format?: ('cjs' | 'esm')[];\n forceRun?: boolean;\n cacheTimeoutMs?: number;\n onIsCached?: () => void | Promise<void>;\n};\n\nconst DEFAULT_PREPARE_INTLAYER_OPTIONS = {\n clean: false,\n env: 'dev',\n format: ['cjs', 'esm'],\n cacheTimeoutMs: 1000 * 60 * 60, // 1 hour\n} satisfies PrepareIntlayerOptions;\n\nexport const prepareIntlayer = async (\n configuration: IntlayerConfig,\n options?: PrepareIntlayerOptions\n) => {\n const appLogger = getAppLogger(configuration);\n\n const sentinelPath = join(\n configuration.system.cacheDir,\n 'intlayer-prepared.lock'\n );\n // Clean output dir if the intlayer version has changed\n const versionCache = cacheDisk(configuration, ['intlayer-version']);\n const intlayerCacheVersion = await versionCache.get();\n const isCorrectVersion = Boolean(\n intlayerCacheVersion && intlayerCacheVersion === packageJson.version\n );\n\n const isConfigSimilar = await isCachedConfigurationUpToDate(configuration);\n\n // Check if any dictionary has been changed to force a new rebuild\n const dictionariesWithStats = await listDictionariesWithStats(configuration);\n let isDictionaryChanged = false;\n try {\n // Try catch as sentinel file may not exist yet\n const sentinelStats = await stat(sentinelPath);\n isDictionaryChanged = dictionariesWithStats.some(\n (dictionary) =>\n dictionary.stats.mtime.getTime() > sentinelStats.mtime.getTime()\n );\n } catch {}\n\n const resolvedPlugins = await Promise.all(configuration.plugins ?? []);\n const hasPluginLoadDictionaries = resolvedPlugins.some((plugin) =>\n Boolean(plugin.loadDictionaries)\n ); // Disable cache if any plugin because it can have custom behavior\n\n const { clean, format, forceRun, onIsCached, cacheTimeoutMs, env } = {\n ...DEFAULT_PREPARE_INTLAYER_OPTIONS,\n forceRun:\n !isCorrectVersion ||\n !isConfigSimilar ||\n isDictionaryChanged ||\n hasPluginLoadDictionaries,\n ...(options ?? {}),\n };\n\n // Skip preparation if it has already been done recently\n await runOnce(\n sentinelPath,\n async () => {\n // comment because of issue with next and webpack\n // await checkVersionsConsistency(configuration);\n\n if (clean || !isCorrectVersion) {\n await cleanOutputDir(configuration);\n }\n\n await versionCache.set(packageJson.version);\n\n const preparationStartMs = Date.now();\n\n appLogger([\n 'Preparing Intlayer',\n colorize(`(v${packageJson.version})`, ANSIColors.GREY_DARK),\n ]);\n\n await writeConfiguration(configuration);\n\n const configurationWrittenTime = Date.now();\n\n appLogger(\n [\n 'Configuration written',\n colorize(\n `(${configurationWrittenTime - preparationStartMs}ms)`,\n ANSIColors.GREY_DARK\n ),\n ],\n {\n isVerbose: true,\n }\n );\n\n const contentDeclarationPaths = dictionariesWithStats.map(\n (dictionary) => dictionary.path\n );\n\n const dictionaries = await loadDictionaries(\n contentDeclarationPaths,\n configuration\n );\n\n const dictionariesLoadedTime = Date.now();\n\n appLogger(\n [\n 'Content loaded',\n colorize(\n [\n dictionaries.remoteDictionaries.length +\n dictionaries.pluginDictionaries.length >\n 0\n ? [\n `(Total: ${dictionariesLoadedTime - configurationWrittenTime}ms`,\n dictionaries.localDictionaries.length > 0\n ? ` - Local: ${dictionaries.time.localDictionaries}ms`\n : '',\n dictionaries.remoteDictionaries.length > 0\n ? ` - Remote: ${dictionaries.time.remoteDictionaries}ms`\n : '',\n dictionaries.pluginDictionaries.length > 0\n ? ` - Plugin: ${dictionaries.time.pluginDictionaries}ms`\n : '',\n `)`,\n ].join('')\n : `(${dictionariesLoadedTime - configurationWrittenTime}ms)`,\n ].join(''),\n ANSIColors.GREY_DARK\n ),\n ],\n {\n isVerbose: true,\n }\n );\n\n // Build local dictionaries\n const dictionariesOutput = await buildDictionary(\n [\n ...dictionaries.localDictionaries,\n ...dictionaries.remoteDictionaries,\n ...dictionaries.pluginDictionaries,\n ],\n configuration,\n { formats: format, importOtherDictionaries: false, env }\n );\n\n // Write remote dictionaries\n // Used as cache for next fetch\n await writeRemoteDictionary(\n dictionaries.remoteDictionaries,\n configuration\n );\n\n const dictionariesToBuild = Object.values(\n dictionariesOutput?.mergedDictionaries ?? {}\n ).map((dictionary) => dictionary.dictionary);\n\n await createTypes(dictionariesToBuild, configuration);\n\n await createDictionaryEntryPoint(configuration, {\n formats: options?.format,\n });\n\n const dictionariesBuiltTime = Date.now();\n\n appLogger([\n 'Dictionaries built',\n colorize(\n `(${dictionariesBuiltTime - preparationStartMs}ms)`,\n ANSIColors.GREY_DARK\n ),\n ]);\n\n await createModuleAugmentation(configuration);\n\n const moduleAugmentationBuiltTime = Date.now();\n\n appLogger(\n [\n 'Module augmentation built',\n colorize(\n `(${moduleAugmentationBuiltTime - dictionariesBuiltTime}ms)`,\n ANSIColors.GREY_DARK\n ),\n ],\n {\n isVerbose: true,\n }\n );\n\n // Plugin transformation\n // Allow plugins to post-process the final build output (e.g., write back ICU JSON)\n for await (const plugin of configuration.plugins ?? []) {\n const { unmergedDictionaries, mergedDictionaries } = dictionariesOutput;\n\n try {\n await plugin.afterBuild?.({\n dictionaries: {\n unmergedDictionaries,\n mergedDictionaries,\n },\n configuration,\n });\n } catch (error) {\n // A failing write-back (e.g. transient network error on a remote\n // target) must not invalidate the dictionaries already built.\n appLogger(\n [\n `Plugin ${plugin.name} afterBuild failed:`,\n (error as Error).message,\n ],\n { level: 'error' }\n );\n }\n }\n\n const preparationElapsedMs = Date.now() - preparationStartMs;\n appLogger(\n [`Done`, colorize(`${preparationElapsedMs}ms`, ANSIColors.GREEN)],\n {\n level: 'info',\n isVerbose: true,\n }\n );\n },\n {\n forceRun,\n onIsCached,\n cacheTimeoutMs,\n }\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"prepareIntlayer.cjs","names":["packageJson","isCachedConfigurationUpToDate","listDictionariesWithStats","runOnce","cleanOutputDir","ANSIColors","writeConfiguration","loadDictionaries","buildDictionary","writeRemoteDictionary","createTypes","createDictionaryEntryPoint","createModuleAugmentation"],"sources":["../../src/prepareIntlayer.ts"],"sourcesContent":["import { stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport packageJson from '@intlayer/config/package.json' with { type: 'json' };\nimport { cacheDisk } from '@intlayer/config/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { buildDictionary } from './buildIntlayerDictionary/buildIntlayerDictionary';\nimport { writeRemoteDictionary } from './buildIntlayerDictionary/writeRemoteDictionary';\nimport { cleanOutputDir } from './cleanOutputDir';\nimport { createDictionaryEntryPoint } from './createDictionaryEntryPoint/createDictionaryEntryPoint';\nimport { createModuleAugmentation, createTypes } from './createType/index';\nimport { listDictionariesWithStats } from './listDictionariesPath';\nimport { loadDictionaries } from './loadDictionaries/loadDictionaries';\nimport { runOnce } from './utils/runOnce';\nimport {\n isCachedConfigurationUpToDate,\n writeConfiguration,\n} from './writeConfiguration';\n\ntype PrepareIntlayerOptions = {\n clean?: boolean;\n env?: 'prod' | 'dev';\n format?: ('cjs' | 'esm')[];\n forceRun?: boolean;\n cacheTimeoutMs?: number;\n onIsCached?: () => void | Promise<void>;\n};\n\nconst DEFAULT_PREPARE_INTLAYER_OPTIONS = {\n clean: false,\n env: 'dev',\n format: ['cjs', 'esm'],\n cacheTimeoutMs: 1000 * 60 * 60, // 1 hour\n} satisfies PrepareIntlayerOptions;\n\nexport const prepareIntlayer = async (\n configuration: IntlayerConfig,\n options?: PrepareIntlayerOptions\n) => {\n const appLogger = getAppLogger(configuration);\n\n const sentinelPath = join(\n configuration.system.cacheDir,\n 'intlayer-prepared.lock'\n );\n // Clean output dir if the intlayer version has changed\n const versionCache = cacheDisk(configuration, ['intlayer-version']);\n const intlayerCacheVersion = await versionCache.get();\n const isCorrectVersion = Boolean(\n intlayerCacheVersion === undefined ||\n intlayerCacheVersion === packageJson.version\n );\n\n const isConfigSimilar = await isCachedConfigurationUpToDate(configuration);\n\n // Check if any dictionary has been changed to force a new rebuild\n const dictionariesWithStats = await listDictionariesWithStats(configuration);\n let isDictionaryChanged = false;\n try {\n // Try catch as sentinel file may not exist yet\n const sentinelStats = await stat(sentinelPath);\n isDictionaryChanged = dictionariesWithStats.some(\n (dictionary) =>\n dictionary.stats.mtime.getTime() > sentinelStats.mtime.getTime()\n );\n } catch {}\n\n const resolvedPlugins = await Promise.all(configuration.plugins ?? []);\n const hasPluginLoadDictionaries = resolvedPlugins.some((plugin) =>\n Boolean(plugin.loadDictionaries)\n ); // Disable cache if any plugin because it can have custom behavior\n\n const { clean, format, forceRun, onIsCached, cacheTimeoutMs, env } = {\n ...DEFAULT_PREPARE_INTLAYER_OPTIONS,\n forceRun:\n !isCorrectVersion ||\n !isConfigSimilar ||\n isDictionaryChanged ||\n hasPluginLoadDictionaries,\n ...(options ?? {}),\n };\n\n // Skip preparation if it has already been done recently\n await runOnce(\n sentinelPath,\n async ({ renewLock }) => {\n // comment because of issue with next and webpack\n // await checkVersionsConsistency(configuration);\n\n // Re-read the cached version now that the lock is held: another process\n // may have upgraded the output directory while this call was queued, in\n // which case cleaning it would throw away a build that just completed.\n const isCorrectVersionUnderLock =\n (await versionCache.get()) === packageJson.version;\n\n if (clean || !isCorrectVersionUnderLock) {\n await cleanOutputDir(configuration);\n // `cleanOutputDir` wipes the cache directory the sentinel lives in.\n // Re-create it immediately, otherwise a concurrent process (e.g. the\n // `intlayer watch` CLI next to a dev server) sees no lock, starts its\n // own run, and cleans the output directory while this one writes.\n await renewLock();\n }\n\n await versionCache.set(packageJson.version);\n\n const preparationStartMs = Date.now();\n\n appLogger([\n 'Preparing Intlayer',\n colorize(`(v${packageJson.version})`, ANSIColors.GREY_DARK),\n ]);\n\n await writeConfiguration(configuration);\n\n const configurationWrittenTime = Date.now();\n\n appLogger(\n [\n 'Configuration written',\n colorize(\n `(${configurationWrittenTime - preparationStartMs}ms)`,\n ANSIColors.GREY_DARK\n ),\n ],\n {\n isVerbose: true,\n }\n );\n\n const contentDeclarationPaths = dictionariesWithStats.map(\n (dictionary) => dictionary.path\n );\n\n const dictionaries = await loadDictionaries(\n contentDeclarationPaths,\n configuration\n );\n\n const dictionariesLoadedTime = Date.now();\n\n appLogger(\n [\n 'Content loaded',\n colorize(\n [\n dictionaries.remoteDictionaries.length +\n dictionaries.pluginDictionaries.length >\n 0\n ? [\n `(Total: ${dictionariesLoadedTime - configurationWrittenTime}ms`,\n dictionaries.localDictionaries.length > 0\n ? ` - Local: ${dictionaries.time.localDictionaries}ms`\n : '',\n dictionaries.remoteDictionaries.length > 0\n ? ` - Remote: ${dictionaries.time.remoteDictionaries}ms`\n : '',\n dictionaries.pluginDictionaries.length > 0\n ? ` - Plugin: ${dictionaries.time.pluginDictionaries}ms`\n : '',\n `)`,\n ].join('')\n : `(${dictionariesLoadedTime - configurationWrittenTime}ms)`,\n ].join(''),\n ANSIColors.GREY_DARK\n ),\n ],\n {\n isVerbose: true,\n }\n );\n\n // Build local dictionaries\n const dictionariesOutput = await buildDictionary(\n [\n ...dictionaries.localDictionaries,\n ...dictionaries.remoteDictionaries,\n ...dictionaries.pluginDictionaries,\n ],\n configuration,\n { formats: format, importOtherDictionaries: false, env }\n );\n\n // Write remote dictionaries\n // Used as cache for next fetch\n await writeRemoteDictionary(\n dictionaries.remoteDictionaries,\n configuration\n );\n\n const dictionariesToBuild = Object.values(\n dictionariesOutput?.mergedDictionaries ?? {}\n ).map((dictionary) => dictionary.dictionary);\n\n await createTypes(dictionariesToBuild, configuration);\n\n await createDictionaryEntryPoint(configuration, {\n formats: options?.format,\n });\n\n const dictionariesBuiltTime = Date.now();\n\n appLogger([\n 'Dictionaries built',\n colorize(\n `(${dictionariesBuiltTime - preparationStartMs}ms)`,\n ANSIColors.GREY_DARK\n ),\n ]);\n\n await createModuleAugmentation(configuration);\n\n const moduleAugmentationBuiltTime = Date.now();\n\n appLogger(\n [\n 'Module augmentation built',\n colorize(\n `(${moduleAugmentationBuiltTime - dictionariesBuiltTime}ms)`,\n ANSIColors.GREY_DARK\n ),\n ],\n {\n isVerbose: true,\n }\n );\n\n // Plugin transformation\n // Allow plugins to post-process the final build output (e.g., write back ICU JSON)\n for await (const plugin of configuration.plugins ?? []) {\n const { unmergedDictionaries, mergedDictionaries } = dictionariesOutput;\n\n try {\n await plugin.afterBuild?.({\n dictionaries: {\n unmergedDictionaries,\n mergedDictionaries,\n },\n configuration,\n });\n } catch (error) {\n // A failing write-back (e.g. transient network error on a remote\n // target) must not invalidate the dictionaries already built.\n appLogger(\n [\n `Plugin ${plugin.name} afterBuild failed:`,\n (error as Error).message,\n ],\n { level: 'error' }\n );\n }\n }\n\n const preparationElapsedMs = Date.now() - preparationStartMs;\n appLogger(\n [`Done`, colorize(`${preparationElapsedMs}ms`, ANSIColors.GREEN)],\n {\n level: 'info',\n isVerbose: true,\n }\n );\n },\n {\n forceRun,\n onIsCached,\n cacheTimeoutMs,\n }\n );\n};\n"],"mappings":"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"}