diff --git a/gitnexus/src/cli/group.ts b/gitnexus/src/cli/group.ts
index d9929cc1f..eb0dffc3d 100644
--- a/gitnexus/src/cli/group.ts
+++ b/gitnexus/src/cli/group.ts
@@ -194,7 +194,10 @@ export function registerGroupCommands(program: Command): void {
)
.option('--direction
', 'upstream or downstream', 'upstream')
.option('--service ', 'Optional monorepo service directory prefix (path filter)')
- .option('--subgroup ', 'Optional prefix limiting which group repos participate in cross fan-out')
+ .option(
+ '--subgroup ',
+ 'Optional prefix limiting which group repos participate in cross fan-out',
+ )
.option('--max-depth ', 'Max graph traversal depth')
.option('--cross-depth ', 'Cross-repository hop depth')
.option('--min-confidence ', 'Minimum relation confidence (0–1)')
diff --git a/gitnexus/src/core/group/cross-impact.ts b/gitnexus/src/core/group/cross-impact.ts
index 6b549422d..fe9db5cc8 100644
--- a/gitnexus/src/core/group/cross-impact.ts
+++ b/gitnexus/src/core/group/cross-impact.ts
@@ -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): {
- 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):
+ | {
+ 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): 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;
diff --git a/gitnexus/src/core/group/service.ts b/gitnexus/src/core/group/service.ts
index 066d61d2e..3111849f3 100644
--- a/gitnexus/src/core/group/service.ts
+++ b/gitnexus/src/core/group/service.ts
@@ -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>; process_symbols?: Array> },
+ queryResult: {
+ processes?: Array>;
+ process_symbols?: Array>;
+ },
servicePrefix: string,
): { processes: Array>; process_symbols: Array> } {
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)
: {},
- 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);
diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts
index c37ebf4a9..129cda678 100644
--- a/gitnexus/src/mcp/local/local-backend.ts
+++ b/gitnexus/src/mcp/local/local-backend.ts
@@ -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;
diff --git a/gitnexus/src/mcp/resources.ts b/gitnexus/src/mcp/resources.ts
index 6fbab99ce..88e7a99cc 100644
--- a/gitnexus/src/mcp/resources.ts
+++ b/gitnexus/src/mcp/resources.ts
@@ -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');
diff --git a/gitnexus/src/mcp/tools.ts b/gitnexus/src/mcp/tools.ts
index f5fc31f47..491c24557 100644
--- a/gitnexus/src/mcp/tools.ts
+++ b/gitnexus/src/mcp/tools.ts
@@ -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,
},
diff --git a/gitnexus/test/unit/group/cross-impact.test.ts b/gitnexus/test/unit/group/cross-impact.test.ts
index 869130357..3d78ff1cf 100644
--- a/gitnexus/test/unit/group/cross-impact.test.ts
+++ b/gitnexus/test/unit/group/cross-impact.test.ts
@@ -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');
});
diff --git a/gitnexus/test/unit/group/service.test.ts b/gitnexus/test/unit/group/service.test.ts
index 56a8811a6..8c5c0ed6d 100644
--- a/gitnexus/test/unit/group/service.test.ts
+++ b/gitnexus/test/unit/group/service.test.ts
@@ -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',
diff --git a/gitnexus/test/unit/mcp/group-repo-routing.test.ts b/gitnexus/test/unit/mcp/group-repo-routing.test.ts
index 69ca0d085..ddf7d03a5 100644
--- a/gitnexus/test/unit/mcp/group-repo-routing.test.ts
+++ b/gitnexus/test/unit/mcp/group-repo-routing.test.ts
@@ -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;
- 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;
+ expect(
+ typeof callArg.repo === 'string' ? (callArg.repo as string).startsWith('@') : false,
+ ).toBe(false);
+ },
+ );
});
});