fix(cli): do not call zero-symbol detect-changes a clean tree (#3138)

* fix(cli): do not call zero-symbol detect-changes a clean tree

Print backend summary.message and distinguish a parsed diff with no overlapping indexed symbols from an empty git diff. Pin --color=never so color.ui=always cannot hide +++ b/ headers.

* fix(cli): localize clean-tree detect-changes and skip no-overlap when partial

Production empty diffs carry English summary.message; route that through t() so zh-CN fires. Do not claim no indexed-symbol overlap on queryDegraded partial results. Pin formatter tests to en and cover the production payload shapes.

* fix(cli): prettier detect-changes-format and degraded eval assertion

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
Kevin Rajan 2026-09-02 12:59:53 -05:00 committed by GitHub
parent dea396a13c
commit 5e6b79deba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 127 additions and 2 deletions

View file

@ -6,6 +6,7 @@ type DetectChangesSummary = {
changed_count?: number;
affected_count?: number;
risk_level?: string;
message?: string;
};
type ChangedSymbol = {
@ -55,6 +56,29 @@ export function formatDetectChangesResult(result: unknown): string {
);
if ((summary.changed_count ?? 0) === 0) {
// Parse-fail payloads set `partial` and an honest `message` (#2915/#3131).
// Production *clean* trees also set English `message: 'No changes detected.'`
// — that must go through `t('tool.detectChanges.noChanges')` or zh-CN never
// fires. Only pass the backend string through on a degraded/parse-fail run.
if (
payload.partial &&
typeof summary.message === 'string' &&
summary.message.trim().length > 0
) {
return [...notes, summary.message.trim()].join('\n');
}
// Confirmed no-overlap: files parsed, mapping succeeded, zero symbols.
// `queryDegraded` is `partial: true` with the same counts and no message —
// do not call that a confirmed mapping (#3131 honesty).
if (!payload.partial && (summary.changed_files ?? 0) > 0) {
return [
...notes,
t('tool.detectChanges.noOverlappingSymbols', { files: summary.changed_files }),
].join('\n');
}
if (payload.partial) {
return notes.join('\n');
}
return [...notes, t('tool.detectChanges.noChanges')].join('\n');
}

View file

@ -76,6 +76,8 @@ export const en = {
'tool.warn.unknownKind':
"--kind '{{kind}}' is not a known symbol kind (e.g. Function, Class, Method); it will not narrow the result.",
'tool.detectChanges.noChanges': 'No changes detected.',
'tool.detectChanges.noOverlappingSymbols':
'Diff touched {{files}} file(s) but no indexed symbols overlap those hunks — not a clean tree.',
'tool.detectChanges.partial':
'PARTIAL RESULT: a graph query failed, so changed symbols may be missing. Do not read this as a clean pre-commit check.',
'tool.detectChanges.truncated':

View file

@ -78,6 +78,8 @@ export const zhCN = {
'tool.warn.unknownKind':
"--kind '{{kind}}' 不是已知的符号类型(如 Function、Class、Method不会用于缩小结果范围。",
'tool.detectChanges.noChanges': '未检测到变更。',
'tool.detectChanges.noOverlappingSymbols':
'diff 触及 {{files}} 个文件,但没有索引符号与这些 hunk 重叠 — 并非干净工作区。',
'tool.detectChanges.partial':
'结果不完整:图查询失败,可能遗漏已变更符号。请勿将其视为通过的提交前检查。',
'tool.detectChanges.truncated':

View file

@ -988,6 +988,9 @@ export function buildDetectChangesDiffArgs(scope: string, baseRef?: string): str
'diff',
'--ignore-cr-at-eol',
'--no-ext-diff',
// color.ui=always prefixes `+++ b/` with ANSI, so parseDiffHunks sees zero
// files and the CLI used to print a clean "No changes detected." (#3131).
'--color=never',
'--src-prefix=a/',
'--dst-prefix=b/',
];

View file

@ -8,11 +8,12 @@ import { parseDiffHunks } from '../../src/storage/git.js';
import { diffArgsFor } from '../helpers/detect-changes-diff-args.js';
import { commitAll, initGitRepo } from '../helpers/temp-git-repo.js';
/** The five flags every scope carries, ahead of its own ref/staging arguments. */
/** The six flags every scope carries, ahead of its own ref/staging arguments. */
const GUARD_FLAGS = [
'diff',
'--ignore-cr-at-eol',
'--no-ext-diff',
'--color=never',
'--src-prefix=a/',
'--dst-prefix=b/',
];

View file

@ -0,0 +1,92 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { formatDetectChangesResult } from '../../src/cli/detect-changes-format.js';
import { setCliLanguage } from '../../src/cli/i18n/index.js';
describe('formatDetectChangesResult — zero-symbol honesty (#3131)', () => {
beforeEach(() => {
setCliLanguage('en');
});
afterEach(() => {
setCliLanguage(null);
});
it('prints backend parse-fail message instead of a generic all-clear', () => {
const text = formatDetectChangesResult({
partial: true,
summary: {
changed_count: 0,
affected_count: 0,
risk_level: 'unknown',
message: 'Could not parse the git diff output — no file headers recognised.',
},
});
expect(text).toContain('PARTIAL RESULT');
expect(text).toContain('Could not parse the git diff output');
expect(text).not.toContain('No changes detected.');
});
it('does not call a parsed diff with no symbol overlap a clean tree', () => {
const text = formatDetectChangesResult({
summary: {
changed_count: 0,
affected_count: 0,
changed_files: 1,
risk_level: 'low',
},
});
expect(text).toMatch(/Diff touched 1 file/);
expect(text).not.toContain('No changes detected.');
expect(text).not.toContain('PARTIAL RESULT');
});
it('does not claim no-overlap when a degraded query left changed_count at zero', () => {
const text = formatDetectChangesResult({
partial: true,
summary: {
changed_count: 0,
affected_count: 0,
changed_files: 1,
risk_level: 'unknown',
},
});
expect(text).toContain('PARTIAL RESULT');
expect(text).not.toMatch(/no indexed symbols overlap/i);
expect(text).not.toContain('No changes detected.');
});
it('keeps the clean-tree sentence only when git produced no files', () => {
const text = formatDetectChangesResult({
summary: { changed_count: 0, affected_count: 0, changed_files: 0, risk_level: 'none' },
});
expect(text).toBe('No changes detected.');
});
it('localizes the production clean-tree payload that carries English summary.message', () => {
setCliLanguage('zh-CN');
const text = formatDetectChangesResult({
summary: {
changed_count: 0,
affected_count: 0,
risk_level: 'none',
message: 'No changes detected.',
},
});
expect(text).toBe('未检测到变更。');
});
it('localizes confirmed no-overlap under GITNEXUS_LANG=zh-CN', () => {
setCliLanguage('zh-CN');
const text = formatDetectChangesResult({
summary: {
changed_count: 0,
affected_count: 0,
changed_files: 1,
risk_level: 'low',
},
});
expect(text).toContain('diff 触及 1 个文件');
expect(text).not.toContain('未检测到变更。');
expect(text).not.toContain('No changes detected.');
});
});

View file

@ -604,7 +604,8 @@ describe('formatDetectChangesResult', () => {
// counts at zero. Without the note the pre-commit gate reads as "clean".
const result = formatDetectChangesResult({ partial: true, summary: { changed_count: 0 } });
expect(result).toContain('PARTIAL RESULT');
expect(result).toContain('No changes detected.');
expect(result).toContain('a graph query failed');
expect(result).not.toContain('No changes detected.');
});
it('flags a degraded run that still found symbols', () => {