@intlayer/cli 8.12.5-canary.0 → 9.0.0-canary.1

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 (100) hide show
  1. package/dist/cjs/cli.cjs +18 -3
  2. package/dist/cjs/cli.cjs.map +1 -1
  3. package/dist/cjs/index.cjs +2 -0
  4. package/dist/cjs/reviewDoc/reviewDoc.cjs +38 -11
  5. package/dist/cjs/reviewDoc/reviewDoc.cjs.map +1 -1
  6. package/dist/cjs/reviewDoc/reviewDocBlockAware.cjs +54 -37
  7. package/dist/cjs/reviewDoc/reviewDocBlockAware.cjs.map +1 -1
  8. package/dist/cjs/reviewDoc/reviewDocLog.cjs +48 -0
  9. package/dist/cjs/reviewDoc/reviewDocLog.cjs.map +1 -0
  10. package/dist/cjs/scan.cjs +75 -0
  11. package/dist/cjs/scan.cjs.map +1 -0
  12. package/dist/cjs/utils/formatLineRanges.cjs +44 -0
  13. package/dist/cjs/utils/formatLineRanges.cjs.map +1 -0
  14. package/dist/esm/cli.mjs +18 -3
  15. package/dist/esm/cli.mjs.map +1 -1
  16. package/dist/esm/index.mjs +2 -1
  17. package/dist/esm/reviewDoc/reviewDoc.mjs +38 -11
  18. package/dist/esm/reviewDoc/reviewDoc.mjs.map +1 -1
  19. package/dist/esm/reviewDoc/reviewDocBlockAware.mjs +54 -37
  20. package/dist/esm/reviewDoc/reviewDocBlockAware.mjs.map +1 -1
  21. package/dist/esm/reviewDoc/reviewDocLog.mjs +46 -0
  22. package/dist/esm/reviewDoc/reviewDocLog.mjs.map +1 -0
  23. package/dist/esm/scan.mjs +72 -0
  24. package/dist/esm/scan.mjs.map +1 -0
  25. package/dist/esm/utils/formatLineRanges.mjs +42 -0
  26. package/dist/esm/utils/formatLineRanges.mjs.map +1 -0
  27. package/dist/types/cli.d.ts.map +1 -1
  28. package/dist/types/index.d.ts +2 -1
  29. package/dist/types/reviewDoc/reviewDoc.d.ts +8 -1
  30. package/dist/types/reviewDoc/reviewDoc.d.ts.map +1 -1
  31. package/dist/types/reviewDoc/reviewDocBlockAware.d.ts +8 -6
  32. package/dist/types/reviewDoc/reviewDocBlockAware.d.ts.map +1 -1
  33. package/dist/types/reviewDoc/reviewDocLog.d.ts +25 -0
  34. package/dist/types/reviewDoc/reviewDocLog.d.ts.map +1 -0
  35. package/dist/types/scan.d.ts +22 -0
  36. package/dist/types/scan.d.ts.map +1 -0
  37. package/dist/types/utils/formatLineRanges.d.ts +21 -0
  38. package/dist/types/utils/formatLineRanges.d.ts.map +1 -0
  39. package/package.json +14 -14
  40. package/dist/cjs/translation-alignment/alignBlocks.cjs +0 -68
  41. package/dist/cjs/translation-alignment/alignBlocks.cjs.map +0 -1
  42. package/dist/cjs/translation-alignment/computeSimilarity.cjs +0 -26
  43. package/dist/cjs/translation-alignment/computeSimilarity.cjs.map +0 -1
  44. package/dist/cjs/translation-alignment/fingerprintBlock.cjs +0 -24
  45. package/dist/cjs/translation-alignment/fingerprintBlock.cjs.map +0 -1
  46. package/dist/cjs/translation-alignment/index.cjs +0 -22
  47. package/dist/cjs/translation-alignment/mapChangedLinesToBlocks.cjs +0 -19
  48. package/dist/cjs/translation-alignment/mapChangedLinesToBlocks.cjs.map +0 -1
  49. package/dist/cjs/translation-alignment/normalizeBlock.cjs +0 -23
  50. package/dist/cjs/translation-alignment/normalizeBlock.cjs.map +0 -1
  51. package/dist/cjs/translation-alignment/pipeline.cjs +0 -38
  52. package/dist/cjs/translation-alignment/pipeline.cjs.map +0 -1
  53. package/dist/cjs/translation-alignment/planActions.cjs +0 -47
  54. package/dist/cjs/translation-alignment/planActions.cjs.map +0 -1
  55. package/dist/cjs/translation-alignment/rebuildDocument.cjs +0 -50
  56. package/dist/cjs/translation-alignment/rebuildDocument.cjs.map +0 -1
  57. package/dist/cjs/translation-alignment/segmentDocument.cjs +0 -67
  58. package/dist/cjs/translation-alignment/segmentDocument.cjs.map +0 -1
  59. package/dist/cjs/translation-alignment/types.cjs +0 -0
  60. package/dist/esm/translation-alignment/alignBlocks.mjs +0 -67
  61. package/dist/esm/translation-alignment/alignBlocks.mjs.map +0 -1
  62. package/dist/esm/translation-alignment/computeSimilarity.mjs +0 -23
  63. package/dist/esm/translation-alignment/computeSimilarity.mjs.map +0 -1
  64. package/dist/esm/translation-alignment/fingerprintBlock.mjs +0 -21
  65. package/dist/esm/translation-alignment/fingerprintBlock.mjs.map +0 -1
  66. package/dist/esm/translation-alignment/index.mjs +0 -11
  67. package/dist/esm/translation-alignment/mapChangedLinesToBlocks.mjs +0 -17
  68. package/dist/esm/translation-alignment/mapChangedLinesToBlocks.mjs.map +0 -1
  69. package/dist/esm/translation-alignment/normalizeBlock.mjs +0 -21
  70. package/dist/esm/translation-alignment/normalizeBlock.mjs.map +0 -1
  71. package/dist/esm/translation-alignment/pipeline.mjs +0 -36
  72. package/dist/esm/translation-alignment/pipeline.mjs.map +0 -1
  73. package/dist/esm/translation-alignment/planActions.mjs +0 -45
  74. package/dist/esm/translation-alignment/planActions.mjs.map +0 -1
  75. package/dist/esm/translation-alignment/rebuildDocument.mjs +0 -47
  76. package/dist/esm/translation-alignment/rebuildDocument.mjs.map +0 -1
  77. package/dist/esm/translation-alignment/segmentDocument.mjs +0 -65
  78. package/dist/esm/translation-alignment/segmentDocument.mjs.map +0 -1
  79. package/dist/esm/translation-alignment/types.mjs +0 -0
  80. package/dist/types/translation-alignment/alignBlocks.d.ts +0 -7
  81. package/dist/types/translation-alignment/alignBlocks.d.ts.map +0 -1
  82. package/dist/types/translation-alignment/computeSimilarity.d.ts +0 -6
  83. package/dist/types/translation-alignment/computeSimilarity.d.ts.map +0 -1
  84. package/dist/types/translation-alignment/fingerprintBlock.d.ts +0 -7
  85. package/dist/types/translation-alignment/fingerprintBlock.d.ts.map +0 -1
  86. package/dist/types/translation-alignment/index.d.ts +0 -11
  87. package/dist/types/translation-alignment/mapChangedLinesToBlocks.d.ts +0 -7
  88. package/dist/types/translation-alignment/mapChangedLinesToBlocks.d.ts.map +0 -1
  89. package/dist/types/translation-alignment/normalizeBlock.d.ts +0 -7
  90. package/dist/types/translation-alignment/normalizeBlock.d.ts.map +0 -1
  91. package/dist/types/translation-alignment/pipeline.d.ts +0 -25
  92. package/dist/types/translation-alignment/pipeline.d.ts.map +0 -1
  93. package/dist/types/translation-alignment/planActions.d.ts +0 -7
  94. package/dist/types/translation-alignment/planActions.d.ts.map +0 -1
  95. package/dist/types/translation-alignment/rebuildDocument.d.ts +0 -32
  96. package/dist/types/translation-alignment/rebuildDocument.d.ts.map +0 -1
  97. package/dist/types/translation-alignment/segmentDocument.d.ts +0 -7
  98. package/dist/types/translation-alignment/segmentDocument.d.ts.map +0 -1
  99. package/dist/types/translation-alignment/types.d.ts +0 -49
  100. package/dist/types/translation-alignment/types.d.ts.map +0 -1
@@ -1,7 +1,9 @@
1
1
  import { setupAI } from "../utils/setupAI.mjs";
2
2
  import { getOutputFilePath } from "../utils/getOutputFilePath.mjs";
3
3
  import { checkFileModifiedRange } from "../utils/checkFileModifiedRange.mjs";
4
+ import { formatLineRanges } from "../utils/formatLineRanges.mjs";
4
5
  import { reviewFileBlockAware } from "./reviewDocBlockAware.mjs";
6
+ import { logReviewFileBlocks } from "./reviewDocLog.mjs";
5
7
  import { existsSync } from "node:fs";
6
8
  import { join, relative } from "node:path";
7
9
  import { listGitFiles, listGitLines, logConfigDetails } from "@intlayer/chokidar/cli";
@@ -9,6 +11,8 @@ import { formatLocale, formatPath, parallelize } from "@intlayer/chokidar/utils"
9
11
  import * as ANSIColors from "@intlayer/config/colors";
10
12
  import { colorize, colorizeNumber, getAppLogger, x } from "@intlayer/config/logger";
11
13
  import { getConfiguration } from "@intlayer/config/node";
14
+ import { readFile } from "node:fs/promises";
15
+ import { buildReviewReport } from "@intlayer/chokidar/docReview";
12
16
  import fg from "fast-glob";
13
17
 
14
18
  //#region src/reviewDoc/reviewDoc.ts
@@ -16,18 +20,26 @@ import fg from "fast-glob";
16
20
  * Main audit function: scans all .md files in "en/" (unless you specified DOC_LIST),
17
21
  * then audits them to each locale in LOCALE_LIST.
18
22
  */
