mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
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:
parent
0d00dd71dd
commit
ddf702b4af
9 changed files with 118 additions and 76 deletions
|
|
@ -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)')
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue