@doist/doistbot-cli 1.0.11 → 1.0.12

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/doistbot-cli",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "Local self-review CLI for Doistbot",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,20 +40,21 @@
40
40
  "test": "vitest run src scripts --config ../../config/vitest.base.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@inquirer/checkbox": "5.2.1",
44
- "@inquirer/password": "5.1.1",
45
- "@earendil-works/pi-ai": "0.84.3",
46
- "@earendil-works/pi-coding-agent": "0.84.3",
43
+ "@earendil-works/pi-ai": "0.85.1",
44
+ "@earendil-works/pi-coding-agent": "0.85.1",
45
+ "@inquirer/checkbox": "5.2.4",
46
+ "@inquirer/password": "5.2.1",
47
47
  "@napi-rs/keyring": "1.3.0",
48
+ "@opentelemetry/api": "1.9.1",
48
49
  "ajv": "8.20.0",
49
50
  "chalk": "4.1.2",
50
51
  "commander": "14.0.3",
51
- "js-yaml": "4.3.1",
52
+ "js-yaml": "4.3.2",
52
53
  "octokit": "5.0.5",
53
- "undici": "8.10.0",
54
- "yocto-spinner": "1.2.0"
54
+ "undici": "8.10.2",
55
+ "yocto-spinner": "1.2.2"
55
56
  },
56
57
  "devDependencies": {
57
- "vitest": "4.1.9"
58
+ "vitest": "4.1.11"
58
59
  }
59
60
  }
@@ -141,6 +141,9 @@ export function parseRawFindings(comments, modelName) {
141
141
  side,
142
142
  body: comment.body.trim(),
143
143
  sourceModel: modelName,
144
+ ...(comment.translationResolution
145
+ ? { translationResolution: comment.translationResolution }
146
+ : {}),
144
147
  };
145
148
  });
146
149
  }
@@ -1,4 +1,5 @@
1
1
  import { Ajv } from 'ajv';
2
+ import { TRANSLATION_RESOLUTIONS } from './types.js';
2
3
  export const REVIEW_SCHEMA = {
3
4
  type: 'object',
4
5
  additionalProperties: false,
@@ -14,7 +15,13 @@ export const REVIEW_SCHEMA = {
14
15
  line: { type: 'integer', minimum: 1 },
15
16
  side: { type: 'string', enum: ['LEFT', 'RIGHT'] },
16
17
  body: { type: 'string', minLength: 1 },
18
+ translationResolution: {
19
+ type: 'string',
20
+ enum: TRANSLATION_RESOLUTIONS,
21
+ },
17
22
  },
23
+ // Classification stays optional so omission keeps the finding instead of failing
24
+ // the entire review pass.
18
25
  required: ['path', 'line', 'side', 'body'],
19
26
  },
20
27
  },
@@ -1 +1,4 @@
1
- export {};
1
+ export const TRANSLATION_RESOLUTIONS = [
2
+ 'automated_sync_resolves',
3
+ 'code_change_required_or_uncertain',
4
+ ];
@@ -1,4 +1,4 @@
1
- const MAX_DIFF_CHARS = 128000;
1
+ export const MAX_DIFF_CHARS = 128000;
2
2
  /**
3
3
  * Converts GitHub's file list (with unified patches) into annotated diffs plus fast line lookups.
4
4
  */
@@ -8,6 +8,7 @@ import { buildMultiFocusPrompt, REVIEW_FOCUS_DEFINITIONS, } from '../multi-focus
8
8
  import { buildFallbackSummary, summarizeReviewFindings } from '../review-summary.js';
9
9
  import { parseSeverity } from '../severity.js';
10
10
  import { buildGeminiFlashReviewSummaryModel } from '../summary-model.js';
11
+ import { filterTranslationSyncOnlyFindings } from '../translation-review-policy.js';
11
12
  import { dedupeMultiFocusFindings } from './dedupe.js';
12
13
  import { buildGeminiFlashCredential, getApiKeys, getAvailableModels, getEnabledReviewModels, getOpenRouterReviewCredential, getProviderFailures, requestModelReview, } from './shared.js';