19
- const reviewDoc = async ({ docPattern, locales, excludedGlobPattern, baseLocale, aiOptions, nbSimultaneousFileProcessed, configOptions, customInstructions, skipIfModifiedBefore, skipIfModifiedAfter, skipIfExists, gitOptions }) => {
23
+ const reviewDoc = async ({ docPattern, locales, excludedGlobPattern, baseLocale, aiOptions, nbSimultaneousFileProcessed, configOptions, customInstructions, skipIfModifiedBefore, skipIfModifiedAfter, skipIfExists, gitOptions, log }) => {
20
24
  const configuration = getConfiguration(configOptions);
21
25
  logConfigDetails(configOptions);
22
- const appLogger = getAppLogger(configuration);
23
- const aiResult = await setupAI(configuration, aiOptions);
24
- if (!aiResult?.hasAIAccess) return;
25
- const { aiClient, aiConfig, isCustomAI } = aiResult;
26
- if (isCustomAI && aiClient && aiConfig) {
27
- const { hasAIAccess, error } = await aiClient.checkAISDKAccess(aiConfig);
28
- if (!hasAIAccess) {
29
- appLogger(`${x} ${error}`);
30
- return;
26
+ const appLogger = getAppLogger({ log: {
27
+ ...configuration.log,
28
+ prefix: ""
29
+ } });
30
+ let aiClient;
31
+ let aiConfig;
32
+ if (!log) {
33
+ const aiResult = await setupAI(configuration, aiOptions);
34
+ if (!aiResult?.hasAIAccess) return;
35
+ aiClient = aiResult.aiClient;
36
+ aiConfig = aiResult.aiConfig;
37
+ if (aiResult.isCustomAI && aiClient && aiConfig) {
38
+ const { hasAIAccess, error } = await aiClient.checkAISDKAccess(aiConfig);
39
+ if (!hasAIAccess) {
40
+ appLogger(`${x} ${error}`);
41
+ return;
42
+ }
31
43
  }
32
44
  }
33
45
  if (nbSimultaneousFileProcessed && nbSimultaneousFileProcessed > 10) {
@@ -65,8 +77,23 @@ const reviewDoc = async ({ docPattern, locales, excludedGlobPattern, baseLocale,
65
77
  let changedLines;
66
78
  if (gitOptions) {
67
79
  const gitChangedLines = await listGitLines(absoluteBaseFilePath, gitOptions);
68
- appLogger(`Git changed lines: ${gitChangedLines.join(", ")}`);
69
80
  changedLines = gitChangedLines;
81
+ appLogger(`Changed lines (${formatLocale(baseLocale)}): ${formatLineRanges(gitChangedLines)}`);
82
+ const { blocks } = buildReviewReport({
83
+ baseText: await readFile(absoluteBaseFilePath, "utf-8").catch(() => ""),
84
+ targetText: existsSync(outputFilePath) ? await readFile(outputFilePath, "utf-8").catch(() => "") : "",
85
+ changedLines: gitChangedLines
86
+ });
87
+ const correspondingTargetLines = blocks.flatMap((block) => {
88
+ if (block.action !== "review" || !block.targetLineRange) return [];
89
+ const { start, end } = block.targetLineRange;
90
+ return Array.from({ length: end - start + 1 }, (_unused, offset) => start + offset);
91
+ });
92
+ appLogger(`Corresponding block (${formatLocale(locale)}): ${formatLineRanges(correspondingTargetLines)}`);
93
+ }
94
+ if (log) {
95
+ await logReviewFileBlocks(absoluteBaseFilePath, outputFilePath, locale, baseLocale, configOptions, changedLines);
96
+ return;
70
97
  }
71
98
  await reviewFileBlockAware(absoluteBaseFilePath, outputFilePath, locale, baseLocale, aiOptions, configOptions, customInstructions, changedLines, aiClient, aiConfig);
72
99
  })), (task) => task(), nbSimultaneousFileProcessed ?? 3);
@@ -1 +1 @@
1
- {"version":3,"file":"reviewDoc.mjs","names":[],"sources":["../../../src/reviewDoc/reviewDoc.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport type { AIOptions } from '@intlayer/api';\nimport {\n type ListGitFilesOptions,\n listGitFiles,\n listGitLines,\n logConfigDetails,\n} from '@intlayer/chokidar/cli';\nimport {\n formatLocale,\n formatPath,\n parallelize,\n} from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeNumber,\n getAppLogger,\n x,\n} from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport fg from 'fast-glob';\nimport { checkFileModifiedRange } from '../utils/checkFileModifiedRange';\nimport { getOutputFilePath } from '../utils/getOutputFilePath';\nimport { setupAI } from '../utils/setupAI';\nimport { reviewFileBlockAware } from './reviewDocBlockAware';\n\ntype ReviewDocOptions = {\n docPattern: string[];\n locales: Locale[];\n excludedGlobPattern: string[];\n baseLocale: Locale;\n aiOptions?: AIOptions;\n nbSimultaneousFileProcessed?: number;\n configOptions?: GetConfigurationOptions;\n customInstructions?: string;\n skipIfModifiedBefore?: number | string | Date;\n skipIfModifiedAfter?: number | string | Date;\n skipIfExists?: boolean;\n gitOptions?: ListGitFilesOptions;\n};\n\n/**\n * Main audit function: scans all .md files in \"en/\" (unless you specified DOC_LIST),\n * then audits them to each locale in LOCALE_LIST.\n */\nexport const reviewDoc = async ({\n docPattern,\n locales,\n excludedGlobPattern,\n baseLocale,\n aiOptions,\n nbSimultaneousFileProcessed,\n configOptions,\n customInstructions,\n skipIfModifiedBefore,\n skipIfModifiedAfter,\n skipIfExists,\n gitOptions,\n}: ReviewDocOptions) => {\n const configuration = getConfiguration(configOptions);\n logConfigDetails(configOptions);\n\n const appLogger = getAppLogger(configuration);\n\n const aiResult = await setupAI(configuration, aiOptions);\n\n if (!aiResult?.hasAIAccess) return;\n\n const { aiClient, aiConfig, isCustomAI } = aiResult;\n\n if (isCustomAI && aiClient && aiConfig) {\n const { hasAIAccess, error } = await aiClient.checkAISDKAccess(aiConfig);\n if (!hasAIAccess) {\n appLogger(`${x} ${error}`);\n return;\n }\n }\n\n if (nbSimultaneousFileProcessed && nbSimultaneousFileProcessed > 10) {\n appLogger(\n `Warning: nbSimultaneousFileProcessed is set to ${nbSimultaneousFileProcessed}, which is greater than 10. Setting it to 10.`\n );\n nbSimultaneousFileProcessed = 10; // Limit the number of simultaneous file processed to 10\n }\n\n let docList: string[] = await fg(docPattern, {\n ignore: excludedGlobPattern,\n });\n\n if (gitOptions) {\n const gitChangedFiles = await listGitFiles(gitOptions);\n\n if (gitChangedFiles) {\n // Convert dictionary file paths to be relative to git root for comparison\n\n // Filter dictionaries based on git changed files\n docList = docList.filter((path) =>\n gitChangedFiles.some((gitFile) => join(process.cwd(), path) === gitFile)\n );\n }\n }\n\n // OAuth handled by API proxy internally\n\n appLogger(`Base locale is ${formatLocale(baseLocale)}`);\n appLogger(\n `Reviewing ${colorizeNumber(locales.length)} locales: [ ${formatLocale(locales)} ]`\n );\n\n appLogger(`Reviewing ${colorizeNumber(docList.length)} files:`);\n appLogger(docList.map((path) => ` - ${formatPath(path)}\\n`));\n\n // Create all tasks to be processed\n const allTasks = docList.flatMap((docPath) =>\n locales.map((locale) => async () => {\n appLogger(\n `Reviewing file: ${formatPath(docPath)} to ${formatLocale(locale)}`\n );\n\n const absoluteBaseFilePath = join(configuration.system.baseDir, docPath);\n const outputFilePath = getOutputFilePath(\n absoluteBaseFilePath,\n locale,\n baseLocale\n );\n\n // Skip if file exists and skipIfExists option is enabled\n if (skipIfExists && existsSync(outputFilePath)) {\n const relativePath = relative(\n configuration.system.baseDir,\n outputFilePath\n );\n appLogger(\n `${colorize('⊘', ANSIColors.YELLOW)} File ${formatPath(relativePath)} already exists, skipping.`\n );\n return;\n }\n\n // Check modification range only if the file exists\n if (existsSync(outputFilePath)) {\n const fileModificationData = checkFileModifiedRange(outputFilePath, {\n skipIfModifiedBefore,\n skipIfModifiedAfter,\n });\n\n if (fileModificationData.isSkipped) {\n appLogger(fileModificationData.message);\n return;\n }\n } else if (skipIfModifiedBefore || skipIfModifiedAfter) {\n // Log if we intended to check modification time but couldn't because the file doesn't exist\n appLogger(\n `${colorize('!', ANSIColors.YELLOW)} File ${formatPath(outputFilePath)} does not exist, skipping modification date check.`\n );\n }\n\n let changedLines: number[] | undefined;\n // FIXED: Enable git optimization that was previously commented out\n if (gitOptions) {\n const gitChangedLines = await listGitLines(\n absoluteBaseFilePath,\n gitOptions\n );\n\n appLogger(`Git changed lines: ${gitChangedLines.join(', ')}`);\n changedLines = gitChangedLines;\n }\n\n await reviewFileBlockAware(\n absoluteBaseFilePath,\n outputFilePath,\n locale as Locale,\n baseLocale,\n aiOptions,\n configOptions,\n customInstructions,\n changedLines,\n aiClient,\n aiConfig\n );\n })\n );\n\n await parallelize(\n allTasks,\n (task) => task(),\n nbSimultaneousFileProcessed ?? 3\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmDA,MAAa,YAAY,OAAO,EAC9B,YACA,SACA,qBACA,YACA,WACA,6BACA,eACA,oBACA,sBACA,qBACA,cACA,iBACsB;CACtB,MAAM,gBAAgB,iBAAiB,cAAc;AACrD,kBAAiB,cAAc;CAE/B,MAAM,YAAY,aAAa,cAAc;CAE7C,MAAM,WAAW,MAAM,QAAQ,eAAe,UAAU;AAExD,KAAI,CAAC,UAAU,YAAa;CAE5B,MAAM,EAAE,UAAU,UAAU,eAAe;AAE3C,KAAI,cAAc,YAAY,UAAU;EACtC,MAAM,EAAE,aAAa,UAAU,MAAM,SAAS,iBAAiB,SAAS;AACxE,MAAI,CAAC,aAAa;AAChB,aAAU,GAAG,EAAE,GAAG,QAAQ;AAC1B;;;AAIJ,KAAI,+BAA+B,8BAA8B,IAAI;AACnE,YACE,kDAAkD,4BAA4B,+CAC/E;AACD,gCAA8B;;CAGhC,IAAI,UAAoB,MAAM,GAAG,YAAY,EAC3C,QAAQ,qBACT,CAAC;AAEF,KAAI,YAAY;EACd,MAAM,kBAAkB,MAAM,aAAa,WAAW;AAEtD,MAAI,gBAIF,WAAU,QAAQ,QAAQ,SACxB,gBAAgB,MAAM,YAAY,KAAK,QAAQ,KAAK,EAAE,KAAK,KAAK,QAAQ,CACzE;;AAML,WAAU,kBAAkB,aAAa,WAAW,GAAG;AACvD,WACE,aAAa,eAAe,QAAQ,OAAO,CAAC,cAAc,aAAa,QAAQ,CAAC,IACjF;AAED,WAAU,aAAa,eAAe,QAAQ,OAAO,CAAC,SAAS;AAC/D,WAAU,QAAQ,KAAK,SAAS,MAAM,WAAW,KAAK,CAAC,IAAI,CAAC;AAyE5D,OAAM,YAtEW,QAAQ,SAAS,YAChC,QAAQ,KAAK,WAAW,YAAY;AAClC,YACE,mBAAmB,WAAW,QAAQ,CAAC,MAAM,aAAa,OAAO,GAClE;EAED,MAAM,uBAAuB,KAAK,cAAc,OAAO,SAAS,QAAQ;EACxE,MAAM,iBAAiB,kBACrB,sBACA,QACA,WACD;AAGD,MAAI,gBAAgB,WAAW,eAAe,EAAE;GAC9C,MAAM,eAAe,SACnB,cAAc,OAAO,SACrB,eACD;AACD,aACE,GAAG,SAAS,KAAK,WAAW,OAAO,CAAC,QAAQ,WAAW,aAAa,CAAC,4BACtE;AACD;;AAIF,MAAI,WAAW,eAAe,EAAE;GAC9B,MAAM,uBAAuB,uBAAuB,gBAAgB;IAClE;IACA;IACD,CAAC;AAEF,OAAI,qBAAqB,WAAW;AAClC,cAAU,qBAAqB,QAAQ;AACvC;;aAEO,wBAAwB,oBAEjC,WACE,GAAG,SAAS,KAAK,WAAW,OAAO,CAAC,QAAQ,WAAW,eAAe,CAAC,oDACxE;EAGH,IAAI;AAEJ,MAAI,YAAY;GACd,MAAM,kBAAkB,MAAM,aAC5B,sBACA,WACD;AAED,aAAU,sBAAsB,gBAAgB,KAAK,KAAK,GAAG;AAC7D,kBAAe;;AAGjB,QAAM,qBACJ,sBACA,gBACA,QACA,YACA,WACA,eACA,oBACA,cACA,UACA,SACD;GACD,CAIM,GACP,SAAS,MAAM,EAChB,+BAA+B,EAChC"}
1
+ {"version":3,"file":"reviewDoc.mjs","names":[],"sources":["../../../src/reviewDoc/reviewDoc.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { join, relative } from 'node:path';\nimport type { AIConfig } from '@intlayer/ai';\nimport type { AIOptions } from '@intlayer/api';\nimport {\n type ListGitFilesOptions,\n listGitFiles,\n listGitLines,\n logConfigDetails,\n} from '@intlayer/chokidar/cli';\nimport { buildReviewReport } from '@intlayer/chokidar/docReview';\nimport {\n formatLocale,\n formatPath,\n parallelize,\n} from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeNumber,\n getAppLogger,\n x,\n} from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport fg from 'fast-glob';\nimport { checkFileModifiedRange } from '../utils/checkFileModifiedRange';\nimport { formatLineRanges } from '../utils/formatLineRanges';\nimport { getOutputFilePath } from '../utils/getOutputFilePath';\nimport { type AIClient, setupAI } from '../utils/setupAI';\nimport { reviewFileBlockAware } from './reviewDocBlockAware';\nimport { logReviewFileBlocks } from './reviewDocLog';\n\ntype ReviewDocOptions = {\n docPattern: string[];\n locales: Locale[];\n excludedGlobPattern: string[];\n baseLocale: Locale;\n aiOptions?: AIOptions;\n nbSimultaneousFileProcessed?: number;\n configOptions?: GetConfigurationOptions;\n customInstructions?: string;\n skipIfModifiedBefore?: number | string | Date;\n skipIfModifiedAfter?: number | string | Date;\n skipIfExists?: boolean;\n gitOptions?: ListGitFilesOptions;\n /**\n * Log-only mode. Instead of translating the changed blocks with AI, log the\n * blocks that need attention (with line numbers and content) for the base and\n * target locales, so another agent can generate the translations.\n */\n log?: boolean;\n};\n\n/**\n * Main audit function: scans all .md files in \"en/\" (unless you specified DOC_LIST),\n * then audits them to each locale in LOCALE_LIST.\n */\nexport const reviewDoc = async ({\n docPattern,\n locales,\n excludedGlobPattern,\n baseLocale,\n aiOptions,\n nbSimultaneousFileProcessed,\n configOptions,\n customInstructions,\n skipIfModifiedBefore,\n skipIfModifiedAfter,\n skipIfExists,\n gitOptions,\n log,\n}: ReviewDocOptions) => {\n const configuration = getConfiguration(configOptions);\n logConfigDetails(configOptions);\n\n const appLogger = getAppLogger({ log: { ...configuration.log, prefix: '' } });\n\n // Log-only mode does not call any AI, so the AI access checks are skipped.\n let aiClient: AIClient | undefined;\n let aiConfig: AIConfig | undefined;\n\n if (!log) {\n const aiResult = await setupAI(configuration, aiOptions);\n\n if (!aiResult?.hasAIAccess) return;\n\n aiClient = aiResult.aiClient;\n aiConfig = aiResult.aiConfig;\n\n if (aiResult.isCustomAI && aiClient && aiConfig) {\n const { hasAIAccess, error } = await aiClient.checkAISDKAccess(aiConfig);\n if (!hasAIAccess) {\n appLogger(`${x} ${error}`);\n return;\n }\n }\n }\n\n if (nbSimultaneousFileProcessed && nbSimultaneousFileProcessed > 10) {\n appLogger(\n `Warning: nbSimultaneousFileProcessed is set to ${nbSimultaneousFileProcessed}, which is greater than 10. Setting it to 10.`\n );\n nbSimultaneousFileProcessed = 10; // Limit the number of simultaneous file processed to 10\n }\n\n let docList: string[] = await fg(docPattern, {\n ignore: excludedGlobPattern,\n });\n\n if (gitOptions) {\n const gitChangedFiles = await listGitFiles(gitOptions);\n\n if (gitChangedFiles) {\n // Convert dictionary file paths to be relative to git root for comparison\n\n // Filter dictionaries based on git changed files\n docList = docList.filter((path) =>\n gitChangedFiles.some((gitFile) => join(process.cwd(), path) === gitFile)\n );\n }\n }\n\n // OAuth handled by API proxy internally\n\n appLogger(`Base locale is ${formatLocale(baseLocale)}`);\n appLogger(\n `Reviewing ${colorizeNumber(locales.length)} locales: [ ${formatLocale(locales)} ]`\n );\n\n appLogger(`Reviewing ${colorizeNumber(docList.length)} files:`);\n appLogger(docList.map((path) => ` - ${formatPath(path)}\\n`));\n\n // Create all tasks to be processed\n const allTasks = docList.flatMap((docPath) =>\n locales.map((locale) => async () => {\n appLogger(\n `Reviewing file: ${formatPath(docPath)} to ${formatLocale(locale)}`\n );\n\n const absoluteBaseFilePath = join(configuration.system.baseDir, docPath);\n const outputFilePath = getOutputFilePath(\n absoluteBaseFilePath,\n locale,\n baseLocale\n );\n\n // Skip if file exists and skipIfExists option is enabled\n if (skipIfExists && existsSync(outputFilePath)) {\n const relativePath = relative(\n configuration.system.baseDir,\n outputFilePath\n );\n appLogger(\n `${colorize('⊘', ANSIColors.YELLOW)} File ${formatPath(relativePath)} already exists, skipping.`\n );\n return;\n }\n\n // Check modification range only if the file exists\n if (existsSync(outputFilePath)) {\n const fileModificationData = checkFileModifiedRange(outputFilePath, {\n skipIfModifiedBefore,\n skipIfModifiedAfter,\n });\n\n if (fileModificationData.isSkipped) {\n appLogger(fileModificationData.message);\n return;\n }\n } else if (skipIfModifiedBefore || skipIfModifiedAfter) {\n // Log if we intended to check modification time but couldn't because the file doesn't exist\n appLogger(\n `${colorize('!', ANSIColors.YELLOW)} File ${formatPath(outputFilePath)} does not exist, skipping modification date check.`\n );\n }\n\n let changedLines: number[] | undefined;\n // FIXED: Enable git optimization that was previously commented out\n if (gitOptions) {\n const gitChangedLines = await listGitLines(\n absoluteBaseFilePath,\n gitOptions\n );\n changedLines = gitChangedLines;\n\n // Report the base lines that changed, then the corresponding line span\n // in the target document — i.e. the blocks the alignment will actually\n // re-translate. `review` blocks are exactly the aligned base blocks the\n // changed lines touched, so their `targetLineRange` is the matching span\n // in the existing translation.\n appLogger(\n `Changed lines (${formatLocale(baseLocale)}): ${formatLineRanges(gitChangedLines)}`\n );\n\n const baseText = await readFile(absoluteBaseFilePath, 'utf-8').catch(\n () => ''\n );\n const targetText = existsSync(outputFilePath)\n ? await readFile(outputFilePath, 'utf-8').catch(() => '')\n : '';\n\n const { blocks } = buildReviewReport({\n baseText,\n targetText,\n changedLines: gitChangedLines,\n });\n\n const correspondingTargetLines = blocks.flatMap((block) => {\n if (block.action !== 'review' || !block.targetLineRange) return [];\n\n const { start, end } = block.targetLineRange;\n return Array.from(\n { length: end - start + 1 },\n (_unused, offset) => start + offset\n );\n });\n\n appLogger(\n `Corresponding block (${formatLocale(locale)}): ${formatLineRanges(correspondingTargetLines)}`\n );\n }\n\n if (log) {\n await logReviewFileBlocks(\n absoluteBaseFilePath,\n outputFilePath,\n locale as Locale,\n baseLocale,\n configOptions,\n changedLines\n );\n return;\n }\n\n await reviewFileBlockAware(\n absoluteBaseFilePath,\n outputFilePath,\n locale as Locale,\n baseLocale,\n aiOptions,\n configOptions,\n customInstructions,\n changedLines,\n aiClient,\n aiConfig\n );\n })\n );\n\n await parallelize(\n allTasks,\n (task) => task(),\n nbSimultaneousFileProcessed ?? 3\n );\n};\n"],"mappings":"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"}
@@ -1,7 +1,5 @@
1
1
  import { readAsset } from "../_virtual/_utils_asset.mjs";
2
2
  import { sanitizeChunk, validateTranslation } from "../translateDoc/validation.mjs";
3
- import { mergeReviewedSegments } from "../translation-alignment/rebuildDocument.mjs";
4
- import { buildAlignmentPlan } from "../translation-alignment/pipeline.mjs";
5
3
  import { chunkInference } from "../utils/chunkInference.mjs";
6
4
  import { fixChunkStartEndChars } from "../utils/fixChunkStartEndChars.mjs";
7
5
  import { mkdirSync, writeFileSync } from "node:fs";
@@ -12,48 +10,69 @@ import { colon, colorize, colorizeNumber, getAppLogger } from "@intlayer/config/
12
10
  import { getConfiguration } from "@intlayer/config/node";
13
11
  import { retryManager } from "@intlayer/config/utils";
14
12
  import { readFile } from "node:fs/promises";
13
+ import { buildAlignmentPlan, mergeReviewedSegments } from "@intlayer/chokidar/docReview";
15
14
  import { getLocaleName } from "@intlayer/core/localization";
16
15
  import { ENGLISH } from "@intlayer/types/locales";
17
16
 
18
17
  //#region src/reviewDoc/reviewDocBlockAware.ts
19
18
  /**
20
19
  * Review a file using block-aware alignment.
21
- * This approach:
22
- * 1. Segments both English and French documents into semantic blocks
23
- * 2. Aligns blocks using structure (special chars, numbers) and context
24
- * 3. Detects which blocks changed, were added, or deleted
25
- * 4. Only sends changed/new blocks to AI for translation
26
- * 5. Handles reordering automatically
20
+ *
21
+ * 1. Segments both base and target documents into semantic blocks.
22
+ * 2. Aligns blocks using structure (special chars, numbers) and context.
23
+ * 3. Detects which blocks changed, were added, or deleted.
24
+ * 4. Applies deletions immediately without AI.
25
+ * 5. Sends changed/new blocks to AI in bottom-up order (last block first), so
26
+ * line numbers of earlier blocks are not shifted by edits below them.
27
+ * 6. Rewrites the file after each block so progress is persisted incrementally.
27
28
  */
28
29
  const reviewFileBlockAware = async (baseFilePath, outputFilePath, locale, baseLocale, aiOptions, configOptions, customInstructions, changedLines, aiClient, aiConfig) => {
29
30
  const configuration = getConfiguration(configOptions);
30
- const applicationLogger = getAppLogger(configuration);
31
- const englishText = await readFile(baseFilePath, "utf-8");
32
- const frenchText = await readFile(outputFilePath, "utf-8").catch(() => "");
31
+ const applicationLogger = getAppLogger({ log: {
32
+ ...configuration.log,
33
+ prefix: ""
34
+ } });
35
+ const baseText = await readFile(baseFilePath, "utf-8");
36
+ const targetText = await readFile(outputFilePath, "utf-8").catch(() => "");
33
37
  const basePrompt = readAsset("./prompts/REVIEW_PROMPT.md", "utf-8").replaceAll("{{localeName}}", `${formatLocale(locale, false)}`).replaceAll("{{baseLocaleName}}", `${formatLocale(baseLocale, false)}`).replace("{{applicationContext}}", aiOptions?.applicationContext ?? "-").replace("{{customInstructions}}", customInstructions ?? "-");
34
38
  const filePrefix = [colon(`${ANSIColors.GREY_DARK}[${formatPath(baseFilePath)}${ANSIColors.GREY_DARK}] `, { colSize: 40 }), `→ ${ANSIColors.RESET}`].join("");
35
39
  const prefix = [colon(`${ANSIColors.GREY_DARK}[${formatPath(baseFilePath)}${ANSIColors.GREY_DARK}][${formatLocale(locale)}${ANSIColors.GREY_DARK}] `, { colSize: 40 }), `→ ${ANSIColors.RESET}`].join("");
36
- const { englishBlocks, frenchBlocks, plan, segmentsToReview } = buildAlignmentPlan({
37
- englishText,
38
- frenchText,
40
+ const { baseBlocks, targetBlocks, plan, segmentsToReview } = buildAlignmentPlan({
41
+ baseText,
42
+ targetText,
39
43
  changedLines
40
44
  });
41
- applicationLogger(`${filePrefix}Block-aware alignment complete. Total blocks: EN=${colorizeNumber(englishBlocks.length)}, FR=${colorizeNumber(frenchBlocks.length)}`);
42
- applicationLogger(`${filePrefix}Actions: reuse=${colorizeNumber(plan.actions.filter((a) => a.kind === "reuse").length)}, review=${colorizeNumber(plan.actions.filter((a) => a.kind === "review").length)}, new=${colorizeNumber(plan.actions.filter((a) => a.kind === "insert_new").length)}, delete=${colorizeNumber(plan.actions.filter((a) => a.kind === "delete").length)}`);
43
- if (segmentsToReview.length === 0) {
44
- applicationLogger(`${filePrefix}No segments need review, reusing existing translation`);
45
+ const deleteCount = plan.actions.filter((a) => a.kind === "delete").length;
46
+ applicationLogger(`${filePrefix}Block-aware alignment complete. Total blocks: base=${colorizeNumber(baseBlocks.length)}, target=${colorizeNumber(targetBlocks.length)}`);
47
+ applicationLogger(`${filePrefix}Actions: reuse=${colorizeNumber(plan.actions.filter((a) => a.kind === "reuse").length)}, review=${colorizeNumber(plan.actions.filter((a) => a.kind === "review").length)}, new=${colorizeNumber(plan.actions.filter((a) => a.kind === "insert_new").length)}, delete=${colorizeNumber(deleteCount)}`);
48
+ const reviewedSegmentsMap = /* @__PURE__ */ new Map();
49
+ for (const [actionIndex, action] of plan.actions.entries()) if (action.kind === "delete") reviewedSegmentsMap.set(actionIndex, "");
50
+ const writeCurrentState = () => {
51
+ const output = mergeReviewedSegments(plan, targetBlocks, reviewedSegmentsMap);
45
52
  mkdirSync(dirname(outputFilePath), { recursive: true });
46
- writeFileSync(outputFilePath, mergeReviewedSegments(plan, frenchBlocks, /* @__PURE__ */ new Map()));
47
- applicationLogger(`${colorize("✔", ANSIColors.GREEN)} File ${formatPath(outputFilePath)} updated successfully (no changes needed).`);
53
+ writeFileSync(outputFilePath, output);
54
+ };
55
+ if (deleteCount > 0) {
56
+ writeCurrentState();
57
+ applicationLogger(`${filePrefix}${colorizeNumber(deleteCount)} block(s) deleted without AI.`);
58
+ }
59
+ if (segmentsToReview.length === 0) {
60
+ if (deleteCount === 0) {
61
+ applicationLogger(`${filePrefix}No segments need review, reusing existing translation`);
62
+ writeCurrentState();
63
+ }
64
+ applicationLogger(`${colorize("✔", ANSIColors.GREEN)} File ${formatPath(outputFilePath)} updated successfully (no AI changes needed).`);
48
65
  return;
49
66
  }
50
- applicationLogger(`${filePrefix}Segments to review: ${colorizeNumber(segmentsToReview.length)}`);
51
- const reviewedSegmentsMap = /* @__PURE__ */ new Map();
52
- for (const segment of segmentsToReview) {
53
- const segmentNumber = segmentsToReview.indexOf(segment) + 1;
54
- const englishBlock = segment.englishBlock;
55
- const getBaseChunkContextPrompt = () => `**BLOCK ${segmentNumber} of ${segmentsToReview.length}** is the base block in ${formatLocale(baseLocale, false)} as reference.\n///chunksStart///\n` + englishBlock.content + `///chunksEnd///`;
56
- const getFrenchChunkPrompt = () => `**BLOCK ${segmentNumber} of ${segmentsToReview.length}** is the current block to review in ${formatLocale(locale, false)}.\n///chunksStart///\n` + (segment.frenchBlockText ?? "") + `///chunksEnd///`;
67
+ applicationLogger(`${filePrefix}Segments to review: ${colorizeNumber(segmentsToReview.length)} (processing bottom-up)`);
68
+ const segmentsBottomUp = segmentsToReview.map((segment, originalIndex) => ({
69
+ segment,
70
+ displayNumber: originalIndex + 1
71
+ })).reverse();
72
+ for (const { segment, displayNumber } of segmentsBottomUp) {
73
+ const baseBlock = segment.baseBlock;
74
+ const getBaseChunkContextPrompt = () => `**BLOCK ${displayNumber} of ${segmentsToReview.length}** is the base block in ${formatLocale(baseLocale, false)} as reference.\n///chunksStart///\n` + baseBlock.content + `///chunksEnd///`;
75
+ const getTargetChunkPrompt = () => `**BLOCK ${displayNumber} of ${segmentsToReview.length}** is the current block to review in ${formatLocale(locale, false)}.\n///chunksStart///\n` + (segment.targetBlockText ?? "") + `///chunksEnd///`;
57
76
  const reviewedChunkResult = await retryManager(async () => {
58
77
  const result = await chunkInference([
59
78
  {
@@ -66,27 +85,25 @@ const reviewFileBlockAware = async (baseFilePath, outputFilePath, locale, baseLo
66
85
  },
67
86
  {
68
87
  role: "system",
69
- content: getFrenchChunkPrompt()
88
+ content: getTargetChunkPrompt()
70
89
  },
71
90
  {
72
91
  role: "system",
73
- content: `The next user message will be the **BLOCK ${colorizeNumber(segmentNumber)} of ${colorizeNumber(segmentsToReview.length)}** that should be translated in ${getLocaleName(locale, ENGLISH)} (${locale}).`
92
+ content: `The next user message will be the **BLOCK ${colorizeNumber(displayNumber)} of ${colorizeNumber(segmentsToReview.length)}** that should be translated in ${getLocaleName(locale, ENGLISH)} (${locale}).`
74
93
  }
75
94
  ], [{
76
95
  role: "user",
77
- content: englishBlock.content
96
+ content: baseBlock.content
78
97
  }], aiOptions, configuration, aiClient, aiConfig);
79
- applicationLogger(`${prefix}${colorizeNumber(result.tokenUsed)} tokens used - Block ${colorizeNumber(segmentNumber)} of ${colorizeNumber(segmentsToReview.length)}`);
80
- let processedChunk = sanitizeChunk(result?.fileContent, englishBlock.content);
81
- processedChunk = fixChunkStartEndChars(processedChunk, englishBlock.content);
82
- if (!validateTranslation(englishBlock.content, processedChunk, applicationLogger)) throw new Error("Validation failed for chunk (structure or length mismatch). Retrying...");
98
+ applicationLogger(`${prefix}${colorizeNumber(result.tokenUsed)} tokens used - Block ${colorizeNumber(displayNumber)} of ${colorizeNumber(segmentsToReview.length)}`);
99
+ let processedChunk = sanitizeChunk(result?.fileContent, baseBlock.content);
100
+ processedChunk = fixChunkStartEndChars(processedChunk, baseBlock.content);
101
+ if (!validateTranslation(baseBlock.content, processedChunk, applicationLogger)) throw new Error("Validation failed for chunk (structure or length mismatch). Retrying...");
83
102
  return processedChunk;
84
103
  })();
85
104
  reviewedSegmentsMap.set(segment.actionIndex, reviewedChunkResult);
105
+ writeCurrentState();
86
106
  }
87
- const finalFrenchOutput = mergeReviewedSegments(plan, frenchBlocks, reviewedSegmentsMap);
88
- mkdirSync(dirname(outputFilePath), { recursive: true });
89
- writeFileSync(outputFilePath, finalFrenchOutput);
90
107
  applicationLogger(`${colorize("✔", ANSIColors.GREEN)} File ${formatPath(outputFilePath)} created/updated successfully.`);
91
108
  };
92
109
 
@@ -1 +1 @@
1
- {"version":3,"file":"reviewDocBlockAware.mjs","names":[],"sources":["../../../src/reviewDoc/reviewDocBlockAware.ts"],"sourcesContent":["import { mkdirSync, writeFileSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport { readAsset } from 'utils:asset';\nimport type { AIConfig } from '@intlayer/ai';\nimport type { AIOptions } from '@intlayer/api';\nimport { formatLocale, formatPath } from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colon,\n colorize,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport { retryManager } from '@intlayer/config/utils';\nimport { getLocaleName } from '@intlayer/core/localization';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport { ENGLISH } from '@intlayer/types/locales';\nimport { sanitizeChunk, validateTranslation } from '../translateDoc/validation';\nimport {\n buildAlignmentPlan,\n mergeReviewedSegments,\n} from '../translation-alignment/pipeline';\nimport { chunkInference } from '../utils/chunkInference';\nimport { fixChunkStartEndChars } from '../utils/fixChunkStartEndChars';\nimport type { AIClient } from '../utils/setupAI';\n\n/**\n * Review a file using block-aware alignment.\n * This approach:\n * 1. Segments both English and French documents into semantic blocks\n * 2. Aligns blocks using structure (special chars, numbers) and context\n * 3. Detects which blocks changed, were added, or deleted\n * 4. Only sends changed/new blocks to AI for translation\n * 5. Handles reordering automatically\n */\nexport const reviewFileBlockAware = async (\n baseFilePath: string,\n outputFilePath: string,\n locale: Locale,\n baseLocale: Locale,\n aiOptions?: AIOptions,\n configOptions?: GetConfigurationOptions,\n customInstructions?: string,\n changedLines?: number[],\n aiClient?: AIClient,\n aiConfig?: AIConfig\n) => {\n const configuration = getConfiguration(configOptions);\n const applicationLogger = getAppLogger(configuration);\n\n const englishText = await readFile(baseFilePath, 'utf-8');\n const frenchText = await readFile(outputFilePath, 'utf-8').catch(() => '');\n\n const basePrompt = readAsset('./prompts/REVIEW_PROMPT.md', 'utf-8')\n .replaceAll('{{localeName}}', `${formatLocale(locale, false)}`)\n .replaceAll('{{baseLocaleName}}', `${formatLocale(baseLocale, false)}`)\n .replace('{{applicationContext}}', aiOptions?.applicationContext ?? '-')\n .replace('{{customInstructions}}', customInstructions ?? '-');\n\n const filePrefixText = `${ANSIColors.GREY_DARK}[${formatPath(baseFilePath)}${ANSIColors.GREY_DARK}] `;\n const filePrefix = [\n colon(filePrefixText, { colSize: 40 }),\n `→ ${ANSIColors.RESET}`,\n ].join('');\n const prefixText = `${ANSIColors.GREY_DARK}[${formatPath(baseFilePath)}${ANSIColors.GREY_DARK}][${formatLocale(locale)}${ANSIColors.GREY_DARK}] `;\n const prefix = [\n colon(prefixText, { colSize: 40 }),\n `→ ${ANSIColors.RESET}`,\n ].join('');\n\n // Build block-aware alignment and plan\n const { englishBlocks, frenchBlocks, plan, segmentsToReview } =\n buildAlignmentPlan({\n englishText,\n frenchText,\n changedLines,\n });\n\n applicationLogger(\n `${filePrefix}Block-aware alignment complete. Total blocks: EN=${colorizeNumber(englishBlocks.length)}, FR=${colorizeNumber(frenchBlocks.length)}`\n );\n applicationLogger(\n `${filePrefix}Actions: reuse=${colorizeNumber(plan.actions.filter((a) => a.kind === 'reuse').length)}, review=${colorizeNumber(plan.actions.filter((a) => a.kind === 'review').length)}, new=${colorizeNumber(plan.actions.filter((a) => a.kind === 'insert_new').length)}, delete=${colorizeNumber(plan.actions.filter((a) => a.kind === 'delete').length)}`\n );\n\n if (segmentsToReview.length === 0) {\n applicationLogger(\n `${filePrefix}No segments need review, reusing existing translation`\n );\n mkdirSync(dirname(outputFilePath), { recursive: true });\n writeFileSync(\n outputFilePath,\n mergeReviewedSegments(plan, frenchBlocks, new Map())\n );\n applicationLogger(\n `${colorize('✔', ANSIColors.GREEN)} File ${formatPath(outputFilePath)} updated successfully (no changes needed).`\n );\n return;\n }\n\n applicationLogger(\n `${filePrefix}Segments to review: ${colorizeNumber(segmentsToReview.length)}`\n );\n\n // Review segments that need AI translation\n const reviewedSegmentsMap = new Map<number, string>();\n\n for (const segment of segmentsToReview) {\n const segmentNumber = segmentsToReview.indexOf(segment) + 1;\n const englishBlock = segment.englishBlock;\n\n const getBaseChunkContextPrompt = () =>\n `**BLOCK ${segmentNumber} of ${segmentsToReview.length}** is the base block in ${formatLocale(baseLocale, false)} as reference.\\n` +\n `///chunksStart///\\n` +\n englishBlock.content +\n `///chunksEnd///`;\n\n const getFrenchChunkPrompt = () =>\n `**BLOCK ${segmentNumber} of ${segmentsToReview.length}** is the current block to review in ${formatLocale(locale, false)}.\\n` +\n `///chunksStart///\\n` +\n (segment.frenchBlockText ?? '') +\n `///chunksEnd///`;\n\n const reviewedChunkResult = await retryManager(async () => {\n const result = await chunkInference(\n [\n { role: 'system', content: basePrompt },\n { role: 'system', content: getBaseChunkContextPrompt() },\n { role: 'system', content: getFrenchChunkPrompt() },\n {\n role: 'system',\n content: `The next user message will be the **BLOCK ${colorizeNumber(segmentNumber)} of ${colorizeNumber(segmentsToReview.length)}** that should be translated in ${getLocaleName(locale, ENGLISH)} (${locale}).`,\n },\n ],\n [{ role: 'user', content: englishBlock.content }],\n aiOptions,\n configuration,\n aiClient,\n aiConfig\n );\n\n applicationLogger(\n `${prefix}${colorizeNumber(result.tokenUsed)} tokens used - Block ${colorizeNumber(segmentNumber)} of ${colorizeNumber(segmentsToReview.length)}`\n );\n\n // Sanitize artifacts (e.g. Markdown code block wrappers)\n let processedChunk = sanitizeChunk(\n result?.fileContent,\n englishBlock.content\n );\n\n // Fix start/end characters\n processedChunk = fixChunkStartEndChars(\n processedChunk,\n englishBlock.content\n );\n\n // Validate Translation (YAML, Code fences, Length ratio)\n const isValid = validateTranslation(\n englishBlock.content,\n processedChunk,\n applicationLogger\n );\n\n if (!isValid) {\n throw new Error(\n 'Validation failed for chunk (structure or length mismatch). Retrying...'\n );\n }\n\n return processedChunk;\n })();\n\n reviewedSegmentsMap.set(segment.actionIndex, reviewedChunkResult);\n }\n\n // Merge reviewed segments back into final document\n const finalFrenchOutput = mergeReviewedSegments(\n plan,\n frenchBlocks,\n reviewedSegmentsMap\n );\n\n mkdirSync(dirname(outputFilePath), { recursive: true });\n writeFileSync(outputFilePath, finalFrenchOutput);\n\n applicationLogger(\n `${colorize('✔', ANSIColors.GREEN)} File ${formatPath(outputFilePath)} created/updated successfully.`\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"reviewDocBlockAware.mjs","names":[],"sources":["../../../src/reviewDoc/reviewDocBlockAware.ts"],"sourcesContent":["import { mkdirSync, writeFileSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport { readAsset } from 'utils:asset';\nimport type { AIConfig } from '@intlayer/ai';\nimport type { AIOptions } from '@intlayer/api';\nimport {\n buildAlignmentPlan,\n mergeReviewedSegments,\n} from '@intlayer/chokidar/docReview';\nimport { formatLocale, formatPath } from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colon,\n colorize,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport { retryManager } from '@intlayer/config/utils';\nimport { getLocaleName } from '@intlayer/core/localization';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport { ENGLISH } from '@intlayer/types/locales';\nimport { sanitizeChunk, validateTranslation } from '../translateDoc/validation';\nimport { chunkInference } from '../utils/chunkInference';\nimport { fixChunkStartEndChars } from '../utils/fixChunkStartEndChars';\nimport type { AIClient } from '../utils/setupAI';\n\n/**\n * Review a file using block-aware alignment.\n *\n * 1. Segments both base and target documents into semantic blocks.\n * 2. Aligns blocks using structure (special chars, numbers) and context.\n * 3. Detects which blocks changed, were added, or deleted.\n * 4. Applies deletions immediately without AI.\n * 5. Sends changed/new blocks to AI in bottom-up order (last block first), so\n * line numbers of earlier blocks are not shifted by edits below them.\n * 6. Rewrites the file after each block so progress is persisted incrementally.\n */\nexport const reviewFileBlockAware = async (\n baseFilePath: string,\n outputFilePath: string,\n locale: Locale,\n baseLocale: Locale,\n aiOptions?: AIOptions,\n configOptions?: GetConfigurationOptions,\n customInstructions?: string,\n changedLines?: number[],\n aiClient?: AIClient,\n aiConfig?: AIConfig\n) => {\n const configuration = getConfiguration(configOptions);\n const applicationLogger = getAppLogger({\n log: { ...configuration.log, prefix: '' },\n });\n\n const baseText = await readFile(baseFilePath, 'utf-8');\n const targetText = await readFile(outputFilePath, 'utf-8').catch(() => '');\n\n const basePrompt = readAsset('./prompts/REVIEW_PROMPT.md', 'utf-8')\n .replaceAll('{{localeName}}', `${formatLocale(locale, false)}`)\n .replaceAll('{{baseLocaleName}}', `${formatLocale(baseLocale, false)}`)\n .replace('{{applicationContext}}', aiOptions?.applicationContext ?? '-')\n .replace('{{customInstructions}}', customInstructions ?? '-');\n\n const filePrefixText = `${ANSIColors.GREY_DARK}[${formatPath(baseFilePath)}${ANSIColors.GREY_DARK}] `;\n const filePrefix = [\n colon(filePrefixText, { colSize: 40 }),\n `→ ${ANSIColors.RESET}`,\n ].join('');\n const prefixText = `${ANSIColors.GREY_DARK}[${formatPath(baseFilePath)}${ANSIColors.GREY_DARK}][${formatLocale(locale)}${ANSIColors.GREY_DARK}] `;\n const prefix = [\n colon(prefixText, { colSize: 40 }),\n `→ ${ANSIColors.RESET}`,\n ].join('');\n\n // Build block-aware alignment and plan\n const { baseBlocks, targetBlocks, plan, segmentsToReview } =\n buildAlignmentPlan({\n baseText,\n targetText,\n changedLines,\n });\n\n const deleteCount = plan.actions.filter((a) => a.kind === 'delete').length;\n\n applicationLogger(\n `${filePrefix}Block-aware alignment complete. Total blocks: base=${colorizeNumber(baseBlocks.length)}, target=${colorizeNumber(targetBlocks.length)}`\n );\n applicationLogger(\n `${filePrefix}Actions: reuse=${colorizeNumber(plan.actions.filter((a) => a.kind === 'reuse').length)}, review=${colorizeNumber(plan.actions.filter((a) => a.kind === 'review').length)}, new=${colorizeNumber(plan.actions.filter((a) => a.kind === 'insert_new').length)}, delete=${colorizeNumber(deleteCount)}`\n );\n\n // Map shared across the entire run: each entry overrides the default behavior\n // of mergeReviewedSegments for that action index.\n const reviewedSegmentsMap = new Map<number, string>();\n\n // --- Step 1: apply deletions immediately (no AI needed) ---\n for (const [actionIndex, action] of plan.actions.entries()) {\n if (action.kind === 'delete') {\n reviewedSegmentsMap.set(actionIndex, '');\n }\n }\n\n const writeCurrentState = (): void => {\n const output = mergeReviewedSegments(\n plan,\n targetBlocks,\n reviewedSegmentsMap\n );\n mkdirSync(dirname(outputFilePath), { recursive: true });\n writeFileSync(outputFilePath, output);\n };\n\n if (deleteCount > 0) {\n writeCurrentState();\n applicationLogger(\n `${filePrefix}${colorizeNumber(deleteCount)} block(s) deleted without AI.`\n );\n }\n\n if (segmentsToReview.length === 0) {\n if (deleteCount === 0) {\n applicationLogger(\n `${filePrefix}No segments need review, reusing existing translation`\n );\n writeCurrentState();\n }\n applicationLogger(\n `${colorize('✔', ANSIColors.GREEN)} File ${formatPath(outputFilePath)} updated successfully (no AI changes needed).`\n );\n return;\n }\n\n applicationLogger(\n `${filePrefix}Segments to review: ${colorizeNumber(segmentsToReview.length)} (processing bottom-up)`\n );\n\n // --- Step 2: process AI segments in bottom-up order ---\n // Reversing ensures edits near the end of the file don't shift line numbers\n // that matter for blocks higher up, and each intermediate file write is valid.\n const segmentsBottomUp = segmentsToReview\n .map((segment, originalIndex) => ({\n segment,\n displayNumber: originalIndex + 1,\n }))\n .reverse();\n\n for (const { segment, displayNumber } of segmentsBottomUp) {\n const baseBlock = segment.baseBlock;\n\n const getBaseChunkContextPrompt = () =>\n `**BLOCK ${displayNumber} of ${segmentsToReview.length}** is the base block in ${formatLocale(baseLocale, false)} as reference.\\n` +\n `///chunksStart///\\n` +\n baseBlock.content +\n `///chunksEnd///`;\n\n const getTargetChunkPrompt = () =>\n `**BLOCK ${displayNumber} of ${segmentsToReview.length}** is the current block to review in ${formatLocale(locale, false)}.\\n` +\n `///chunksStart///\\n` +\n (segment.targetBlockText ?? '') +\n `///chunksEnd///`;\n\n const reviewedChunkResult = await retryManager(async () => {\n const result = await chunkInference(\n [\n { role: 'system', content: basePrompt },\n { role: 'system', content: getBaseChunkContextPrompt() },\n { role: 'system', content: getTargetChunkPrompt() },\n {\n role: 'system',\n content: `The next user message will be the **BLOCK ${colorizeNumber(displayNumber)} of ${colorizeNumber(segmentsToReview.length)}** that should be translated in ${getLocaleName(locale, ENGLISH)} (${locale}).`,\n },\n ],\n [{ role: 'user', content: baseBlock.content }],\n aiOptions,\n configuration,\n aiClient,\n aiConfig\n );\n\n applicationLogger(\n `${prefix}${colorizeNumber(result.tokenUsed)} tokens used - Block ${colorizeNumber(displayNumber)} of ${colorizeNumber(segmentsToReview.length)}`\n );\n\n let processedChunk = sanitizeChunk(\n result?.fileContent,\n baseBlock.content\n );\n processedChunk = fixChunkStartEndChars(processedChunk, baseBlock.content);\n\n const isValid = validateTranslation(\n baseBlock.content,\n processedChunk,\n applicationLogger\n );\n\n if (!isValid) {\n throw new Error(\n 'Validation failed for chunk (structure or length mismatch). Retrying...'\n );\n }\n\n return processedChunk;\n })();\n\n reviewedSegmentsMap.set(segment.actionIndex, reviewedChunkResult);\n\n // Rewrite the file after every block so progress is never lost.\n writeCurrentState();\n }\n\n applicationLogger(\n `${colorize('✔', ANSIColors.GREEN)} File ${formatPath(outputFilePath)} created/updated successfully.`\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAa,uBAAuB,OAClC,cACA,gBACA,QACA,YACA,WACA,eACA,oBACA,cACA,UACA,aACG;CACH,MAAM,gBAAgB,iBAAiB,cAAc;CACrD,MAAM,oBAAoB,aAAa,EACrC,KAAK;EAAE,GAAG,cAAc;EAAK,QAAQ;EAAI,EAC1C,CAAC;CAEF,MAAM,WAAW,MAAM,SAAS,cAAc,QAAQ;CACtD,MAAM,aAAa,MAAM,SAAS,gBAAgB,QAAQ,CAAC,YAAY,GAAG;CAE1E,MAAM,aAAa,UAAU,8BAA8B,QAAQ,CAChE,WAAW,kBAAkB,GAAG,aAAa,QAAQ,MAAM,GAAG,CAC9D,WAAW,sBAAsB,GAAG,aAAa,YAAY,MAAM,GAAG,CACtE,QAAQ,0BAA0B,WAAW,sBAAsB,IAAI,CACvE,QAAQ,0BAA0B,sBAAsB,IAAI;CAG/D,MAAM,aAAa,CACjB,MAAM,GAFkB,WAAW,UAAU,GAAG,WAAW,aAAa,GAAG,WAAW,UAAU,KAE1E,EAAE,SAAS,IAAI,CAAC,EACtC,KAAK,WAAW,QACjB,CAAC,KAAK,GAAG;CAEV,MAAM,SAAS,CACb,MAAM,GAFc,WAAW,UAAU,GAAG,WAAW,aAAa,GAAG,WAAW,UAAU,IAAI,aAAa,OAAO,GAAG,WAAW,UAAU,KAE1H,EAAE,SAAS,IAAI,CAAC,EAClC,KAAK,WAAW,QACjB,CAAC,KAAK,GAAG;CAGV,MAAM,EAAE,YAAY,cAAc,MAAM,qBACtC,mBAAmB;EACjB;EACA;EACA;EACD,CAAC;CAEJ,MAAM,cAAc,KAAK,QAAQ,QAAQ,MAAM,EAAE,SAAS,SAAS,CAAC;AAEpE,mBACE,GAAG,WAAW,qDAAqD,eAAe,WAAW,OAAO,CAAC,WAAW,eAAe,aAAa,OAAO,GACpJ;AACD,mBACE,GAAG,WAAW,iBAAiB,eAAe,KAAK,QAAQ,QAAQ,MAAM,EAAE,SAAS,QAAQ,CAAC,OAAO,CAAC,WAAW,eAAe,KAAK,QAAQ,QAAQ,MAAM,EAAE,SAAS,SAAS,CAAC,OAAO,CAAC,QAAQ,eAAe,KAAK,QAAQ,QAAQ,MAAM,EAAE,SAAS,aAAa,CAAC,OAAO,CAAC,WAAW,eAAe,YAAY,GACjT;CAID,MAAM,sCAAsB,IAAI,KAAqB;AAGrD,MAAK,MAAM,CAAC,aAAa,WAAW,KAAK,QAAQ,SAAS,CACxD,KAAI,OAAO,SAAS,SAClB,qBAAoB,IAAI,aAAa,GAAG;CAI5C,MAAM,0BAAgC;EACpC,MAAM,SAAS,sBACb,MACA,cACA,oBACD;AACD,YAAU,QAAQ,eAAe,EAAE,EAAE,WAAW,MAAM,CAAC;AACvD,gBAAc,gBAAgB,OAAO;;AAGvC,KAAI,cAAc,GAAG;AACnB,qBAAmB;AACnB,oBACE,GAAG,aAAa,eAAe,YAAY,CAAC,+BAC7C;;AAGH,KAAI,iBAAiB,WAAW,GAAG;AACjC,MAAI,gBAAgB,GAAG;AACrB,qBACE,GAAG,WAAW,uDACf;AACD,sBAAmB;;AAErB,oBACE,GAAG,SAAS,KAAK,WAAW,MAAM,CAAC,QAAQ,WAAW,eAAe,CAAC,+CACvE;AACD;;AAGF,mBACE,GAAG,WAAW,sBAAsB,eAAe,iBAAiB,OAAO,CAAC,yBAC7E;CAKD,MAAM,mBAAmB,iBACtB,KAAK,SAAS,mBAAmB;EAChC;EACA,eAAe,gBAAgB;EAChC,EAAE,CACF,SAAS;AAEZ,MAAK,MAAM,EAAE,SAAS,mBAAmB,kBAAkB;EACzD,MAAM,YAAY,QAAQ;EAE1B,MAAM,kCACJ,WAAW,cAAc,MAAM,iBAAiB,OAAO,0BAA0B,aAAa,YAAY,MAAM,CAAC,uCAEjH,UAAU,UACV;EAEF,MAAM,6BACJ,WAAW,cAAc,MAAM,iBAAiB,OAAO,uCAAuC,aAAa,QAAQ,MAAM,CAAC,2BAEzH,QAAQ,mBAAmB,MAC5B;EAEF,MAAM,sBAAsB,MAAM,aAAa,YAAY;GACzD,MAAM,SAAS,MAAM,eACnB;IACE;KAAE,MAAM;KAAU,SAAS;KAAY;IACvC;KAAE,MAAM;KAAU,SAAS,2BAA2B;KAAE;IACxD;KAAE,MAAM;KAAU,SAAS,sBAAsB;KAAE;IACnD;KACE,MAAM;KACN,SAAS,6CAA6C,eAAe,cAAc,CAAC,MAAM,eAAe,iBAAiB,OAAO,CAAC,kCAAkC,cAAc,QAAQ,QAAQ,CAAC,IAAI,OAAO;KAC/M;IACF,EACD,CAAC;IAAE,MAAM;IAAQ,SAAS,UAAU;IAAS,CAAC,EAC9C,WACA,eACA,UACA,SACD;AAED,qBACE,GAAG,SAAS,eAAe,OAAO,UAAU,CAAC,uBAAuB,eAAe,cAAc,CAAC,MAAM,eAAe,iBAAiB,OAAO,GAChJ;GAED,IAAI,iBAAiB,cACnB,QAAQ,aACR,UAAU,QACX;AACD,oBAAiB,sBAAsB,gBAAgB,UAAU,QAAQ;AAQzE,OAAI,CANY,oBACd,UAAU,SACV,gBACA,kBAGU,CACV,OAAM,IAAI,MACR,0EACD;AAGH,UAAO;IACP,EAAE;AAEJ,sBAAoB,IAAI,QAAQ,aAAa,oBAAoB;AAGjE,qBAAmB;;AAGrB,mBACE,GAAG,SAAS,KAAK,WAAW,MAAM,CAAC,QAAQ,WAAW,eAAe,CAAC,gCACvE"}
@@ -0,0 +1,46 @@
1
+ import { existsSync } from "node:fs";
2
+ import { formatLocale, formatPath } from "@intlayer/chokidar/utils";
3
+ import { getAppLogger } from "@intlayer/config/logger";
4
+ import { getConfiguration } from "@intlayer/config/node";
5
+ import { readFile } from "node:fs/promises";
6
+ import { buildReviewReport, formatReviewReport } from "@intlayer/chokidar/docReview";
7
+
8
+ //#region src/reviewDoc/reviewDocLog.ts
9
+ /**
10
+ * Log-only review of a single file/locale pair.
11
+ *
12
+ * Instead of calling an AI to translate the changed blocks, this compares the
13
+ * base document with its translation and logs the blocks that need attention
14
+ * (with their line ranges and content) so another agent or a human can generate
15
+ * the missing translations.
16
+ *
17
+ * @param baseFilePath - Absolute path of the base (source) document.
18
+ * @param outputFilePath - Absolute path of the target (translated) document.
19
+ * @param locale - The target locale being reviewed.
20
+ * @param baseLocale - The base locale used as reference.
21
+ * @param configOptions - Optional Intlayer configuration overrides.
22
+ * @param changedLines - 1-based base line numbers that changed (from git), if any.
23
+ * @returns The structured review report.
24
+ */
25
+ const logReviewFileBlocks = async (baseFilePath, outputFilePath, locale, baseLocale, configOptions, changedLines) => {
26
+ const appLogger = getAppLogger({ log: {
27
+ ...getConfiguration(configOptions).log,
28
+ prefix: ""
29
+ } });
30
+ const report = buildReviewReport({
31
+ baseText: await readFile(baseFilePath, "utf-8"),
32
+ targetText: existsSync(outputFilePath) ? await readFile(outputFilePath, "utf-8").catch(() => "") : "",
33
+ changedLines
34
+ });
35
+ const formatted = formatReviewReport(report, {
36
+ baseLabel: formatLocale(baseLocale),
37
+ targetLabel: formatLocale(locale)
38
+ });
39
+ appLogger(`${formatPath(baseFilePath)} → ${formatLocale(locale)}`);
40
+ for (const line of formatted.split("\n")) appLogger(line);
41
+ return report;
42
+ };
43
+
44
+ //#endregion
45
+ export { logReviewFileBlocks };
46
+ //# sourceMappingURL=reviewDocLog.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reviewDocLog.mjs","names":[],"sources":["../../../src/reviewDoc/reviewDocLog.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport {\n buildReviewReport,\n formatReviewReport,\n type ReviewReport,\n} from '@intlayer/chokidar/docReview';\nimport { formatLocale, formatPath } from '@intlayer/chokidar/utils';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport type { Locale } from '@intlayer/types/allLocales';\n\n/**\n * Log-only review of a single file/locale pair.\n *\n * Instead of calling an AI to translate the changed blocks, this compares the\n * base document with its translation and logs the blocks that need attention\n * (with their line ranges and content) so another agent or a human can generate\n * the missing translations.\n *\n * @param baseFilePath - Absolute path of the base (source) document.\n * @param outputFilePath - Absolute path of the target (translated) document.\n * @param locale - The target locale being reviewed.\n * @param baseLocale - The base locale used as reference.\n * @param configOptions - Optional Intlayer configuration overrides.\n * @param changedLines - 1-based base line numbers that changed (from git), if any.\n * @returns The structured review report.\n */\nexport const logReviewFileBlocks = async (\n baseFilePath: string,\n outputFilePath: string,\n locale: Locale,\n baseLocale: Locale,\n configOptions?: GetConfigurationOptions,\n changedLines?: number[]\n): Promise<ReviewReport> => {\n const configuration = getConfiguration(configOptions);\n const appLogger = getAppLogger({ log: { ...configuration.log, prefix: '' } });\n\n const baseText = await readFile(baseFilePath, 'utf-8');\n const targetText = existsSync(outputFilePath)\n ? await readFile(outputFilePath, 'utf-8').catch(() => '')\n : '';\n\n const report = buildReviewReport({ baseText, targetText, changedLines });\n\n const formatted = formatReviewReport(report, {\n baseLabel: formatLocale(baseLocale),\n targetLabel: formatLocale(locale),\n });\n\n appLogger(`${formatPath(baseFilePath)} → ${formatLocale(locale)}`);\n for (const line of formatted.split('\\n')) {\n appLogger(line);\n }\n\n return report;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,sBAAsB,OACjC,cACA,gBACA,QACA,YACA,eACA,iBAC0B;CAE1B,MAAM,YAAY,aAAa,EAAE,KAAK;EAAE,GADlB,iBAAiB,cACiB,CAAC;EAAK,QAAQ;EAAI,EAAE,CAAC;CAO7E,MAAM,SAAS,kBAAkB;EAAE,gBALZ,SAAS,cAAc,QAAQ;EAKT,YAJ1B,WAAW,eAAe,GACzC,MAAM,SAAS,gBAAgB,QAAQ,CAAC,YAAY,GAAG,GACvD;EAEqD;EAAc,CAAC;CAExE,MAAM,YAAY,mBAAmB,QAAQ;EAC3C,WAAW,aAAa,WAAW;EACnC,aAAa,aAAa,OAAO;EAClC,CAAC;AAEF,WAAU,GAAG,WAAW,aAAa,CAAC,KAAK,aAAa,OAAO,GAAG;AAClE,MAAK,MAAM,QAAQ,UAAU,MAAM,KAAK,CACtC,WAAU,KAAK;AAGjB,QAAO"}
@@ -0,0 +1,72 @@
1
+ import * as ANSIColors from "@intlayer/config/colors";
2
+ import { colorize, getAppLogger } from "@intlayer/config/logger";
3
+ import { getConfiguration } from "@intlayer/config/node";
4
+ import { formatSize, scanWebsite } from "@intlayer/chokidar/scan";
5
+
6
+ //#region src/scan.ts
7
+ /** Human-readable labels for each scorable check type. */
8
+ const checkLabels = {
9
+ robots_robotsPresent: "robots.txt present",
10
+ robots_noLocalizedUrlsForgotten: "robots.txt keeps locale paths crawlable",
11
+ sitemap_sitemapPresent: "sitemap.xml present",
12
+ sitemap_noLocalizedUrlsForgotten: "sitemap lists every locale",
13
+ sitemap_hasAlternates: "sitemap has alternate links",
14
+ sitemap_hasXDefault: "sitemap has x-default",
15
+ url_htmlLang: "html lang attribute",
16
+ url_htmlDir: "html dir attribute",
17
+ url_hasCanonical: "canonical link",
18
+ url_hreflang: "hreflang tags",
19
+ url_hasLocalizedLinks: "localized internal links",
20
+ url_hasXDefault: "x-default hreflang",
21
+ url_allAnchorsLocalized: "all internal links localized",
22
+ url_currentLocale: "current locale detected",
23
+ url_unusedBundleContent: "unused bundle locale content"
24
+ };
25
+ const statusIcon = (status) => {
26
+ if (status === "success") return colorize("✓", ANSIColors.GREEN);
27
+ if (status === "warning") return colorize("⚠", ANSIColors.YELLOW);
28
+ return colorize("✗", ANSIColors.RED);
29
+ };
30
+ const scoreColor = (score) => {
31
+ if (score >= 80) return ANSIColors.GREEN;
32
+ if (score >= 50) return ANSIColors.YELLOW;
33
+ return ANSIColors.RED;
34
+ };
35
+ /**
36
+ * Scan a website for i18n/SEO health and bundle weight, printing a formatted
37
+ * report (or JSON with `--json`).
38
+ *
39
+ * @example
40
+ * ```sh
41
+ * npx intlayer scan https://intlayer.org
42
+ * ```
43
+ */
44
+ const scan = async (url, options = {}) => {
45
+ const appLogger = getAppLogger(getConfiguration(options.configOptions));
46
+ const result = await scanWebsite(url, { deep: options.deep });
47
+ if (options.json) {
48
+ appLogger(JSON.stringify(result, null, 2));
49
+ return;
50
+ }
51
+ appLogger(`\n🔍 Scanned ${colorize(result.url, ANSIColors.GREY_LIGHT)} ${colorize(`(${result.mode} mode)`, ANSIColors.GREY)}\n`);
52
+ appLogger(`Score: ${colorize(`${result.score}/100`, scoreColor(result.score))}`);
53
+ appLogger(`Page size: ${colorize(formatSize(result.totalPageSize), ANSIColors.BLUE)} ${colorize(`(HTML ${formatSize(result.htmlSize)})`, ANSIColors.GREY)}`);
54
+ if (result.locales.length > 0) appLogger(`Locales: ${colorize(result.locales.join(", "), ANSIColors.GREEN)}`);
55
+ appLogger("\nChecks:");
56
+ for (const event of result.events) {
57
+ const type = event.type.split("\\")[0];
58
+ if (!type) continue;
59
+ const label = checkLabels[type] ?? type;
60
+ appLogger(` ${statusIcon(event.status)} ${label}`);
61
+ }
62
+ if (result.bundle && result.bundle.totalLocaleSize > 0) {
63
+ const { bundle } = result;
64
+ appLogger("\nBundle locale weight:");
65
+ appLogger(` Translations shipped: ${colorize(formatSize(bundle.totalLocaleSize), ANSIColors.BLUE)}`);
66
+ appLogger(` Unused (other locales): ${colorize(`${formatSize(bundle.totalUnusedLocaleSize)} (${bundle.unusedPercentOfLocale}%)`, bundle.unusedPercentOfLocale > 30 ? ANSIColors.RED : bundle.unusedPercentOfLocale > 0 ? ANSIColors.YELLOW : ANSIColors.GREEN)}`);
67
+ }
68
+ };
69
+
70
+ //#endregion
71
+ export { scan };
72
+ //# sourceMappingURL=scan.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.mjs","names":[],"sources":["../../src/scan.ts"],"sourcesContent":["import {\n formatSize,\n type ScanEvent,\n scanWebsite,\n} from '@intlayer/chokidar/scan';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport type { ConfigurationOptions } from './cli';\n\n/** Options accepted by the {@link scan} command. */\nexport type ScanCommandOptions = {\n /** Disable the deeper puppeteer-based render scan. */\n deep?: boolean;\n /** Output the raw {@link ScanResult} as JSON instead of formatted text. */\n json?: boolean;\n configOptions?: ConfigurationOptions;\n};\n\n/** Human-readable labels for each scorable check type. */\nconst checkLabels: Record<string, string> = {\n robots_robotsPresent: 'robots.txt present',\n robots_noLocalizedUrlsForgotten: 'robots.txt keeps locale paths crawlable',\n sitemap_sitemapPresent: 'sitemap.xml present',\n sitemap_noLocalizedUrlsForgotten: 'sitemap lists every locale',\n sitemap_hasAlternates: 'sitemap has alternate links',\n sitemap_hasXDefault: 'sitemap has x-default',\n url_htmlLang: 'html lang attribute',\n url_htmlDir: 'html dir attribute',\n url_hasCanonical: 'canonical link',\n url_hreflang: 'hreflang tags',\n url_hasLocalizedLinks: 'localized internal links',\n url_hasXDefault: 'x-default hreflang',\n url_allAnchorsLocalized: 'all internal links localized',\n url_currentLocale: 'current locale detected',\n url_unusedBundleContent: 'unused bundle locale content',\n};\n\nconst statusIcon = (status: ScanEvent['status']): string => {\n if (status === 'success') return colorize('✓', ANSIColors.GREEN);\n if (status === 'warning') return colorize('⚠', ANSIColors.YELLOW);\n return colorize('✗', ANSIColors.RED);\n};\n\nconst scoreColor = (score: number) => {\n if (score >= 80) return ANSIColors.GREEN;\n if (score >= 50) return ANSIColors.YELLOW;\n return ANSIColors.RED;\n};\n\n/**\n * Scan a website for i18n/SEO health and bundle weight, printing a formatted\n * report (or JSON with `--json`).\n *\n * @example\n * ```sh\n * npx intlayer scan https://intlayer.org\n * ```\n */\nexport const scan = async (\n url: string,\n options: ScanCommandOptions = {}\n): Promise<void> => {\n const configuration = getConfiguration(options.configOptions);\n const appLogger = getAppLogger(configuration);\n\n const result = await scanWebsite(url, { deep: options.deep });\n\n if (options.json) {\n appLogger(JSON.stringify(result, null, 2));\n return;\n }\n\n appLogger(\n `\\n🔍 Scanned ${colorize(result.url, ANSIColors.GREY_LIGHT)} ${colorize(\n `(${result.mode} mode)`,\n ANSIColors.GREY\n )}\\n`\n );\n\n appLogger(\n `Score: ${colorize(`${result.score}/100`, scoreColor(result.score))}`\n );\n appLogger(\n `Page size: ${colorize(formatSize(result.totalPageSize), ANSIColors.BLUE)} ${colorize(\n `(HTML ${formatSize(result.htmlSize)})`,\n ANSIColors.GREY\n )}`\n );\n if (result.locales.length > 0) {\n appLogger(\n `Locales: ${colorize(result.locales.join(', '), ANSIColors.GREEN)}`\n );\n }\n\n appLogger('\\nChecks:');\n for (const event of result.events) {\n const type = event.type.split('\\\\')[0];\n\n if (!type) continue;\n\n const label = checkLabels[type] ?? type;\n appLogger(` ${statusIcon(event.status)} ${label}`);\n }\n\n if (result.bundle && result.bundle.totalLocaleSize > 0) {\n const { bundle } = result;\n appLogger('\\nBundle locale weight:');\n appLogger(\n ` Translations shipped: ${colorize(formatSize(bundle.totalLocaleSize), ANSIColors.BLUE)}`\n );\n appLogger(\n ` Unused (other locales): ${colorize(\n `${formatSize(bundle.totalUnusedLocaleSize)} (${bundle.unusedPercentOfLocale}%)`,\n bundle.unusedPercentOfLocale > 30\n ? ANSIColors.RED\n : bundle.unusedPercentOfLocale > 0\n ? ANSIColors.YELLOW\n : ANSIColors.GREEN\n )}`\n );\n }\n};\n"],"mappings":";;;;;;;AAoBA,MAAM,cAAsC;CAC1C,sBAAsB;CACtB,iCAAiC;CACjC,wBAAwB;CACxB,kCAAkC;CAClC,uBAAuB;CACvB,qBAAqB;CACrB,cAAc;CACd,aAAa;CACb,kBAAkB;CAClB,cAAc;CACd,uBAAuB;CACvB,iBAAiB;CACjB,yBAAyB;CACzB,mBAAmB;CACnB,yBAAyB;CAC1B;AAED,MAAM,cAAc,WAAwC;AAC1D,KAAI,WAAW,UAAW,QAAO,SAAS,KAAK,WAAW,MAAM;AAChE,KAAI,WAAW,UAAW,QAAO,SAAS,KAAK,WAAW,OAAO;AACjE,QAAO,SAAS,KAAK,WAAW,IAAI;;AAGtC,MAAM,cAAc,UAAkB;AACpC,KAAI,SAAS,GAAI,QAAO,WAAW;AACnC,KAAI,SAAS,GAAI,QAAO,WAAW;AACnC,QAAO,WAAW;;;;;;;;;;;AAYpB,MAAa,OAAO,OAClB,KACA,UAA8B,EAAE,KACd;CAElB,MAAM,YAAY,aADI,iBAAiB,QAAQ,cACH,CAAC;CAE7C,MAAM,SAAS,MAAM,YAAY,KAAK,EAAE,MAAM,QAAQ,MAAM,CAAC;AAE7D,KAAI,QAAQ,MAAM;AAChB,YAAU,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC;AAC1C;;AAGF,WACE,gBAAgB,SAAS,OAAO,KAAK,WAAW,WAAW,CAAC,GAAG,SAC7D,IAAI,OAAO,KAAK,SAChB,WAAW,KACZ,CAAC,IACH;AAED,WACE,UAAU,SAAS,GAAG,OAAO,MAAM,OAAO,WAAW,OAAO,MAAM,CAAC,GACpE;AACD,WACE,cAAc,SAAS,WAAW,OAAO,cAAc,EAAE,WAAW,KAAK,CAAC,GAAG,SAC3E,SAAS,WAAW,OAAO,SAAS,CAAC,IACrC,WAAW,KACZ,GACF;AACD,KAAI,OAAO,QAAQ,SAAS,EAC1B,WACE,YAAY,SAAS,OAAO,QAAQ,KAAK,KAAK,EAAE,WAAW,MAAM,GAClE;AAGH,WAAU,YAAY;AACtB,MAAK,MAAM,SAAS,OAAO,QAAQ;EACjC,MAAM,OAAO,MAAM,KAAK,MAAM,KAAK,CAAC;AAEpC,MAAI,CAAC,KAAM;EAEX,MAAM,QAAQ,YAAY,SAAS;AACnC,YAAU,KAAK,WAAW,MAAM,OAAO,CAAC,GAAG,QAAQ;;AAGrD,KAAI,OAAO,UAAU,OAAO,OAAO,kBAAkB,GAAG;EACtD,MAAM,EAAE,WAAW;AACnB,YAAU,0BAA0B;AACpC,YACE,2BAA2B,SAAS,WAAW,OAAO,gBAAgB,EAAE,WAAW,KAAK,GACzF;AACD,YACE,6BAA6B,SAC3B,GAAG,WAAW,OAAO,sBAAsB,CAAC,IAAI,OAAO,sBAAsB,KAC7E,OAAO,wBAAwB,KAC3B,WAAW,MACX,OAAO,wBAAwB,IAC7B,WAAW,SACX,WAAW,MAClB,GACF"}
@@ -0,0 +1,42 @@
1
+ //#region src/utils/formatLineRanges.ts
2
+ /**
3
+ * Formats a list of line numbers into a compact, human-readable string where
4
+ * runs of consecutive lines are collapsed into ranges.
5
+ *
6
+ * The input is sorted and de-duplicated first, so callers don't need to
7
+ * pre-process it. A run of a single line is printed as the bare number; a run
8
+ * of two or more consecutive lines is printed as `start-end`.
9
+ *
10
+ * @example
11
+ * formatLineRanges([2, 3, 4, 5, 333, 412, 413, 414]);
12
+ * // → '2-5, 333, 412-414'
13
+ *
14
+ * @param lineNumbers - The (possibly unsorted, possibly duplicated) line numbers.
15
+ * @param separator - String inserted between groups. Defaults to `', '`.
16
+ * @returns The grouped string, or an empty string when no lines are provided.
17
+ */
18
+ const formatLineRanges = (lineNumbers, separator = ", ") => {
19
+ const sortedUniqueLines = [...new Set(lineNumbers)].sort((a, b) => a - b);
20
+ if (sortedUniqueLines.length === 0) return "";
21
+ const groups = [];
22
+ let rangeStart = sortedUniqueLines[0];
23
+ let rangeEnd = rangeStart;
24
+ const pushGroup = () => {
25
+ groups.push(rangeStart === rangeEnd ? `${rangeStart}` : `${rangeStart}-${rangeEnd}`);
26
+ };
27
+ for (const lineNumber of sortedUniqueLines.slice(1)) {
28
+ if (lineNumber === rangeEnd + 1) {
29
+ rangeEnd = lineNumber;
30
+ continue;
31
+ }
32
+ pushGroup();
33
+ rangeStart = lineNumber;
34
+ rangeEnd = lineNumber;
35
+ }
36
+ pushGroup();
37
+ return groups.join(separator);
38
+ };
39
+
40
+ //#endregion
41
+ export { formatLineRanges };
42
+ //# sourceMappingURL=formatLineRanges.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatLineRanges.mjs","names":[],"sources":["../../../src/utils/formatLineRanges.ts"],"sourcesContent":["/**\n * Formats a list of line numbers into a compact, human-readable string where\n * runs of consecutive lines are collapsed into ranges.\n *\n * The input is sorted and de-duplicated first, so callers don't need to\n * pre-process it. A run of a single line is printed as the bare number; a run\n * of two or more consecutive lines is printed as `start-end`.\n *\n * @example\n * formatLineRanges([2, 3, 4, 5, 333, 412, 413, 414]);\n * // → '2-5, 333, 412-414'\n *\n * @param lineNumbers - The (possibly unsorted, possibly duplicated) line numbers.\n * @param separator - String inserted between groups. Defaults to `', '`.\n * @returns The grouped string, or an empty string when no lines are provided.\n */\nexport const formatLineRanges = (\n lineNumbers: number[],\n separator = ', '\n): string => {\n const sortedUniqueLines = [...new Set(lineNumbers)].sort((a, b) => a - b);\n\n if (sortedUniqueLines.length === 0) return '';\n\n const groups: string[] = [];\n let rangeStart = sortedUniqueLines[0]!;\n let rangeEnd = rangeStart;\n\n const pushGroup = (): void => {\n groups.push(\n rangeStart === rangeEnd ? `${rangeStart}` : `${rangeStart}-${rangeEnd}`\n );\n };\n\n for (const lineNumber of sortedUniqueLines.slice(1)) {\n if (lineNumber === rangeEnd + 1) {\n // Still inside the current consecutive run.\n rangeEnd = lineNumber;\n continue;\n }\n\n // Gap detected: close the current run and start a new one.\n pushGroup();\n rangeStart = lineNumber;\n rangeEnd = lineNumber;\n }\n\n pushGroup();\n\n return groups.join(separator);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,MAAa,oBACX,aACA,YAAY,SACD;CACX,MAAM,oBAAoB,CAAC,GAAG,IAAI,IAAI,YAAY,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,EAAE;AAEzE,KAAI,kBAAkB,WAAW,EAAG,QAAO;CAE3C,MAAM,SAAmB,EAAE;CAC3B,IAAI,aAAa,kBAAkB;CACnC,IAAI,WAAW;CAEf,MAAM,kBAAwB;AAC5B,SAAO,KACL,eAAe,WAAW,GAAG,eAAe,GAAG,WAAW,GAAG,WAC9D;;AAGH,MAAK,MAAM,cAAc,kBAAkB,MAAM,EAAE,EAAE;AACnD,MAAI,eAAe,WAAW,GAAG;AAE/B,cAAW;AACX;;AAIF,aAAW;AACX,eAAa;AACb,aAAW;;AAGb,YAAW;AAEX,QAAO,OAAO,KAAK,UAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","names":[],"sources":["../../src/cli.ts"],"mappings":";;;cA0Ca,OAAA;AAAA,KA4IR,UAAA;EACH,MAAA;EACA,OAAA;AAAA;AAAA,KAGU,oBAAA;EACV,OAAA;EACA,GAAA;EACA,OAAA;EACA,OAAA;EACA,UAAA;AAAA,IACE,UAAA;AANJ;;;;;;;;AAAA,cAuEa,MAAA,QAAa,OAAA"}
1
+ {"version":3,"file":"cli.d.ts","names":[],"sources":["../../src/cli.ts"],"mappings":";;;cA2Ca,OAAA;AAAA,KA4IR,UAAA;EACH,MAAA;EACA,OAAA;AAAA;AAAA,KAGU,oBAAA;EACV,OAAA;EACA,GAAA;EACA,OAAA;EACA,OAAA;EACA,UAAA;AAAA,IACE,UAAA;AANJ;;;;;;;;AAAA,cAuEa,MAAA,QAAa,OAAA"}
@@ -13,6 +13,7 @@ import { pull } from "./pull.js";
13
13
  import { push } from "./push/push.js";
14
14
  import { pushConfig } from "./pushConfig.js";
15
15
  import { reviewDoc } from "./reviewDoc/reviewDoc.js";
16
+ import { ScanCommandOptions, scan } from "./scan.js";
16
17
  import { searchDoc } from "./searchDoc.js";
17
18
  import { listMissingTranslations, listMissingTranslationsWithConfig } from "./test/listMissingTranslations.js";
18
19
  import { testMissingTranslations } from "./test/test.js";
@@ -21,4 +22,4 @@ export * from "@intlayer/chokidar/cli";
21
22
  export * from "@intlayer/chokidar/utils";
22
23
  export * from "@intlayer/chokidar/build";
23
24
  export * from "@intlayer/chokidar/watcher";
24
- export { ConfigurationOptions, FillOptions, ListProjectsCommandOptions, PLATFORM_OPTIONS, build, bundle, dirname, extract, fill, findProjectRoot, getDetectedPlatform, init, initSkills, listContentDeclaration, listContentDeclarationRows, listMissingTranslations, listMissingTranslationsWithConfig, listProjectsCommand, liveSync, pull, push, pushConfig, reviewDoc, searchDoc, setAPI, startEditor, testMissingTranslations, translateDoc };
25
+ export { ConfigurationOptions, FillOptions, ListProjectsCommandOptions, PLATFORM_OPTIONS, ScanCommandOptions, build, bundle, dirname, extract, fill, findProjectRoot, getDetectedPlatform, init, initSkills, listContentDeclaration, listContentDeclarationRows, listMissingTranslations, listMissingTranslationsWithConfig, listProjectsCommand, liveSync, pull, push, pushConfig, reviewDoc, scan, searchDoc, setAPI, startEditor, testMissingTranslations, translateDoc };
@@ -17,6 +17,12 @@ type ReviewDocOptions = {
17
17
  skipIfModifiedAfter?: number | string | Date;
18
18
  skipIfExists?: boolean;
19
19
  gitOptions?: ListGitFilesOptions;
20
+ /**
21
+ * Log-only mode. Instead of translating the changed blocks with AI, log the
22
+ * blocks that need attention (with line numbers and content) for the base and
23
+ * target locales, so another agent can generate the translations.
24
+ */
25
+ log?: boolean;
20
26
  };
21
27
  /**
22
28
  * Main audit function: scans all .md files in "en/" (unless you specified DOC_LIST),
@@ -34,7 +40,8 @@ declare const reviewDoc: ({
34
40
  skipIfModifiedBefore,
35
41
  skipIfModifiedAfter,
36
42
  skipIfExists,
37
- gitOptions
43
+ gitOptions,
44
+ log
38
45
  }: ReviewDocOptions) => Promise<void>;
39
46
  //#endregion
40
47
  export { reviewDoc };
@@ -1 +1 @@
1
- {"version":3,"file":"reviewDoc.d.ts","names":[],"sources":["../../../src/reviewDoc/reviewDoc.ts"],"mappings":";;;;;;KAgCK,gBAAA;EACH,UAAA;EACA,OAAA,EAAS,MAAA;EACT,mBAAA;EACA,UAAA,EAAY,MAAA;EACZ,SAAA,GAAY,SAAA;EACZ,2BAAA;EACA,aAAA,GAAgB,uBAAA;EAChB,kBAAA;EACA,oBAAA,qBAAyC,IAAA;EACzC,mBAAA,qBAAwC,IAAA;EACxC,YAAA;EACA,UAAA,GAAa,mBAAA;AAAA;;;;;cAOF,SAAA;EAAmB,UAAA;EAAA,OAAA;EAAA,mBAAA;EAAA,UAAA;EAAA,SAAA;EAAA,2BAAA;EAAA,aAAA;EAAA,kBAAA;EAAA,oBAAA;EAAA,mBAAA;EAAA,YAAA;EAAA;AAAA,GAa7B,gBAAA,KAAgB,OAAA"}
1
+ {"version":3,"file":"reviewDoc.d.ts","names":[],"sources":["../../../src/reviewDoc/reviewDoc.ts"],"mappings":";;;;;;KAqCK,gBAAA;EACH,UAAA;EACA,OAAA,EAAS,MAAA;EACT,mBAAA;EACA,UAAA,EAAY,MAAA;EACZ,SAAA,GAAY,SAAA;EACZ,2BAAA;EACA,aAAA,GAAgB,uBAAA;EAChB,kBAAA;EACA,oBAAA,qBAAyC,IAAA;EACzC,mBAAA,qBAAwC,IAAA;EACxC,YAAA;EACA,UAAA,GAAa,mBAAA;EAAmB;;;;;EAMhC,GAAA;AAAA;;;;;cAOW,SAAA;EAAmB,UAAA;EAAA,OAAA;EAAA,mBAAA;EAAA,UAAA;EAAA,SAAA;EAAA,2BAAA;EAAA,aAAA;EAAA,kBAAA;EAAA,oBAAA;EAAA,mBAAA;EAAA,YAAA;EAAA,UAAA;EAAA;AAAA,GAc7B,gBAAA,KAAgB,OAAA"}
@@ -7,12 +7,14 @@ import { AIConfig } from "@intlayer/ai";
7
7
  //#region src/reviewDoc/reviewDocBlockAware.d.ts
8
8
  /**
9
9
  * Review a file using block-aware alignment.
10
- * This approach:
11
- * 1. Segments both English and French documents into semantic blocks
12
- * 2. Aligns blocks using structure (special chars, numbers) and context
13
- * 3. Detects which blocks changed, were added, or deleted
14
- * 4. Only sends changed/new blocks to AI for translation
15
- * 5. Handles reordering automatically
10
+ *
11
+ * 1. Segments both base and target documents into semantic blocks.
12
+ * 2. Aligns blocks using structure (special chars, numbers) and context.
13
+ * 3. Detects which blocks changed, were added, or deleted.
14
+ * 4. Applies deletions immediately without AI.
15
+ * 5. Sends changed/new blocks to AI in bottom-up order (last block first), so
16
+ * line numbers of earlier blocks are not shifted by edits below them.
17
+ * 6. Rewrites the file after each block so progress is persisted incrementally.
16
18
  */
17
19
  declare const reviewFileBlockAware: (baseFilePath: string, outputFilePath: string, locale: Locale, baseLocale: Locale, aiOptions?: AIOptions, configOptions?: GetConfigurationOptions, customInstructions?: string, changedLines?: number[], aiClient?: AIClient, aiConfig?: AIConfig) => Promise<void>;
18
20
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"reviewDocBlockAware.d.ts","names":[],"sources":["../../../src/reviewDoc/reviewDocBlockAware.ts"],"mappings":";;;;;;;;;AAwCA;;;;;;;cAAa,oBAAA,GACX,YAAA,UACA,cAAA,UACA,MAAA,EAAQ,MAAA,EACR,UAAA,EAAY,MAAA,EACZ,SAAA,GAAY,SAAA,EACZ,aAAA,GAAgB,uBAAA,EAChB,kBAAA,WACA,YAAA,aACA,QAAA,GAAW,QAAA,EACX,QAAA,GAAW,QAAA,KAAQ,OAAA"}
1
+ {"version":3,"file":"reviewDocBlockAware.d.ts","names":[],"sources":["../../../src/reviewDoc/reviewDocBlockAware.ts"],"mappings":";;;;;;;;;AA0CA;;;;;;;;;cAAa,oBAAA,GACX,YAAA,UACA,cAAA,UACA,MAAA,EAAQ,MAAA,EACR,UAAA,EAAY,MAAA,EACZ,SAAA,GAAY,SAAA,EACZ,aAAA,GAAgB,uBAAA,EAChB,kBAAA,WACA,YAAA,aACA,QAAA,GAAW,QAAA,EACX,QAAA,GAAW,QAAA,KAAQ,OAAA"}