@intlayer/cli 9.1.3 → 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 (36) hide show
  1. package/dist/cjs/cli.cjs +3 -3
  2. package/dist/cjs/cli.cjs.map +1 -1
  3. package/dist/cjs/reviewDoc/reviewDoc.cjs +57 -15
  4. package/dist/cjs/reviewDoc/reviewDoc.cjs.map +1 -1
  5. package/dist/cjs/reviewDoc/reviewDocBlockAware.cjs +14 -3
  6. package/dist/cjs/reviewDoc/reviewDocBlockAware.cjs.map +1 -1
  7. package/dist/cjs/reviewDoc/reviewDocLog.cjs +20 -15
  8. package/dist/cjs/reviewDoc/reviewDocLog.cjs.map +1 -1
  9. package/dist/cjs/reviewDoc/reviewDocProgress.cjs +66 -0
  10. package/dist/cjs/reviewDoc/reviewDocProgress.cjs.map +1 -0
  11. package/dist/cjs/reviewDoc/reviewDocSynthesis.cjs +80 -0
  12. package/dist/cjs/reviewDoc/reviewDocSynthesis.cjs.map +1 -0
  13. package/dist/esm/cli.mjs +3 -3
  14. package/dist/esm/cli.mjs.map +1 -1
  15. package/dist/esm/reviewDoc/reviewDoc.mjs +58 -16
  16. package/dist/esm/reviewDoc/reviewDoc.mjs.map +1 -1
  17. package/dist/esm/reviewDoc/reviewDocBlockAware.mjs +14 -3
  18. package/dist/esm/reviewDoc/reviewDocBlockAware.mjs.map +1 -1
  19. package/dist/esm/reviewDoc/reviewDocLog.mjs +20 -16
  20. package/dist/esm/reviewDoc/reviewDocLog.mjs.map +1 -1
  21. package/dist/esm/reviewDoc/reviewDocProgress.mjs +63 -0
  22. package/dist/esm/reviewDoc/reviewDocProgress.mjs.map +1 -0
  23. package/dist/esm/reviewDoc/reviewDocSynthesis.mjs +77 -0
  24. package/dist/esm/reviewDoc/reviewDocSynthesis.mjs.map +1 -0
  25. package/dist/types/index.d.ts +2 -2
  26. package/dist/types/reviewDoc/reviewDoc.d.ts +15 -8
  27. package/dist/types/reviewDoc/reviewDoc.d.ts.map +1 -1
  28. package/dist/types/reviewDoc/reviewDocBlockAware.d.ts +5 -1
  29. package/dist/types/reviewDoc/reviewDocBlockAware.d.ts.map +1 -1
  30. package/dist/types/reviewDoc/reviewDocLog.d.ts +14 -10
  31. package/dist/types/reviewDoc/reviewDocLog.d.ts.map +1 -1
  32. package/dist/types/reviewDoc/reviewDocProgress.d.ts +25 -0
  33. package/dist/types/reviewDoc/reviewDocProgress.d.ts.map +1 -0
  34. package/dist/types/reviewDoc/reviewDocSynthesis.d.ts +41 -0
  35. package/dist/types/reviewDoc/reviewDocSynthesis.d.ts.map +1 -0
  36. package/package.json +14 -14
@@ -3,7 +3,9 @@ import { getOutputFilePath } from "../utils/getOutputFilePath.mjs";
3
3
  import { checkFileModifiedRange } from "../utils/checkFileModifiedRange.mjs";
4
4
  import { formatLineRanges } from "../utils/formatLineRanges.mjs";
5
5
  import { reviewFileBlockAware } from "./reviewDocBlockAware.mjs";
6
- import { logReviewFileBlocks } from "./reviewDocLog.mjs";
6
+ import { buildFileReviewReport, logReviewFileBlocks } from "./reviewDocLog.mjs";
7
+ import { createReviewProgressLogger } from "./reviewDocProgress.mjs";
8
+ import { formatReviewSynthesis } from "./reviewDocSynthesis.mjs";
7
9
  import { existsSync } from "node:fs";
8
10
  import { join, relative } from "node:path";
9
11
  import * as ANSIColors from "@intlayer/config/colors";
@@ -20,7 +22,7 @@ import fg from "fast-glob";
20
22
  * Main audit function: scans all .md files in "en/" (unless you specified DOC_LIST),
21
23
  * then audits them to each locale in LOCALE_LIST.
22
24
  */
