style(#794): apply prettier --write to files added/changed by this PR

Fixes the `quality / format` CI job. `npx prettier --check .` reported
29 files repo-wide; this commit reformats only the 9 files that this PR
authored/modified:

- gitnexus/src/cli/group.ts
- gitnexus/src/core/group/{cross-impact,service}.ts
- gitnexus/src/mcp/{tools,resources}.ts
- gitnexus/src/mcp/local/local-backend.ts
- gitnexus/test/unit/group/{cross-impact,service}.test.ts
- gitnexus/test/unit/mcp/group-repo-routing.test.ts

The remaining 20 prettier offenders are pre-existing on `main`
(`.claude-plugin/marketplace.json`, `.mcp.json`, several
`gitnexus-claude-plugin/**/*.json`, `gitnexus-cursor-integration/hooks/hooks.json`,
`gitnexus/test/unit/{ast-cache,embedder,lazy-action,pipeline-exports}.test.ts`,
`gitnexus/test/utils/hook-test-helpers.ts`, `gitnexus/tsconfig.test.json`,
`gitnexus-web/tsconfig.node.json`, `.github/FUNDING.yml`) and out of
scope for this PR.

Re-verified after format:
- `npx tsc --noEmit` (in gitnexus/) — clean.
- `npx vitest run --pool=forks` over the #794 verification block —
  9 files / 111 tests passed.

Refs #794

Made-with: Cursor
This commit is contained in:
ivkond 2026-04-19 14:10:17 +03:00
parent 0d00dd71dd
commit ddf702b4af
9 changed files with 118 additions and 76 deletions

View file

@ -194,7 +194,10 @@ export function registerGroupCommands(program: Command): void {
)
.option('--direction <dir>', 'upstream or downstream', 'upstream')
.option('--service <path>', 'Optional monorepo service directory prefix (path filter)')
.option('--subgroup <path>', 'Optional prefix limiting which group repos participate in cross fan-out')
.option(
'--subgroup <path>',
'Optional prefix limiting which group repos participate in cross fan-out',
)
.option('--max-depth <n>', 'Max graph traversal depth')
.option('--cross-depth <n>', 'Cross-repository hop depth')
.option('--min-confidence <n>', 'Minimum relation confidence (0–1)')

View file

@ -18,12 +18,7 @@ import type { GroupRepoHandle, GroupToolPort } from './service.js';
import { loadGroupConfig } from './config-parser.js';
import { fileMatchesServicePrefix, normalizeServicePrefix } from './group-path-utils.js';
import { getGroupDir } from './storage.js';
import {
closeBridgeDb,
openBridgeDbReadOnly,
queryBridge,
readBridgeMeta,
} from './bridge-db.js';
import { closeBridgeDb, openBridgeDbReadOnly, queryBridge, readBridgeMeta } from './bridge-db.js';
import { BRIDGE_SCHEMA_VERSION } from './bridge-schema.js';
/** Cross-boundary hops beyond this value are clamped (multi-hop reserved for future work). */
@ -98,29 +93,36 @@ function clampCrossDepth(raw: unknown): { depth: number; warning?: string } {
return { depth: d };
}
export function validateGroupImpactParams(params: Record<string, unknown>): {
ok: true;
name: string;
repoPath: string;
target: string;
direction: 'upstream' | 'downstream';
maxDepth: number;
crossDepth: number;
crossDepthWarning?: string;
relationTypes?: string[];
includeTests: boolean;
minConfidence: number;
service?: string;
subgroup?: string;
timeoutMs: number;
} | { ok: false; error: string } {
export function validateGroupImpactParams(params: Record<string, unknown>):
| {
ok: true;
name: string;
repoPath: string;
target: string;
direction: 'upstream' | 'downstream';
maxDepth: number;
crossDepth: number;
crossDepthWarning?: string;
relationTypes?: string[];
includeTests: boolean;
minConfidence: number;
service?: string;
subgroup?: string;
timeoutMs: number;
}
| { ok: false; error: string } {
const name = String(params.name ?? '').trim();
const repoPath = String(params.repo ?? '').trim();
const target = String(params.target ?? '').trim();
if (!name) return { ok: false, error: 'name is required' };
if (!repoPath) return { ok: false, error: 'repo is required (group repo path, e.g. app/backend)' };
if (!repoPath)
return { ok: false, error: 'repo is required (group repo path, e.g. app/backend)' };
if (!target) return { ok: false, error: 'target is required' };
if (params.service !== undefined && params.service !== null && String(params.service).trim() === '') {
if (
params.service !== undefined &&
params.service !== null &&
String(params.service).trim() === ''
) {
return { ok: false, error: 'service must not be an empty string' };
}
const direction = parseDirection(params.direction);
@ -292,7 +294,8 @@ function rowToNeighbor(r: Record<string, unknown>): BridgeNeighborRow | null {
return {
neighborRepo,
neighborUid,
neighborFilePath: r.neighborFilePath !== undefined ? String(r.neighborFilePath) : String(r[2] ?? ''),
neighborFilePath:
r.neighborFilePath !== undefined ? String(r.neighborFilePath) : String(r[2] ?? ''),
matchType: String(r.matchType ?? r[3] ?? 'exact'),
confidence: Number(r.confidence ?? r[4] ?? 0),
contractId: String(r.contractId ?? r[5] ?? ''),
@ -500,17 +503,12 @@ export async function runGroupImpact(
continue;
}
const fan = await deps.port.impactByUid(
neighborHandle.id,
n.neighborUid,
direction,
{
maxDepth,
relationTypes: relationTypes ?? [],
minConfidence,
includeTests,
},
);
const fan = await deps.port.impactByUid(neighborHandle.id, n.neighborUid, direction, {
maxDepth,
relationTypes: relationTypes ?? [],
minConfidence,
includeTests,
});
if (fan == null) {
truncatedRepos.push(n.neighborRepo);
continue;

View file

@ -8,12 +8,7 @@ import path from 'node:path';
import { checkStaleness } from '../git-staleness.js';
import { loadGroupConfig } from './config-parser.js';
import { fileMatchesServicePrefix, normalizeServicePrefix } from './group-path-utils.js';
import {
getDefaultGitnexusDir,
getGroupDir,
listGroups,
readContractRegistry,
} from './storage.js';
import { getDefaultGitnexusDir, getGroupDir, listGroups, readContractRegistry } from './storage.js';
import { syncGroup } from './sync.js';
import type {
ContractRegistry,
@ -109,11 +104,17 @@ function isStoredContract(raw: unknown): raw is StoredContract {
}
function filterQueryByServicePrefix(
queryResult: { processes?: Array<Record<string, unknown>>; process_symbols?: Array<Record<string, unknown>> },
queryResult: {
processes?: Array<Record<string, unknown>>;
process_symbols?: Array<Record<string, unknown>>;
},
servicePrefix: string,
): { processes: Array<Record<string, unknown>>; process_symbols: Array<Record<string, unknown>> } {
const symbols = (queryResult.process_symbols || []).filter((s) =>
fileMatchesServicePrefix(typeof s.filePath === 'string' ? s.filePath : undefined, servicePrefix),
fileMatchesServicePrefix(
typeof s.filePath === 'string' ? s.filePath : undefined,
servicePrefix,
),
);
const allowed = new Set(
symbols.map((s) => String((s as { process_id?: string }).process_id ?? '')).filter(Boolean),
@ -135,8 +136,7 @@ function isCrossLink(raw: unknown): raw is CrossLink {
async function loadContractRegistryResilient(
groupDir: string,
): Promise<
| { ok: true; registry: ContractRegistry; skippedCorrupt: number }
| { ok: false; error: string }
{ ok: true; registry: ContractRegistry; skippedCorrupt: number } | { ok: false; error: string }
> {
const filePath = path.join(groupDir, 'contracts.json');
let raw: string;
@ -206,9 +206,7 @@ async function loadContractRegistryResilient(
base.repoSnapshots && typeof base.repoSnapshots === 'object' && base.repoSnapshots !== null
? (base.repoSnapshots as Record<string, { indexedAt: string; lastCommit: string }>)
: {},
missingRepos: Array.isArray(base.missingRepos)
? (base.missingRepos as string[])
: [],
missingRepos: Array.isArray(base.missingRepos) ? (base.missingRepos as string[]) : [],
contracts,
crossLinks,
};
@ -295,7 +293,11 @@ export class GroupService {
const uid = typeof params.uid === 'string' ? params.uid.trim() : undefined;
const file_path = typeof params.file_path === 'string' ? params.file_path : undefined;
const include_content = Boolean(params.include_content);
if (params.service !== undefined && params.service !== null && String(params.service).trim() === '') {
if (
params.service !== undefined &&
params.service !== null &&
String(params.service).trim() === ''
) {
return { group: name || '', error: 'service must not be an empty string', results: [] };
}
const servicePrefix = normalizeServicePrefix(params.service);
@ -367,7 +369,11 @@ export class GroupService {
const name = String(params.name ?? '').trim();
const queryText = String(params.query ?? '').trim();
if (!name || !queryText) return { error: 'name and query are required' };
if (params.service !== undefined && params.service !== null && String(params.service).trim() === '') {
if (
params.service !== undefined &&
params.service !== null &&
String(params.service).trim() === ''
) {
return { error: 'service must not be an empty string' };
}
const servicePrefix = normalizeServicePrefix(params.service);

View file

@ -2892,7 +2892,8 @@ export class LocalBackend {
if (params.relationTypes !== undefined) impactArgs.relationTypes = params.relationTypes;
if (params.includeTests !== undefined) impactArgs.includeTests = params.includeTests;
if (params.minConfidence !== undefined) impactArgs.minConfidence = params.minConfidence;
if (params.service !== undefined && params.service !== null) impactArgs.service = params.service;
if (params.service !== undefined && params.service !== null)
impactArgs.service = params.service;
if (typeof params.subgroup === 'string') impactArgs.subgroup = params.subgroup;
if (params.timeoutMs !== undefined) impactArgs.timeoutMs = params.timeoutMs;
if (params.timeout !== undefined) impactArgs.timeout = params.timeout;
@ -2908,7 +2909,8 @@ export class LocalBackend {
if (typeof params.limit === 'number') queryArgs.limit = params.limit;
if (typeof params.max_symbols === 'number') queryArgs.max_symbols = params.max_symbols;
if (params.include_content !== undefined) queryArgs.include_content = params.include_content;
if (params.service !== undefined && params.service !== null) queryArgs.service = params.service;
if (params.service !== undefined && params.service !== null)
queryArgs.service = params.service;
if (memberRest !== undefined) {
queryArgs.subgroup = memberRest;
queryArgs.subgroupExact = true;
@ -2928,8 +2930,10 @@ export class LocalBackend {
};
if (typeof params.uid === 'string') contextArgs.uid = params.uid;
if (typeof params.file_path === 'string') contextArgs.file_path = params.file_path;
if (params.include_content !== undefined) contextArgs.include_content = params.include_content;
if (params.service !== undefined && params.service !== null) contextArgs.service = params.service;
if (params.include_content !== undefined)
contextArgs.include_content = params.include_content;
if (params.service !== undefined && params.service !== null)
contextArgs.service = params.service;
if (memberRest !== undefined) {
contextArgs.subgroup = memberRest;
contextArgs.subgroupExact = true;

View file

@ -153,7 +153,10 @@ export function parseResourceUri(uri: string): ParsedGitnexusResource {
}
if (u.hostname === 'group') {
const segments = u.pathname.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
const segments = u.pathname
.replace(/^\/+|\/+$/g, '')
.split('/')
.filter(Boolean);
if (segments.length < 2) {
throw new Error(
`Invalid group resource URI (expected gitnexus://group/{name}/contracts or .../status): ${uri}`,
@ -186,7 +189,10 @@ export function parseResourceUri(uri: string): ParsedGitnexusResource {
}
if (u.hostname === 'repo') {
const segments = u.pathname.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
const segments = u.pathname
.replace(/^\/+|\/+$/g, '')
.split('/')
.filter(Boolean);
if (segments.length < 2) {
throw new Error(`Unknown resource URI: ${uri}`);
}
@ -342,7 +348,9 @@ async function getContextResource(backend: LocalBackend, repoName?: string): Pro
lines.push(` - gitnexus://repo/${context.projectName}/processes: All execution flows`);
lines.push(` - gitnexus://repo/${context.projectName}/cluster/{name}: Module details`);
lines.push(` - gitnexus://repo/${context.projectName}/process/{name}: Process trace`);
lines.push(' - gitnexus://group/{name}/contracts: Group contract registry (optional ?type=&repo=&unmatchedOnly=)');
lines.push(
' - gitnexus://group/{name}/contracts: Group contract registry (optional ?type=&repo=&unmatchedOnly=)',
);
lines.push(' - gitnexus://group/{name}/status: Group index / contract staleness');
return lines.join('\n');

View file

@ -357,7 +357,8 @@ SERVICE: optional monorepo path prefix (case-sensitive path segments). When "rep
includeTests: { type: 'boolean', description: 'Include test files (default: false)' },
minConfidence: {
type: 'number',
description: 'Minimum edge confidence 0–1 (default: 0 when omitted; server clamps to 0–1)',
description:
'Minimum edge confidence 0–1 (default: 0 when omitted; server clamps to 0–1)',
default: 0,
minimum: 0,
maximum: 1,
@ -380,7 +381,8 @@ SERVICE: optional monorepo path prefix (case-sensitive path segments). When "rep
},
timeoutMs: {
type: 'number',
description: 'Wall-clock budget in milliseconds for the Phase-1 local impact leg (default 30000)',
description:
'Wall-clock budget in milliseconds for the Phase-1 local impact leg (default 30000)',
minimum: 1,
maximum: 3600000,
},

View file

@ -53,7 +53,12 @@ describe('cross-impact', () => {
});
it('test_validateGroupImpactParams_rejects_bad_direction', () => {
const r = validateGroupImpactParams({ name: 'g', repo: 'a', target: 't', direction: 'sideways' });
const r = validateGroupImpactParams({
name: 'g',
repo: 'a',
target: 't',
direction: 'sideways',
});
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error).toContain('direction');
});

View file

@ -398,7 +398,10 @@ repos:
query: 'x',
subgroup: 'app/frontend',
})) as { per_repo: Array<{ repo: string }> };
expect(prefixOnly.per_repo.map((r) => r.repo).sort()).toEqual(['app/frontend', 'app/frontend/mobile']);
expect(prefixOnly.per_repo.map((r) => r.repo).sort()).toEqual([
'app/frontend',
'app/frontend/mobile',
]);
const exact = (await svc.groupQuery({
name: 'nest-group',

View file

@ -63,8 +63,12 @@ repos:
`,
);
vi.stubEnv('GITNEXUS_HOME', tmpDir);
groupSpyQuery = vi.spyOn(GroupService.prototype, 'groupQuery').mockResolvedValue({ via: 'query' });
groupSpyImpact = vi.spyOn(GroupService.prototype, 'groupImpact').mockResolvedValue({ via: 'impact' });
groupSpyQuery = vi
.spyOn(GroupService.prototype, 'groupQuery')
.mockResolvedValue({ via: 'query' });
groupSpyImpact = vi
.spyOn(GroupService.prototype, 'groupImpact')
.mockResolvedValue({ via: 'impact' });
groupSpyContext = vi.spyOn(GroupService.prototype, 'groupContext').mockResolvedValue({
group: 'g1',
results: [],
@ -139,7 +143,9 @@ repos:
const backend = new LocalBackend();
const out = await backend.callTool('query', { repo: '@no-such-group', query: 'x' });
expect(out).toHaveProperty('error');
expect(String((out as { error: string }).error)).toMatch(/not found|no such|unknown|exist|ENOENT/i);
expect(String((out as { error: string }).error)).toMatch(
/not found|no such|unknown|exist|ENOENT/i,
);
});
it('returns error for unknown member path', async () => {
@ -165,7 +171,9 @@ repos:
it('removed group_contracts mentions migration', async () => {
const backend = new LocalBackend();
await expect(backend.callTool('group_contracts', { name: 'g1' })).rejects.toThrow(/Removed tools/);
await expect(backend.callTool('group_contracts', { name: 'g1' })).rejects.toThrow(
/Removed tools/,
);
});
it('removed group_status mentions migration', async () => {
@ -204,14 +212,19 @@ repos:
params: { repo: '@myproduct', target: 'UserService.login', service: 'app/backend' },
spy: () => groupSpyContext,
},
])('$method with repo "@myproduct" routes to GroupService and forwards service', async ({ method, params, spy }) => {
const backend = new LocalBackend();
await backend.callTool(method, params);
expect(spy()).toHaveBeenCalledWith(
expect.objectContaining({ name: 'myproduct', service: 'app/backend' }),
);
const callArg = spy().mock.calls[0][0] as Record<string, unknown>;
expect(typeof callArg.repo === 'string' ? (callArg.repo as string).startsWith('@') : false).toBe(false);
});
])(
'$method with repo "@myproduct" routes to GroupService and forwards service',
async ({ method, params, spy }) => {
const backend = new LocalBackend();
await backend.callTool(method, params);
expect(spy()).toHaveBeenCalledWith(
expect.objectContaining({ name: 'myproduct', service: 'app/backend' }),
);
const callArg = spy().mock.calls[0][0] as Record<string, unknown>;
expect(
typeof callArg.repo === 'string' ? (callArg.repo as string).startsWith('@') : false,
).toBe(false);
},
);
});
});