feat(cli): soft-validate impact --kind (U4, #1907)

An unknown --kind value was silently a no-op. Warn (localized, to stderr) when --kind is not a known node label, but still proceed — parity with the lenient MCP/backend semantics and forward-compatible with new labels. Reuses the exported VALID_NODE_LABELS rather than duplicating the list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-05-30 08:11:04 +00:00
parent bb110aca91
commit 4679c9e1b9
4 changed files with 40 additions and 2 deletions

View file

@ -46,6 +46,8 @@ export const en = {
'tool.usage.impact':
'Usage: gitnexus impact <symbol_name> [--uid <uid>] [--file <path>] [--kind <kind>] [--direction upstream|downstream]',
'tool.usage.cypher': 'Usage: gitnexus cypher <cypher_query>',
'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.changesSummary': 'Changes: {{files}} files, {{symbols}} symbols',
'tool.detectChanges.affectedProcesses': 'Affected processes: {{count}}',

View file

@ -50,6 +50,8 @@ export const zhCN = {
'tool.usage.impact':
'用法:gitnexus impact <符号名> [--uid <uid>] [--file <路径>] [--kind <类型>] [--direction upstream|downstream]',
'tool.usage.cypher': '用法:gitnexus cypher <Cypher 查询>',
'tool.warn.unknownKind':
"--kind '{{kind}}' 不是已知的符号类型(如 Function、Class、Method),不会用于缩小结果范围。",
'tool.detectChanges.noChanges': '未检测到变更。',
'tool.detectChanges.changesSummary': '变更:{{files}} 个文件,{{symbols}} 个符号',
'tool.detectChanges.affectedProcesses': '受影响流程:{{count}}',

View file

@ -16,8 +16,8 @@
*/
import { writeSync } from 'node:fs';
import { LocalBackend } from '../mcp/local/local-backend.js';
import { cliErrorKey } from './cli-message.js';
import { LocalBackend, VALID_NODE_LABELS } from '../mcp/local/local-backend.js';
import { cliErrorKey, cliWarnKey } from './cli-message.js';
import { formatDetectChangesResult } from './detect-changes-format.js';
let _backend: LocalBackend | null = null;
@ -143,6 +143,12 @@ export async function impactCommand(
cliErrorKey('tool.usage.impact');
process.exit(1);
}
// Soft-validate --kind: an unknown kind is a no-op hint (the backend scores
// it but it matches nothing), so warn and proceed rather than rejecting —
// parity with the lenient MCP surface and forward-compatible with new labels.
if (options?.kind && !VALID_NODE_LABELS.has(options.kind)) {
cliWarnKey('tool.warn.unknownKind', { kind: options.kind });
}
try {
const backend = await getBackend();

View file

@ -23,6 +23,8 @@ vi.mock('../../src/mcp/local/local-backend.js', () => ({
init = init;
callTool = callTool;
},
// U4: impactCommand imports VALID_NODE_LABELS to soft-validate --kind.
VALID_NODE_LABELS: new Set(['Function', 'Class', 'Interface', 'Method', 'Constructor']),
}));
// impactCommand prints its result via fs.writeSync(fd 1, …). Silence that so
@ -109,4 +111,30 @@ describe('CLI impact disambiguation flags (#1907)', () => {
exitSpy.mockRestore();
});
// U4 (#1914 review F3): an unknown --kind warns to stderr but still resolves.
it('warns on an unknown --kind but still forwards the request', async () => {
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
await impactCommand('login', { kind: 'Funktion', direction: 'upstream' });
expect(callTool).toHaveBeenCalledTimes(1);
const stderr = stderrSpy.mock.calls.map((c) => String(c[0])).join('');
expect(stderr).toContain('Funktion');
expect(stderr).toContain('not a known symbol kind');
stderrSpy.mockRestore();
});
it('does not warn for a known --kind', async () => {
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
await impactCommand('login', { kind: 'Function', direction: 'upstream' });
expect(callTool).toHaveBeenCalledTimes(1);
const stderr = stderrSpy.mock.calls.map((c) => String(c[0])).join('');
expect(stderr).not.toContain('not a known symbol kind');
stderrSpy.mockRestore();
});
});