23
- const reviewDoc = async ({ docPattern, locales, excludedGlobPattern, baseLocale, aiOptions, nbSimultaneousFileProcessed, configOptions, customInstructions, skipIfModifiedBefore, skipIfModifiedAfter, skipIfExists, gitOptions, log }) => {
25
+ const reviewDoc = async ({ docPattern, locales, excludedGlobPattern, baseLocale, aiOptions, nbSimultaneousFileProcessed, configOptions, customInstructions, skipIfModifiedBefore, skipIfModifiedAfter, skipIfExists, gitOptions, mode = "apply" }) => {
24
26
  const configuration = getConfiguration(configOptions);
25
27
  logConfigDetails(configOptions);
26
28
  const appLogger = getAppLogger({ log: {
@@ -29,16 +31,16 @@ const reviewDoc = async ({ docPattern, locales, excludedGlobPattern, baseLocale,
29
31
  } });
30
32
  let aiClient;
31
33
  let aiConfig;
32
- if (!log) {
34
+ if (mode === "apply") {
33
35
  const aiResult = await setupAI(configuration, aiOptions);
34
- if (!aiResult?.hasAIAccess) return;
36
+ if (!aiResult?.hasAIAccess) return [];
35
37
  aiClient = aiResult.aiClient;
36
38
  aiConfig = aiResult.aiConfig;
37
39
  if (aiResult.isCustomAI && aiClient && aiConfig) {
38
40
  const { hasAIAccess, error } = await aiClient.checkAISDKAccess(aiConfig);
39
41
  if (!hasAIAccess) {
40
42
  appLogger(`${x} ${error}`);
41
- return;
43
+ return [];
42
44
  }
43
45
  }
44
46
  }
@@ -55,13 +57,25 @@ const reviewDoc = async ({ docPattern, locales, excludedGlobPattern, baseLocale,
55
57
  appLogger(`Reviewing ${colorizeNumber(locales.length)} locales: [ ${formatLocale(locales)} ]`);
56
58
  appLogger(`Reviewing ${colorizeNumber(docList.length)} files:`);
57
59
  appLogger(docList.map((path) => ` - ${formatPath(path)}\n`));
58
- await parallelize(docList.flatMap((docPath) => locales.map((locale) => async () => {
59
- appLogger(`Reviewing file: ${formatPath(docPath)} to ${formatLocale(locale)}`);
60
+ const logProgress = (...content) => {
61
+ if (mode === "synthesis") return;
62
+ appLogger(...content);
63
+ };
64
+ const reviewResults = [];
65
+ /** A document is up to date when no block has to be added, reviewed or removed. */
66
+ const getOutcome = (summary) => summary.review + summary.insertNew + summary.delete > 0 ? "toEdit" : "upToDate";
67
+ const allTasks = docList.flatMap((docPath) => locales.map((locale) => async () => {
68
+ logProgress(`Reviewing file: ${formatPath(docPath)} to ${formatLocale(locale)}`);
60
69
  const absoluteBaseFilePath = join(configuration.system.baseDir, docPath);
61
70
  const outputFilePath = getOutputFilePath(absoluteBaseFilePath, locale, baseLocale);
62
71
  if (skipIfExists && existsSync(outputFilePath)) {
63
72
  const relativePath = relative(configuration.system.baseDir, outputFilePath);
64
- appLogger(`${colorize("⊘", ANSIColors.YELLOW)} File ${formatPath(relativePath)} already exists, skipping.`);
73
+ logProgress(`${colorize("⊘", ANSIColors.YELLOW)} File ${formatPath(relativePath)} already exists, skipping.`);
74
+ reviewResults.push({
75
+ docPath,
76
+ locale,
77
+ outcome: "skipped"
78
+ });
65
79
  return;
66
80
  }
67
81
  if (existsSync(outputFilePath)) {
@@ -70,15 +84,20 @@ const reviewDoc = async ({ docPattern, locales, excludedGlobPattern, baseLocale,
70
84
  skipIfModifiedAfter
71
85
  });
72
86
  if (fileModificationData.isSkipped) {
73
- appLogger(fileModificationData.message);
87
+ logProgress(fileModificationData.message);
88
+ reviewResults.push({
89
+ docPath,
90
+ locale,
91
+ outcome: "skipped"
92
+ });
74
93
  return;
75
94
  }
76
- } else if (skipIfModifiedBefore || skipIfModifiedAfter) appLogger(`${colorize("!", ANSIColors.YELLOW)} File ${formatPath(outputFilePath)} does not exist, skipping modification date check.`);
95
+ } else if (skipIfModifiedBefore || skipIfModifiedAfter) logProgress(`${colorize("!", ANSIColors.YELLOW)} File ${formatPath(outputFilePath)} does not exist, skipping modification date check.`);
77
96
  let changedLines;
78
97
  if (gitOptions) {
79
98
  const gitChangedLines = await listGitLines(absoluteBaseFilePath, gitOptions);
80
99
  changedLines = gitChangedLines;
81
- appLogger(`Changed lines (${formatLocale(baseLocale)}): ${formatLineRanges(gitChangedLines)}`);
100
+ logProgress(`Changed lines (${formatLocale(baseLocale)}): ${formatLineRanges(gitChangedLines)}`);
82
101
  const { blocks } = buildReviewReport({
83
102
  baseText: await readFile(absoluteBaseFilePath, "utf-8").catch(() => ""),
84
103
  targetText: existsSync(outputFilePath) ? await readFile(outputFilePath, "utf-8").catch(() => "") : "",
@@ -89,14 +108,37 @@ const reviewDoc = async ({ docPattern, locales, excludedGlobPattern, baseLocale,
89
108
  const { start, end } = block.targetLineRange;
90
109
  return Array.from({ length: end - start + 1 }, (_unused, offset) => start + offset);
91
110
  });
92
- appLogger(`Corresponding block (${formatLocale(locale)}): ${formatLineRanges(correspondingTargetLines)}`);
111
+ logProgress(`Corresponding block (${formatLocale(locale)}): ${formatLineRanges(correspondingTargetLines)}`);
93
112
  }
94
- if (log) {
95
- await logReviewFileBlocks(absoluteBaseFilePath, outputFilePath, locale, baseLocale, configOptions, changedLines);
113
+ if (mode !== "apply") {
114
+ const report = await buildFileReviewReport(absoluteBaseFilePath, outputFilePath, changedLines);
115
+ if (mode === "report") logReviewFileBlocks(report, absoluteBaseFilePath, locale, baseLocale, configOptions);
116
+ reviewResults.push({
117
+ docPath,
118
+ locale,
119
+ outcome: getOutcome(report.summary),
120
+ summary: report.summary
121
+ });
96
122
  return;
97
123
  }
98
- await reviewFileBlockAware(absoluteBaseFilePath, outputFilePath, locale, baseLocale, aiOptions, configOptions, customInstructions, changedLines, aiClient, aiConfig);
99
- })), (task) => task(), nbSimultaneousFileProcessed ?? 3);
124
+ const reviewSummary = await reviewFileBlockAware(absoluteBaseFilePath, outputFilePath, locale, baseLocale, aiOptions, configOptions, customInstructions, changedLines, aiClient, aiConfig);
125
+ reviewResults.push({
126
+ docPath,
127
+ locale,
128
+ outcome: getOutcome(reviewSummary),
129
+ summary: reviewSummary
130
+ });
131
+ }));
132
+ const progressLogger = createReviewProgressLogger(allTasks.length);
133
+ if (mode === "synthesis") progressLogger.start();
134
+ await parallelize(allTasks, async (task) => {
135
+ await task();
136
+ progressLogger.increment();
137
+ }, nbSimultaneousFileProcessed ?? 3);
138
+ progressLogger.stop();
139
+ const synthesis = formatReviewSynthesis(reviewResults, { hasAppliedChanges: mode === "apply" });
140
+ for (const line of synthesis.split("\n")) appLogger(line);
141
+ return reviewResults;
100
142
  };
101
143
 
102
144
  //#endregion
@@ -1 +1 @@
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 * 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 {\n type ListGitFilesOptions,\n listGitFiles,\n listGitLines,\n logConfigDetails,\n} from '@intlayer/engine/cli';\nimport { buildReviewReport } from '@intlayer/engine/docReview';\nimport { formatLocale, formatPath, parallelize } from '@intlayer/engine/utils';\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
+ {"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 * 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 {\n type ListGitFilesOptions,\n listGitFiles,\n listGitLines,\n logConfigDetails,\n} from '@intlayer/engine/cli';\nimport {\n buildReviewReport,\n type ReviewReportSummary,\n} from '@intlayer/engine/docReview';\nimport { formatLocale, formatPath, parallelize } from '@intlayer/engine/utils';\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 { buildFileReviewReport, logReviewFileBlocks } from './reviewDocLog';\nimport { createReviewProgressLogger } from './reviewDocProgress';\nimport {\n formatReviewSynthesis,\n type ReviewFileResult,\n type ReviewOutcome,\n} from './reviewDocSynthesis';\n\n/**\n * How the review should be run.\n *\n * - `apply`: translate the diverging blocks with AI and write the target files.\n * - `report`: no AI. Log every block that needs attention (line numbers and\n * content, for both locales) so another agent can generate the translations.\n * - `synthesis`: no AI, no per-block output. Only log the final synthesis of the\n * documents that are up to date and the ones that still have blocks to edit.\n */\nexport type ReviewDocMode = 'apply' | 'report' | 'synthesis';\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 /** How the review should be run. Defaults to `apply`. See {@link ReviewDocMode}. */\n mode?: ReviewDocMode;\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 mode = 'apply',\n}: ReviewDocOptions) => {\n const configuration = getConfiguration(configOptions);\n logConfigDetails(configOptions);\n\n const appLogger = getAppLogger({ log: { ...configuration.log, prefix: '' } });\n\n // The `report` and `synthesis` modes never call an AI, so the AI access\n // checks are skipped.\n let aiClient: AIClient | undefined;\n let aiConfig: AIConfig | undefined;\n\n if (mode === 'apply') {\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 // In `synthesis` mode only the final recap is printed, so the per-file\n // progress logs are muted.\n const logProgress: typeof appLogger = (...content) => {\n if (mode === 'synthesis') return;\n appLogger(...content);\n };\n\n // Filled by every task, then rendered as the end-of-run synthesis.\n const reviewResults: ReviewFileResult[] = [];\n\n /** A document is up to date when no block has to be added, reviewed or removed. */\n const getOutcome = (summary: ReviewReportSummary): ReviewOutcome =>\n summary.review + summary.insertNew + summary.delete > 0\n ? 'toEdit'\n : 'upToDate';\n\n // Create all tasks to be processed\n const allTasks = docList.flatMap((docPath) =>\n locales.map((locale) => async () => {\n logProgress(\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 logProgress(\n `${colorize('⊘', ANSIColors.YELLOW)} File ${formatPath(relativePath)} already exists, skipping.`\n );\n reviewResults.push({ docPath, locale, outcome: 'skipped' });\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 logProgress(fileModificationData.message);\n reviewResults.push({ docPath, locale, outcome: 'skipped' });\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 logProgress(\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 logProgress(\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 logProgress(\n `Corresponding block (${formatLocale(locale)}): ${formatLineRanges(correspondingTargetLines)}`\n );\n }\n\n if (mode !== 'apply') {\n const report = await buildFileReviewReport(\n absoluteBaseFilePath,\n outputFilePath,\n changedLines\n );\n\n if (mode === 'report') {\n logReviewFileBlocks(\n report,\n absoluteBaseFilePath,\n locale as Locale,\n baseLocale,\n configOptions\n );\n }\n\n reviewResults.push({\n docPath,\n locale,\n outcome: getOutcome(report.summary),\n summary: report.summary,\n });\n return;\n }\n\n const reviewSummary = 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 reviewResults.push({\n docPath,\n locale,\n outcome: getOutcome(reviewSummary),\n summary: reviewSummary,\n });\n })\n );\n\n // The per-file logs are muted in `synthesis` mode, so a progress line keeps\n // the run readable while every document is compared.\n const progressLogger = createReviewProgressLogger(allTasks.length);\n\n if (mode === 'synthesis') progressLogger.start();\n\n await parallelize(\n allTasks,\n async (task) => {\n await task();\n progressLogger.increment();\n },\n nbSimultaneousFileProcessed ?? 3\n );\n\n progressLogger.stop();\n\n const synthesis = formatReviewSynthesis(reviewResults, {\n hasAppliedChanges: mode === 'apply',\n });\n\n for (const line of synthesis.split('\\n')) {\n appLogger(line);\n }\n\n return reviewResults;\n};\n"],"mappings":"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"}
@@ -25,6 +25,9 @@ import { ENGLISH } from "@intlayer/types/locales";
25
25
  * 5. Sends changed/new blocks to AI in bottom-up order (last block first), so
26
26
  * line numbers of earlier blocks are not shifted by edits below them.
27
27
  * 6. Rewrites the file after each block so progress is persisted incrementally.
28
+ *
29
+ * @returns The per-action block counts of the alignment plan, so the caller can
30
+ * synthesize what was reused and what had to be edited.
28
31
  */
29
32
  const reviewFileBlockAware = async (baseFilePath, outputFilePath, locale, baseLocale, aiOptions, configOptions, customInstructions, changedLines, aiClient, aiConfig) => {
30
33
  const configuration = getConfiguration(configOptions);
@@ -42,9 +45,16 @@ const reviewFileBlockAware = async (baseFilePath, outputFilePath, locale, baseLo
42
45
  targetText,
43
46
  changedLines
44
47
  });
45
- const deleteCount = plan.actions.filter((a) => a.kind === "delete").length;
48
+ const countActions = (kind) => plan.actions.filter((action) => action.kind === kind).length;
49
+ const reviewSummary = {
50
+ reuse: countActions("reuse"),
51
+ review: countActions("review"),
52
+ insertNew: countActions("insert_new"),
53
+ delete: countActions("delete")
54
+ };
55
+ const deleteCount = reviewSummary.delete;
46
56
  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)}`);
57
+ applicationLogger(`${filePrefix}Actions: reuse=${colorizeNumber(reviewSummary.reuse)}, review=${colorizeNumber(reviewSummary.review)}, new=${colorizeNumber(reviewSummary.insertNew)}, delete=${colorizeNumber(deleteCount)}`);
48
58
  const reviewedSegmentsMap = /* @__PURE__ */ new Map();
49
59
  for (const [actionIndex, action] of plan.actions.entries()) if (action.kind === "delete") reviewedSegmentsMap.set(actionIndex, "");
50
60
  const writeCurrentState = () => {
@@ -62,7 +72,7 @@ const reviewFileBlockAware = async (baseFilePath, outputFilePath, locale, baseLo
62
72
  writeCurrentState();
63
73
  }
64
74
  applicationLogger(`${colorize("✔", ANSIColors.GREEN)} File ${formatPath(outputFilePath)} updated successfully (no AI changes needed).`);
65
- return;
75
+ return reviewSummary;
66
76
  }
67
77
  applicationLogger(`${filePrefix}Segments to review: ${colorizeNumber(segmentsToReview.length)} (processing bottom-up)`);
68
78
  const segmentsBottomUp = segmentsToReview.map((segment, originalIndex) => ({
@@ -105,6 +115,7 @@ const reviewFileBlockAware = async (baseFilePath, outputFilePath, locale, baseLo
105
115
  writeCurrentState();
106
116
  }
107
117
  applicationLogger(`${colorize("✔", ANSIColors.GREEN)} File ${formatPath(outputFilePath)} created/updated successfully.`);
118
+ return reviewSummary;
108
119
  };
109
120
 
110
121
  //#endregion
@@ -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 * 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 {\n buildAlignmentPlan,\n mergeReviewedSegments,\n} from '@intlayer/engine/docReview';\nimport { formatLocale, formatPath } from '@intlayer/engine/utils';\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":"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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 * 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 {\n buildAlignmentPlan,\n mergeReviewedSegments,\n type PlannedAction,\n type ReviewReportSummary,\n} from '@intlayer/engine/docReview';\nimport { formatLocale, formatPath } from '@intlayer/engine/utils';\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 *\n * @returns The per-action block counts of the alignment plan, so the caller can\n * synthesize what was reused and what had to be edited.\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): Promise<ReviewReportSummary> => {\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 countActions = (kind: PlannedAction['kind']): number =>\n plan.actions.filter((action: PlannedAction) => action.kind === kind).length;\n\n const reviewSummary: ReviewReportSummary = {\n reuse: countActions('reuse'),\n review: countActions('review'),\n insertNew: countActions('insert_new'),\n delete: countActions('delete'),\n };\n\n const deleteCount = reviewSummary.delete;\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(reviewSummary.reuse)}, review=${colorizeNumber(reviewSummary.review)}, new=${colorizeNumber(reviewSummary.insertNew)}, 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 reviewSummary;\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 return reviewSummary;\n};\n"],"mappings":"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"}
@@ -7,40 +7,44 @@ import { buildReviewReport, formatReviewReport } from "@intlayer/engine/docRevie
7
7
 
8
8
  //#region src/reviewDoc/reviewDocLog.ts
9
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.
10
+ * Compare a base document with its translation without calling any AI.
16
11
  *
17
12
  * @param baseFilePath - Absolute path of the base (source) document.
18
13
  * @param outputFilePath - Absolute path of the target (translated) document.
14
+ * @param changedLines - 1-based base line numbers that changed (from git), if any.
15
+ * @returns The structured review report.
16
+ */
17
+ const buildFileReviewReport = async (baseFilePath, outputFilePath, changedLines) => {
18
+ return buildReviewReport({
19
+ baseText: await readFile(baseFilePath, "utf-8"),
20
+ targetText: existsSync(outputFilePath) ? await readFile(outputFilePath, "utf-8").catch(() => "") : "",
21
+ changedLines
22
+ });
23
+ };
24
+ /**
25
+ * Log the blocks that need attention (with their line ranges and content) for a
26
+ * single file/locale pair, so another agent or a human can generate the missing
27
+ * translations.
28
+ *
29
+ * @param report - The report built by {@link buildFileReviewReport}.
30
+ * @param baseFilePath - Absolute path of the base (source) document.
19
31
  * @param locale - The target locale being reviewed.
20
32
  * @param baseLocale - The base locale used as reference.
21
33
  * @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
34
  */
25
- const logReviewFileBlocks = async (baseFilePath, outputFilePath, locale, baseLocale, configOptions, changedLines) => {
35
+ const logReviewFileBlocks = (report, baseFilePath, locale, baseLocale, configOptions) => {
26
36
  const appLogger = getAppLogger({ log: {
27
37
  ...getConfiguration(configOptions).log,
28
38
  prefix: ""
29
39
  } });
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
40
  const formatted = formatReviewReport(report, {
36
41
  baseLabel: formatLocale(baseLocale),
37
42
  targetLabel: formatLocale(locale)
38
43
  });
39
44
  appLogger(`${formatPath(baseFilePath)} → ${formatLocale(locale)}`);
40
45
  for (const line of formatted.split("\n")) appLogger(line);
41
- return report;
42
46
  };
43
47
 
44
48
  //#endregion
45
- export { logReviewFileBlocks };
49
+ export { buildFileReviewReport, logReviewFileBlocks };
46
50
  //# sourceMappingURL=reviewDocLog.mjs.map
@@ -1 +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 { getAppLogger } from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport {\n buildReviewReport,\n formatReviewReport,\n type ReviewReport,\n} from '@intlayer/engine/docReview';\nimport { formatLocale, formatPath } from '@intlayer/engine/utils';\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,aACgB,CAAC,CAAC;EAAK,QAAQ;CAAG,EAAE,CAAC;CAO5E,MAAM,SAAS,kBAAkB;EAAE,gBALZ,SAAS,cAAc,OAAO;EAKR,YAJ1B,WAAW,cAAc,IACxC,MAAM,SAAS,gBAAgB,OAAO,CAAC,CAAC,YAAY,EAAE,IACtD;EAEqD;CAAa,CAAC;CAEvE,MAAM,YAAY,mBAAmB,QAAQ;EAC3C,WAAW,aAAa,UAAU;EAClC,aAAa,aAAa,MAAM;CAClC,CAAC;CAED,UAAU,GAAG,WAAW,YAAY,EAAE,KAAK,aAAa,MAAM,GAAG;CACjE,KAAK,MAAM,QAAQ,UAAU,MAAM,IAAI,GACrC,UAAU,IAAI;CAGhB,OAAO;AACT"}
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 { getAppLogger } from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport {\n buildReviewReport,\n formatReviewReport,\n type ReviewReport,\n} from '@intlayer/engine/docReview';\nimport { formatLocale, formatPath } from '@intlayer/engine/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\n\n/**\n * Compare a base document with its translation without calling any AI.\n *\n * @param baseFilePath - Absolute path of the base (source) document.\n * @param outputFilePath - Absolute path of the target (translated) document.\n * @param changedLines - 1-based base line numbers that changed (from git), if any.\n * @returns The structured review report.\n */\nexport const buildFileReviewReport = async (\n baseFilePath: string,\n outputFilePath: string,\n changedLines?: number[]\n): Promise<ReviewReport> => {\n const baseText = await readFile(baseFilePath, 'utf-8');\n const targetText = existsSync(outputFilePath)\n ? await readFile(outputFilePath, 'utf-8').catch(() => '')\n : '';\n\n return buildReviewReport({ baseText, targetText, changedLines });\n};\n\n/**\n * Log the blocks that need attention (with their line ranges and content) for a\n * single file/locale pair, so another agent or a human can generate the missing\n * translations.\n *\n * @param report - The report built by {@link buildFileReviewReport}.\n * @param baseFilePath - Absolute path of the base (source) 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 */\nexport const logReviewFileBlocks = (\n report: ReviewReport,\n baseFilePath: string,\n locale: Locale,\n baseLocale: Locale,\n configOptions?: GetConfigurationOptions\n): void => {\n const configuration = getConfiguration(configOptions);\n const appLogger = getAppLogger({ log: { ...configuration.log, prefix: '' } });\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"],"mappings":";;;;;;;;;;;;;;;;AAuBA,MAAa,wBAAwB,OACnC,cACA,gBACA,iBAC0B;CAM1B,OAAO,kBAAkB;EAAE,gBALJ,SAAS,cAAc,OAAO;EAKhB,YAJlB,WAAW,cAAc,IACxC,MAAM,SAAS,gBAAgB,OAAO,CAAC,CAAC,YAAY,EAAE,IACtD;EAE6C;CAAa,CAAC;AACjE;;;;;;;;;;;;AAaA,MAAa,uBACX,QACA,cACA,QACA,YACA,kBACS;CAET,MAAM,YAAY,aAAa,EAAE,KAAK;EAAE,GADlB,iBAAiB,aACgB,CAAC,CAAC;EAAK,QAAQ;CAAG,EAAE,CAAC;CAE5E,MAAM,YAAY,mBAAmB,QAAQ;EAC3C,WAAW,aAAa,UAAU;EAClC,aAAa,aAAa,MAAM;CAClC,CAAC;CAED,UAAU,GAAG,WAAW,YAAY,EAAE,KAAK,aAAa,MAAM,GAAG;CACjE,KAAK,MAAM,QAAQ,UAAU,MAAM,IAAI,GACrC,UAAU,IAAI;AAElB"}
@@ -0,0 +1,63 @@
1
+ import * as ANSIColors from "@intlayer/config/colors";
2
+ import { colorize, colorizeNumber, spinnerFrames } from "@intlayer/config/logger";
3
+
4
+ //#region src/reviewDoc/reviewDocProgress.ts
5
+ /** Refresh rate of the spinner, in milliseconds. */
6
+ const SPINNER_INTERVAL_MS = 100;
7
+ /**
8
+ * Single-line progress indicator used while the documents are compared with
9
+ * their translations.
10
+ *
11
+ * The line is rewritten in place on an interactive terminal, then erased so the
12
+ * final synthesis is printed on a clean line. On a non-interactive output (a
13
+ * file, a CI log) a single static line is printed instead of the animation, to
14
+ * avoid flooding the log with escape sequences.
15
+ *
16
+ * @param totalCount - Number of document/locale pairs to process.
17
+ * @returns The progress logger handles.
18
+ */
19
+ const createReviewProgressLogger = (totalCount) => {
20
+ const isInteractive = Boolean(process.stdout.isTTY);
21
+ let spinnerTimer = null;
22
+ let spinnerIndex = 0;
23
+ let processedCount = 0;
24
+ let isStarted = false;
25
+ const render = () => {
26
+ const frame = spinnerFrames[spinnerIndex] ?? "";
27
+ process.stdout.write(`\r\x1b[2K${colorize(frame, ANSIColors.BLUE)} Processing ${colorizeNumber(processedCount)}/${colorizeNumber(totalCount)}…`);
28
+ };
29
+ const start = () => {
30
+ isStarted = true;
31
+ if (!isInteractive) {
32
+ process.stdout.write(`Processing ${colorizeNumber(totalCount)} document/locale pair(s)…\n`);
33
+ return;
34
+ }
35
+ if (spinnerTimer) return;
36
+ render();
37
+ spinnerTimer = setInterval(() => {
38
+ spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length;
39
+ render();
40
+ }, SPINNER_INTERVAL_MS);
41
+ };
42
+ const increment = () => {
43
+ processedCount += 1;
44
+ if (isStarted && isInteractive) render();
45
+ };
46
+ const stop = () => {
47
+ if (spinnerTimer) {
48
+ clearInterval(spinnerTimer);
49
+ spinnerTimer = null;
50
+ }
51
+ if (isStarted && isInteractive) process.stdout.write("\r\x1B[2K");
52
+ isStarted = false;
53
+ };
54
+ return {
55
+ start,
56
+ increment,
57
+ stop
58
+ };
59
+ };
60
+
61
+ //#endregion
62
+ export { createReviewProgressLogger };
63
+ //# sourceMappingURL=reviewDocProgress.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reviewDocProgress.mjs","names":[],"sources":["../../../src/reviewDoc/reviewDocProgress.ts"],"sourcesContent":["import * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeNumber,\n spinnerFrames,\n} from '@intlayer/config/logger';\n\n/** Refresh rate of the spinner, in milliseconds. */\nconst SPINNER_INTERVAL_MS = 100;\n\nexport type ReviewProgressLogger = {\n /** Show the progress line and start animating the spinner. */\n start: () => void;\n /** Count one processed document/locale pair and refresh the progress line. */\n increment: () => void;\n /** Stop the spinner and erase the progress line. */\n stop: () => void;\n};\n\n/**\n * Single-line progress indicator used while the documents are compared with\n * their translations.\n *\n * The line is rewritten in place on an interactive terminal, then erased so the\n * final synthesis is printed on a clean line. On a non-interactive output (a\n * file, a CI log) a single static line is printed instead of the animation, to\n * avoid flooding the log with escape sequences.\n *\n * @param totalCount - Number of document/locale pairs to process.\n * @returns The progress logger handles.\n */\nexport const createReviewProgressLogger = (\n totalCount: number\n): ReviewProgressLogger => {\n const isInteractive = Boolean(process.stdout.isTTY);\n\n let spinnerTimer: NodeJS.Timeout | null = null;\n let spinnerIndex = 0;\n let processedCount = 0;\n // Until `start` is called the logger stays silent, so the caller can create it\n // unconditionally and only display it when relevant.\n let isStarted = false;\n\n const render = (): void => {\n const frame = spinnerFrames[spinnerIndex] ?? '';\n\n process.stdout.write(\n `\\r\\x1b[2K${colorize(frame, ANSIColors.BLUE)} Processing ${colorizeNumber(processedCount)}/${colorizeNumber(totalCount)}…`\n );\n };\n\n const start = (): void => {\n isStarted = true;\n\n if (!isInteractive) {\n process.stdout.write(\n `Processing ${colorizeNumber(totalCount)} document/locale pair(s)…\\n`\n );\n return;\n }\n\n if (spinnerTimer) return;\n\n render();\n\n spinnerTimer = setInterval(() => {\n spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length;\n render();\n }, SPINNER_INTERVAL_MS);\n };\n\n const increment = (): void => {\n processedCount += 1;\n\n if (isStarted && isInteractive) render();\n };\n\n const stop = (): void => {\n if (spinnerTimer) {\n clearInterval(spinnerTimer);\n spinnerTimer = null;\n }\n\n if (isStarted && isInteractive) process.stdout.write('\\r\\x1b[2K');\n\n isStarted = false;\n };\n\n return { start, increment, stop };\n};\n"],"mappings":";;;;;AAQA,MAAM,sBAAsB;;;;;;;;;;;;;AAuB5B,MAAa,8BACX,eACyB;CACzB,MAAM,gBAAgB,QAAQ,QAAQ,OAAO,KAAK;CAElD,IAAI,eAAsC;CAC1C,IAAI,eAAe;CACnB,IAAI,iBAAiB;CAGrB,IAAI,YAAY;CAEhB,MAAM,eAAqB;EACzB,MAAM,QAAQ,cAAc,iBAAiB;EAE7C,QAAQ,OAAO,MACb,YAAY,SAAS,OAAO,WAAW,IAAI,EAAE,cAAc,eAAe,cAAc,EAAE,GAAG,eAAe,UAAU,EAAE,EAC1H;CACF;CAEA,MAAM,cAAoB;EACxB,YAAY;EAEZ,IAAI,CAAC,eAAe;GAClB,QAAQ,OAAO,MACb,cAAc,eAAe,UAAU,EAAE,4BAC3C;GACA;EACF;EAEA,IAAI,cAAc;EAElB,OAAO;EAEP,eAAe,kBAAkB;GAC/B,gBAAgB,eAAe,KAAK,cAAc;GAClD,OAAO;EACT,GAAG,mBAAmB;CACxB;CAEA,MAAM,kBAAwB;EAC5B,kBAAkB;EAElB,IAAI,aAAa,eAAe,OAAO;CACzC;CAEA,MAAM,aAAmB;EACvB,IAAI,cAAc;GAChB,cAAc,YAAY;GAC1B,eAAe;EACjB;EAEA,IAAI,aAAa,eAAe,QAAQ,OAAO,MAAM,WAAW;EAEhE,YAAY;CACd;CAEA,OAAO;EAAE;EAAO;EAAW;CAAK;AAClC"}
@@ -0,0 +1,77 @@
1
+ import * as ANSIColors from "@intlayer/config/colors";
2
+ import { colorize, colorizeNumber } from "@intlayer/config/logger";
3
+ import { formatPath } from "@intlayer/engine/utils";
4
+
5
+ //#region src/reviewDoc/reviewDocSynthesis.ts
6
+ /** Group results by base document path, preserving the first-seen order. */
7
+ const groupResultsByDocPath = (results) => {
8
+ const grouped = /* @__PURE__ */ new Map();
9
+ for (const result of results) {
10
+ const existingResults = grouped.get(result.docPath);
11
+ if (existingResults) existingResults.push(result);
12
+ else grouped.set(result.docPath, [result]);
13
+ }
14
+ return grouped;
15
+ };
16
+ /** Render the per-action counts of a document, omitting the zeroed ones. */
17
+ const formatBlockCounts = (summary) => {
18
+ if (!summary) return "";
19
+ const formattedCounts = [
20
+ ["review", summary.review],
21
+ ["new", summary.insertNew],
22
+ ["delete", summary.delete]
23
+ ].filter(([, count]) => count > 0).map(([label, count]) => `${label}=${colorizeNumber(count)}`);
24
+ if (formattedCounts.length === 0) return "";
25
+ return colorize(` (${formattedCounts.join(", ")})`, ANSIColors.GREY);
26
+ };
27
+ /** Render one outcome section: a title line followed by one line per document. */
28
+ const formatSection = (title, icon, color, results, withBlockCounts) => {
29
+ if (results.length === 0) return [];
30
+ const lines = [`${colorize(icon, color)} ${colorize(title, color)} (${colorizeNumber(results.length)})`];
31
+ for (const [docPath, docResults] of groupResultsByDocPath(results)) {
32
+ lines.push(` ${formatPath(docPath)}`);
33
+ if (withBlockCounts) {
34
+ for (const result of docResults) lines.push(` ${colorize(result.locale, color)}${formatBlockCounts(result.summary)}`);
35
+ continue;
36
+ }
37
+ const localeCodes = docResults.map((result) => result.locale).join(", ");
38
+ lines.push(` ${colorize(localeCodes, color)}`);
39
+ }
40
+ return lines;
41
+ };
42
+ /**
43
+ * Render the end-of-run synthesis: which documents are aligned with their base
44
+ * document and which ones still have blocks to edit.
45
+ *
46
+ * @param results - One entry per reviewed base document / target locale pair.
47
+ * @param options - Rendering options.
48
+ * @returns A multi-line string, ready to be logged.
49
+ */
50
+ const formatReviewSynthesis = (results, options) => {
51
+ const upToDateResults = results.filter((result) => result.outcome === "upToDate");
52
+ const toEditResults = results.filter((result) => result.outcome === "toEdit");
53
+ const skippedResults = results.filter((result) => result.outcome === "skipped");
54
+ const documentCount = new Set(results.map((result) => result.docPath)).size;
55
+ return [
56
+ [
57
+ colorize("Review synthesis: ", ANSIColors.BEIGE),
58
+ colorizeNumber(documentCount),
59
+ colorize(" document(s), ", ANSIColors.BEIGE),
60
+ colorizeNumber(results.length),
61
+ colorize(" locale pair(s) — ", ANSIColors.BEIGE),
62
+ colorizeNumber(upToDateResults.length),
63
+ colorize(" up to date, ", ANSIColors.BEIGE),
64
+ colorizeNumber(toEditResults.length),
65
+ colorize(options?.hasAppliedChanges ? " updated, " : " to edit, ", ANSIColors.BEIGE),
66
+ colorizeNumber(skippedResults.length),
67
+ colorize(" skipped.", ANSIColors.BEIGE)
68
+ ].join(""),
69
+ ...formatSection(options?.hasAppliedChanges ? "Updated" : "To edit", "✎", ANSIColors.ORANGE, toEditResults, true),
70
+ ...formatSection("Up to date", "✔", ANSIColors.GREEN, upToDateResults, false),
71
+ ...formatSection("Skipped", "⊘", ANSIColors.YELLOW, skippedResults, false)
72
+ ].join("\n");
73
+ };
74
+
75
+ //#endregion
76
+ export { formatReviewSynthesis };
77
+ //# sourceMappingURL=reviewDocSynthesis.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reviewDocSynthesis.mjs","names":[],"sources":["../../../src/reviewDoc/reviewDocSynthesis.ts"],"sourcesContent":["import * as ANSIColors from '@intlayer/config/colors';\nimport {\n type ANSIColorsType,\n colorize,\n colorizeNumber,\n} from '@intlayer/config/logger';\nimport type { ReviewReportSummary } from '@intlayer/engine/docReview';\nimport { formatPath } from '@intlayer/engine/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\n\n/**\n * The outcome of reviewing one base document against one target locale.\n *\n * - `upToDate`: the translation matches the base document, nothing to do.\n * - `toEdit`: at least one block diverges (to review, to add, or to delete).\n * - `skipped`: the pair was not reviewed (already exists, modification range…).\n */\nexport type ReviewOutcome = 'upToDate' | 'toEdit' | 'skipped';\n\n/** The result of reviewing a single base document / target locale pair. */\nexport type ReviewFileResult = {\n /** Path of the base document, as matched by the doc pattern. */\n docPath: string;\n /** The reviewed target locale. */\n locale: Locale;\n /** What the review concluded for this pair. */\n outcome: ReviewOutcome;\n /** Per-action block counts, when the document was actually compared. */\n summary?: ReviewReportSummary;\n};\n\nexport type FormatReviewSynthesisOptions = {\n /**\n * When `true`, the diverging blocks were fixed during the run, so the section\n * is labelled `Updated` instead of `To edit`.\n */\n hasAppliedChanges?: boolean;\n};\n\n/** Group results by base document path, preserving the first-seen order. */\nconst groupResultsByDocPath = (\n results: ReviewFileResult[]\n): Map<string, ReviewFileResult[]> => {\n const grouped = new Map<string, ReviewFileResult[]>();\n\n for (const result of results) {\n const existingResults = grouped.get(result.docPath);\n\n if (existingResults) existingResults.push(result);\n else grouped.set(result.docPath, [result]);\n }\n\n return grouped;\n};\n\n/** Render the per-action counts of a document, omitting the zeroed ones. */\nconst formatBlockCounts = (summary?: ReviewReportSummary): string => {\n if (!summary) return '';\n\n const countEntries: [label: string, count: number][] = [\n ['review', summary.review],\n ['new', summary.insertNew],\n ['delete', summary.delete],\n ];\n\n const formattedCounts = countEntries\n .filter(([, count]) => count > 0)\n .map(([label, count]) => `${label}=${colorizeNumber(count)}`);\n\n if (formattedCounts.length === 0) return '';\n\n return colorize(` (${formattedCounts.join(', ')})`, ANSIColors.GREY);\n};\n\n/** Render one outcome section: a title line followed by one line per document. */\nconst formatSection = (\n title: string,\n icon: string,\n color: ANSIColorsType,\n results: ReviewFileResult[],\n withBlockCounts: boolean\n): string[] => {\n if (results.length === 0) return [];\n\n const lines: string[] = [\n `${colorize(icon, color)} ${colorize(title, color)} (${colorizeNumber(results.length)})`,\n ];\n\n for (const [docPath, docResults] of groupResultsByDocPath(results)) {\n lines.push(` ${formatPath(docPath)}`);\n\n if (withBlockCounts) {\n for (const result of docResults) {\n lines.push(\n ` ${colorize(result.locale, color)}${formatBlockCounts(result.summary)}`\n );\n }\n continue;\n }\n\n const localeCodes = docResults.map((result) => result.locale).join(', ');\n lines.push(` ${colorize(localeCodes, color)}`);\n }\n\n return lines;\n};\n\n/**\n * Render the end-of-run synthesis: which documents are aligned with their base\n * document and which ones still have blocks to edit.\n *\n * @param results - One entry per reviewed base document / target locale pair.\n * @param options - Rendering options.\n * @returns A multi-line string, ready to be logged.\n */\nexport const formatReviewSynthesis = (\n results: ReviewFileResult[],\n options?: FormatReviewSynthesisOptions\n): string => {\n const upToDateResults = results.filter(\n (result) => result.outcome === 'upToDate'\n );\n const toEditResults = results.filter((result) => result.outcome === 'toEdit');\n const skippedResults = results.filter(\n (result) => result.outcome === 'skipped'\n );\n\n const documentCount = new Set(results.map((result) => result.docPath)).size;\n\n const header = [\n colorize('Review synthesis: ', ANSIColors.BEIGE),\n colorizeNumber(documentCount),\n colorize(' document(s), ', ANSIColors.BEIGE),\n colorizeNumber(results.length),\n colorize(' locale pair(s) — ', ANSIColors.BEIGE),\n colorizeNumber(upToDateResults.length),\n colorize(' up to date, ', ANSIColors.BEIGE),\n colorizeNumber(toEditResults.length),\n colorize(\n options?.hasAppliedChanges ? ' updated, ' : ' to edit, ',\n ANSIColors.BEIGE\n ),\n colorizeNumber(skippedResults.length),\n colorize(' skipped.', ANSIColors.BEIGE),\n ].join('');\n\n return [\n header,\n ...formatSection(\n options?.hasAppliedChanges ? 'Updated' : 'To edit',\n '✎',\n ANSIColors.ORANGE,\n toEditResults,\n true\n ),\n ...formatSection(\n 'Up to date',\n '✔',\n ANSIColors.GREEN,\n upToDateResults,\n false\n ),\n ...formatSection('Skipped', '⊘', ANSIColors.YELLOW, skippedResults, false),\n ].join('\\n');\n};\n"],"mappings":";;;;;;AAwCA,MAAM,yBACJ,YACoC;CACpC,MAAM,0BAAU,IAAI,IAAgC;CAEpD,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,kBAAkB,QAAQ,IAAI,OAAO,OAAO;EAElD,IAAI,iBAAiB,gBAAgB,KAAK,MAAM;OAC3C,QAAQ,IAAI,OAAO,SAAS,CAAC,MAAM,CAAC;CAC3C;CAEA,OAAO;AACT;;AAGA,MAAM,qBAAqB,YAA0C;CACnE,IAAI,CAAC,SAAS,OAAO;CAQrB,MAAM,kBAAkB;EALtB,CAAC,UAAU,QAAQ,MAAM;EACzB,CAAC,OAAO,QAAQ,SAAS;EACzB,CAAC,UAAU,QAAQ,MAAM;CAGQ,CAAC,CACjC,QAAQ,GAAG,WAAW,QAAQ,CAAC,CAAC,CAChC,KAAK,CAAC,OAAO,WAAW,GAAG,MAAM,GAAG,eAAe,KAAK,GAAG;CAE9D,IAAI,gBAAgB,WAAW,GAAG,OAAO;CAEzC,OAAO,SAAS,KAAK,gBAAgB,KAAK,IAAI,EAAE,IAAI,WAAW,IAAI;AACrE;;AAGA,MAAM,iBACJ,OACA,MACA,OACA,SACA,oBACa;CACb,IAAI,QAAQ,WAAW,GAAG,OAAO,CAAC;CAElC,MAAM,QAAkB,CACtB,GAAG,SAAS,MAAM,KAAK,EAAE,GAAG,SAAS,OAAO,KAAK,EAAE,IAAI,eAAe,QAAQ,MAAM,EAAE,EACxF;CAEA,KAAK,MAAM,CAAC,SAAS,eAAe,sBAAsB,OAAO,GAAG;EAClE,MAAM,KAAK,MAAM,WAAW,OAAO,GAAG;EAEtC,IAAI,iBAAiB;GACnB,KAAK,MAAM,UAAU,YACnB,MAAM,KACJ,QAAQ,SAAS,OAAO,QAAQ,KAAK,IAAI,kBAAkB,OAAO,OAAO,GAC3E;GAEF;EACF;EAEA,MAAM,cAAc,WAAW,KAAK,WAAW,OAAO,MAAM,CAAC,CAAC,KAAK,IAAI;EACvE,MAAM,KAAK,QAAQ,SAAS,aAAa,KAAK,GAAG;CACnD;CAEA,OAAO;AACT;;;;;;;;;AAUA,MAAa,yBACX,SACA,YACW;CACX,MAAM,kBAAkB,QAAQ,QAC7B,WAAW,OAAO,YAAY,UACjC;CACA,MAAM,gBAAgB,QAAQ,QAAQ,WAAW,OAAO,YAAY,QAAQ;CAC5E,MAAM,iBAAiB,QAAQ,QAC5B,WAAW,OAAO,YAAY,SACjC;CAEA,MAAM,gBAAgB,IAAI,IAAI,QAAQ,KAAK,WAAW,OAAO,OAAO,CAAC,CAAC,CAAC;CAmBvE,OAAO;EAjBQ;GACb,SAAS,sBAAsB,WAAW,KAAK;GAC/C,eAAe,aAAa;GAC5B,SAAS,kBAAkB,WAAW,KAAK;GAC3C,eAAe,QAAQ,MAAM;GAC7B,SAAS,sBAAsB,WAAW,KAAK;GAC/C,eAAe,gBAAgB,MAAM;GACrC,SAAS,iBAAiB,WAAW,KAAK;GAC1C,eAAe,cAAc,MAAM;GACnC,SACE,SAAS,oBAAoB,eAAe,cAC5C,WAAW,KACb;GACA,eAAe,eAAe,MAAM;GACpC,SAAS,aAAa,WAAW,KAAK;EACxC,CAAC,CAAC,KAAK,EAGA;EACL,GAAG,cACD,SAAS,oBAAoB,YAAY,WACzC,KACA,WAAW,QACX,eACA,IACF;EACA,GAAG,cACD,cACA,KACA,WAAW,OACX,iBACA,KACF;EACA,GAAG,cAAc,WAAW,KAAK,WAAW,QAAQ,gBAAgB,KAAK;CAC3E,CAAC,CAAC,KAAK,IAAI;AACb"}
@@ -12,7 +12,7 @@ import { liveSync } from "./liveSync.js";
12
12
  import { pull } from "./pull.js";
13
13
  import { push } from "./push/push.js";
14
14
  import { pushConfig } from "./pushConfig.js";
15
- import { reviewDoc } from "./reviewDoc/reviewDoc.js";
15
+ import { ReviewDocMode, reviewDoc } from "./reviewDoc/reviewDoc.js";
16
16
  import { ScanCommandOptions, scan } from "./scan.js";
17
17
  import { searchDoc } from "./searchDoc.js";
18
18
  import { listMissingTranslations, listMissingTranslationsWithConfig } from "./test/listMissingTranslations.js";
@@ -23,4 +23,4 @@ export type * from "@intlayer/engine/cli";
23
23
  export type * from "@intlayer/engine/build";
24
24
  export type * from "@intlayer/engine/utils";
25
25
  export type * from "@intlayer/engine/watcher";
26
- 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 };
26
+ export { ConfigurationOptions, FillOptions, ListProjectsCommandOptions, PLATFORM_OPTIONS, ReviewDocMode, 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 };
@@ -1,8 +1,19 @@
1
+ import { ReviewFileResult } from "./reviewDocSynthesis.js";
1
2
  import { GetConfigurationOptions } from "@intlayer/config/node";
2
3
  import { AIOptions } from "@intlayer/api";
3
4
  import { ListGitFilesOptions } from "@intlayer/engine/cli";
4
5
  import { Locale } from "@intlayer/types/allLocales";
5
6
  //#region src/reviewDoc/reviewDoc.d.ts
7
+ /**
8
+ * How the review should be run.
9
+ *
10
+ * - `apply`: translate the diverging blocks with AI and write the target files.
11
+ * - `report`: no AI. Log every block that needs attention (line numbers and
12
+ * content, for both locales) so another agent can generate the translations.
13
+ * - `synthesis`: no AI, no per-block output. Only log the final synthesis of the
14
+ * documents that are up to date and the ones that still have blocks to edit.
15
+ */
16
+ type ReviewDocMode = 'apply' | 'report' | 'synthesis';
6
17
  type ReviewDocOptions = {
7
18
  docPattern: string[];
8
19
  locales: Locale[];
@@ -16,18 +27,14 @@ type ReviewDocOptions = {
16
27
  skipIfModifiedAfter?: number | string | Date;
17
28
  skipIfExists?: boolean;
18
29
  gitOptions?: ListGitFilesOptions;
19
- /**
20
- * Log-only mode. Instead of translating the changed blocks with AI, log the
21
- * blocks that need attention (with line numbers and content) for the base and
22
- * target locales, so another agent can generate the translations.
23
- */
24
- log?: boolean;
30
+ /** How the review should be run. Defaults to `apply`. See {@link ReviewDocMode}. */
31
+ mode?: ReviewDocMode;
25
32
  };
26
33
  /**
27
34
  * Main audit function: scans all .md files in "en/" (unless you specified DOC_LIST),
28
35
  * then audits them to each locale in LOCALE_LIST.
29
36
  */
30
- declare const reviewDoc: ({ docPattern, locales, excludedGlobPattern, baseLocale, aiOptions, nbSimultaneousFileProcessed, configOptions, customInstructions, skipIfModifiedBefore, skipIfModifiedAfter, skipIfExists, gitOptions, log }: ReviewDocOptions) => Promise<void>;
37
+ declare const reviewDoc: ({ docPattern, locales, excludedGlobPattern, baseLocale, aiOptions, nbSimultaneousFileProcessed, configOptions, customInstructions, skipIfModifiedBefore, skipIfModifiedAfter, skipIfExists, gitOptions, mode }: ReviewDocOptions) => Promise<ReviewFileResult[]>;
31
38
  //#endregion
32
- export { reviewDoc };
39
+ export { ReviewDocMode, reviewDoc };
33
40
  //# sourceMappingURL=reviewDoc.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"reviewDoc.d.ts","names":[],"sources":["../../../src/reviewDoc/reviewDoc.ts"],"mappings":";;;;;KAiCK;EACH;EACA,SAAS;EACT;EACA,YAAY;EACZ,YAAY;EACZ;EACA,gBAAgB;EAChB;EACA,yCAAyC;EACzC,wCAAwC;EACxC;EACA,aAAa;;;;;;EAMb;;;;;;cAOW,cAAS,YAAA,SAAA,qBAAA,YAAA,WAAA,6BAAA,eAAA,oBAAA,sBAAA,qBAAA,cAAA,YAAA,OAcnB,qBAAgB"}
1
+ {"version":3,"file":"reviewDoc.d.ts","names":[],"sources":["../../../src/reviewDoc/reviewDoc.ts"],"mappings":";;;;;;;;;;;;;;;KAmDY;KAEP;EACH;EACA,SAAS;EACT;EACA,YAAY;EACZ,YAAY;EACZ;EACA,gBAAgB;EAChB;EACA,yCAAyC;EACzC,wCAAwC;EACxC;EACA,aAAa;;EAEb,OAAO;;;;;;cAOI,cAAS,YAAA,SAAA,qBAAA,YAAA,WAAA,6BAAA,eAAA,oBAAA,sBAAA,qBAAA,cAAA,YAAA,QAcnB,qBAAgB,QAAA"}