13
14
  const DEFAULT_REVIEW_MODELS_BY_FOCUS = {
@@ -340,8 +341,26 @@ export async function runMultiFocusReviewEngine({ ctx, prompt, lineLookup, prTit
340
341
  providerFailures,
341
342
  };
342
343
  }
343
- const allFindings = successfulRuns.flatMap((run) => run.findings);
344
- const reviewObservations = collectReviewObservations(successfulRuns);
344
+ const modelFindings = successfulRuns.flatMap((run) => run.findings);
345
+ const { findings: allFindings, droppedFindings: translationSyncFindings } = filterTranslationSyncOnlyFindings({
346
+ repoOwner: ctx.owner,
347
+ repoName: ctx.repo,
348
+ findings: modelFindings,
349
+ });
350
+ if (translationSyncFindings.length > 0) {
351
+ logger.info('Filtered translation sync-only findings', {
352
+ repository: `${ctx.owner}/${ctx.repo}`,
353
+ droppedCount: translationSyncFindings.length,
354
+ droppedFindingIds: translationSyncFindings.map((finding) => finding.id),
355
+ });
356
+ }
357
+ const suppressedFindings = new Set(translationSyncFindings);
358
+ const reviewObservationRuns = successfulRuns.map((run) => {
359
+ const hasSuppressedFinding = run.findings.some((finding) => suppressedFindings.has(finding));
360
+ const hasKeptFinding = run.findings.some((finding) => !suppressedFindings.has(finding));
361
+ return hasSuppressedFinding && !hasKeptFinding ? { ...run, summary: '' } : run;
362
+ });
363
+ const reviewObservations = collectReviewObservations(reviewObservationRuns);
345
364
  let dedupedFindings = allFindings;
346
365
  try {
347
366
  dedupedFindings = await dedupeMultiFocusFindings({
@@ -380,7 +399,9 @@ export async function runMultiFocusReviewEngine({ ctx, prompt, lineLookup, prTit
380
399
  supportModels: allAvailableModels,
381
400
  focuses: focusDefinitions.map((focus) => focus.name),
382
401
  successfulRuns: successfulRuns.length,
383
- findingsCount: allFindings.length,
402
+ findingsCount: modelFindings.length,
403
+ findingsAfterPolicyCount: allFindings.length,
404
+ translationSyncFindingsCount: translationSyncFindings.length,
384
405
  dedupedFindingsCount: dedupedFindings.length,
385
406
  inlineFindingsCount: routedFindings.inlineFindings.length,
386
407
  summaryOnlyFindingsCount: routedFindings.summaryFindings.length,
@@ -1,14 +1,36 @@
1
1
  import { execFileSync } from 'child_process';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- import {} from './diff.js';
4
+ import { MAX_DIFF_CHARS } from './diff.js';
5
5
  const GIT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
6
6
  export function createGitRunner(workspaceDir) {
7
- return (args) => execFileSync('git', ['-C', workspaceDir, '-c', 'core.quotePath=false', ...args], {
8
- encoding: 'utf8',
9
- maxBuffer: GIT_MAX_BUFFER_BYTES,
10
- stdio: ['ignore', 'pipe', 'pipe'],
11
- });
7
+ return (args, options) => {
8
+ try {
9
+ return execFileSync('git', ['-C', workspaceDir, '-c', 'core.quotePath=false', ...args], {
10
+ encoding: 'utf8',
11
+ maxBuffer: options?.maxBufferBytes ?? GIT_MAX_BUFFER_BYTES,
12
+ stdio: ['ignore', 'pipe', 'pipe'],
13
+ });
14
+ }
15
+ catch (error) {
16
+ const failure = error;
17
+ const stdout = typeof failure.stdout === 'string'
18
+ ? failure.stdout
19
+ : Buffer.isBuffer(failure.stdout)
20
+ ? failure.stdout.toString('utf8')
21
+ : '';
22
+ if (failure.code === 'ENOBUFS' &&
23
+ options?.returnPartialOutputOnMaxBuffer === true &&
24
+ options.maxBufferBytes !== undefined) {
25
+ return stdout.slice(0, options.maxBufferBytes);
26
+ }
27
+ if (typeof failure.status === 'number' &&
28
+ options?.allowedExitCodes?.includes(failure.status)) {
29
+ return stdout;
30
+ }
31
+ throw error;
32
+ }
33
+ };
12
34
  }
13
35
  export function tryGit(runner, args) {
14
36
  try {
@@ -75,7 +97,7 @@ function unquoteGitPath(rawPath) {
75
97
  unquoted = trimmed.slice(1, -1);
76
98
  }
77
99
  }
78
- return unquoted.replace(/^[ab]\//, '');
100
+ return unquoted.replace(/^[ab]\//, '').replace(/^\.\//, '');
79
101
  }
80
102
  function readDiffGitPath(input, startIndex) {
81
103
  let index = startIndex;
@@ -162,6 +184,38 @@ export function parseGitDiffToFiles(diff) {
162
184
  flushCurrent();
163
185
  return files;
164
186
  }
187
+ function getUntrackedReviewFiles(runner) {
188
+ const untrackedPaths = runner(['ls-files', '--others', '--exclude-standard', '-z'])
189
+ .split('\0')
190
+ .filter(Boolean);
191
+ if (untrackedPaths.length === 0) {
192
+ return [];
193
+ }
194
+ const files = [];
195
+ let remainingDiffChars = MAX_DIFF_CHARS;
196
+ for (const filename of untrackedPaths) {
197
+ if (remainingDiffChars <= 0) {
198
+ break;
199
+ }
200
+ // The two-file form also works with Git versions before 2.51.
201
+ const diff = runner([
202
+ 'diff',
203
+ '--no-index',
204
+ '--no-ext-diff',
205
+ '--no-color',
206
+ '--',
207
+ '/dev/null',
208
+ `./${filename}`,
209
+ ], {
210
+ allowedExitCodes: [1],
211
+ maxBufferBytes: remainingDiffChars,
212
+ returnPartialOutputOnMaxBuffer: true,
213
+ });
214
+ files.push(...parseGitDiffToFiles(diff));
215
+ remainingDiffChars -= diff.length;
216
+ }
217
+ return files;
218
+ }
165
219
  export function getLocalReviewFiles({ runner, source, }) {
166
220
  if (source === 'staged') {
167
221
  const diff = runner(['diff', '--staged', '--no-ext-diff', '--no-color']);
@@ -183,7 +237,7 @@ export function getLocalReviewFiles({ runner, source, }) {
183
237
  const mergeBase = runner(['merge-base', baseBranch, 'HEAD']).trim();
184
238
  const diff = runner(['diff', '--no-ext-diff', '--no-color', mergeBase]);
185
239
  return {
186
- files: parseGitDiffToFiles(diff),
240
+ files: [...parseGitDiffToFiles(diff), ...getUntrackedReviewFiles(runner)],
187
241
  baseBranch,
188
242
  };
189
243
  }
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { logger } from '../../core/logging.js';
5
+ import { buildTranslationReviewGuidance } from './translation-review-policy.js';
5
6
  const MAX_BODY_CHARS = 4000;
6
7
  let cachedStandardsSection = null;
7
8
  const REVIEW_TASK_DIR = path.dirname(fileURLToPath(import.meta.url));
@@ -75,5 +76,6 @@ export function buildReviewPromptVariables({ repoOwner, repoName, prNumber, prTi
75
76
  PR_CONVERSATION: formatPrConversation(prConversation),
76
77
  DIFF_SECTION: fullDiffSection,
77
78
  STANDARDS_SECTION: standardsSection,
79
+ TRANSLATION_REVIEW_GUIDANCE: buildTranslationReviewGuidance(repoOwner, repoName),
78
80
  };
79
81
  }
@@ -0,0 +1,37 @@
1
+ const ENGLISH_SOURCE_FIRST_REPOSITORIES = new Set([
2
+ 'doist/todoist-web',
3
+ 'doist/todoist-android',
4
+ 'doist/todoist-ios',
5
+ 'doist/todoist-electron',
6
+ 'doist/todoist-macos',
7
+ ]);
8
+ export function usesEnglishSourceFirstTranslations(repoOwner, repoName) {
9
+ return ENGLISH_SOURCE_FIRST_REPOSITORIES.has(`${repoOwner}/${repoName}`.toLowerCase());
10
+ }
11
+ export function buildTranslationReviewGuidance(repoOwner, repoName) {
12
+ if (!usesEnglishSourceFirstTranslations(repoOwner, repoName)) {
13
+ return '';
14
+ }
15
+ return `## Repository-specific Translation Workflow
16
+
17
+ This repository uses an English-source-first workflow: feature PRs change the English source strings, automation sends them to the repository's translation service, and translated locale files arrive through a later automated sync.
18
+
19
+ - Do not create a finding or summary note merely because translated locale files or generated locale bundles are unchanged in the feature PR.
20
+ - Do not ask the author to pull, update, generate, or commit translations manually. If the PR description already mentions the translation-service follow-up, including Transifex, do not repeat it.
21
+ - Continue to report concrete i18n implementation errors introduced by the diff, such as incorrect translation API or gettext usage, broken interpolation placeholders, pluralization or context mistakes, and patterns that prevent source-string extraction.`;
22
+ }
23
+ export function isTranslationSyncOnlyFinding(repoOwner, repoName, finding) {
24
+ return (usesEnglishSourceFirstTranslations(repoOwner, repoName) &&
25
+ finding.translationResolution === 'automated_sync_resolves');
26
+ }
27
+ export function filterTranslationSyncOnlyFindings({ repoOwner, repoName, findings, }) {
28
+ const keptFindings = [];
29
+ const droppedFindings = [];
30
+ for (const finding of findings) {
31
+ const destination = isTranslationSyncOnlyFinding(repoOwner, repoName, finding)
32
+ ? droppedFindings
33
+ : keptFindings;
34
+ destination.push(finding);
35
+ }
36
+ return { findings: keptFindings, droppedFindings };
37
+ }
@@ -73,6 +73,8 @@ $DIFF_SECTION
73
73
  - Keep code ranges tight and comments easy to understand without close reading.
74
74
  - **Do not flag dependency or tooling versions as nonexistent, outdated, or "too new."** Your training data has a cutoff, so you can't know which versions of GitHub Actions, npm/PyPI/etc. packages, or their APIs are currently published. Never tell the author a version doesn't exist, is wrong, or should be downgraded based on your own knowledge — and never claim a library feature or API is unavailable because you assume the project uses an older major version. Raise a version-related finding only when the diff or checked-out repo contains concrete contradicting evidence (e.g., the manifest/lockfile pins a different version, or the referenced symbol is absent from the dependency's actual source on disk).
75
75
 
76
+ $TRANSLATION_REVIEW_GUIDANCE
77
+
76
78
  ## Writing Style
77
79
 
78
80
  - Write clearly, concisely, and directly. Be engaging without sounding generic.
@@ -97,6 +99,20 @@ At the beginning of each finding title, tag it with a priority level:
97
99
 
98
100
  ## Output Format
99
101
 
102
+ For every finding, answer this internal classification question: assuming the repository's normal
103
+ translation automation completes successfully, would the issue be fully resolved without changing
104
+ application code, source-string keys, extraction configuration, lookup behavior, placeholders,
105
+ pluralization, or locale context?
106
+
107
+ - Use `automated_sync_resolves` only when the answer is clearly yes.
108
+ - Use `code_change_required_or_uncertain` for all non-translation findings, concrete i18n
109
+ correctness problems, and any uncertain case.
110
+ - Keep this classification out of the finding body.
111
+ - Missing or stale translated content, requests to pull or commit locale artifacts, and advice to
112
+ wait for the automated sync are `automated_sync_resolves` when no code-level problem remains.
113
+ - Broken extraction, keys, lookups, placeholders, pluralization, or locale handling are
114
+ `code_change_required_or_uncertain`, even when their symptom is a missing translation.
115
+
100
116
  Return strict JSON in this exact shape:
101
117
 
102
118
  ```json
@@ -107,7 +123,8 @@ Return strict JSON in this exact shape:
107
123
  "path": "relative file path exactly as shown in the diff above",
108
124
  "line": 42,
109
125
  "side": "RIGHT",
110
- "body": "[P2] Brief description of the issue and actionable fix."
126
+ "body": "[P2] Brief description of the issue and actionable fix.",
127
+ "translationResolution": "code_change_required_or_uncertain"
111
128
  }
112
129
  ]
113
130
  }