From 7e993ab8972386294fb96bf14a8665d0b5325397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Magyar?= Date: Sat, 29 Aug 2026 22:48:46 +0100 Subject: [PATCH 1/2] fix(group): fail ambiguous sync names and honor analyze --name (#3094) * fix(group): fail sync when a member name is ambiguous Silent first-match bound the wrong clone when --allow-duplicate-name registered two paths under one alias. Refs #3028. Co-authored-by: Cursor * fix(analyze): apply --name on the already-up-to-date path A rename should not require --force when the index is already current. Register before the same-commit branch restamp. Refs #3028. Co-authored-by: Cursor * fix(group): hint member path when impact --repo is an alias $localRepo stays the yaml key; joining on the registry alias is a non-join. List matching keys so operators can retry. Refs #3028. Co-authored-by: Cursor * fix(group): keep injected sync and alias hints consistent Workspace-deps path maps reuse the resolved handle so duplicate names cannot throw after an injected resolver. Alias hints match case-insensitively. Co-authored-by: Cursor --------- Co-authored-by: Gergo Magyar Co-authored-by: Cursor --- gitnexus/src/cli/analyze.ts | 3 + gitnexus/src/cli/group.ts | 8 +- gitnexus/src/core/group/config-parser.ts | 11 +- gitnexus/src/core/group/cross-impact.ts | 11 + gitnexus/src/core/group/service.ts | 6 +- gitnexus/src/core/group/sync.ts | 43 ++- gitnexus/src/core/run-analyze.ts | 36 +++ gitnexus/src/storage/repo-manager.ts | 28 +- .../test/integration/group/group-cli.test.ts | 45 +++ .../test/unit/group-service-not-found.test.ts | 5 +- .../test/unit/group/config-parser.test.ts | 19 ++ gitnexus/test/unit/group/cross-impact.test.ts | 98 +++++++ .../group/resolve-bridge-neighbors.test.ts | 56 ++++ .../group/service-group-sync-payload.test.ts | 1 + .../group/sync-partial-extraction.test.ts | 12 +- .../unit/group/sync-registry-identity.test.ts | 273 ++++++++++++++++++ .../unit/group/sync-unreadable-repos.test.ts | 12 +- .../group/sync-windowed-resolution.test.ts | 12 +- .../repo-manager-registry-strict-read.test.ts | 16 +- gitnexus/test/unit/repo-manager.test.ts | 8 + .../test/unit/run-analyze-fts-repair.test.ts | 46 +++ gitnexus/test/unit/run-analyze.test.ts | 193 +++++++++++++ 22 files changed, 918 insertions(+), 24 deletions(-) create mode 100644 gitnexus/test/unit/group/sync-registry-identity.test.ts diff --git a/gitnexus/src/cli/analyze.ts b/gitnexus/src/cli/analyze.ts index 54bf12d11..2719b8f40 100644 --- a/gitnexus/src/cli/analyze.ts +++ b/gitnexus/src/cli/analyze.ts @@ -1436,6 +1436,9 @@ const analyzeCommandImpl = async ( console.error = origError; bar.stop(); console.log(' Already up to date\n'); + if (runOptions.registryName) { + console.log(` Registry name: ${result.repoName}\n`); + } if (baseRefRefreshed.length > 0) { console.log( ` Updated base_ref to "${resolvedDefaultBranch}" in ${baseRefRefreshed.join(', ')}\n`, diff --git a/gitnexus/src/cli/group.ts b/gitnexus/src/cli/group.ts index 7bb7bc180..abc13fa2b 100644 --- a/gitnexus/src/cli/group.ts +++ b/gitnexus/src/cli/group.ts @@ -219,8 +219,9 @@ export function registerGroupCommands(program: Command): void { .action(async (name: string, opts: Record) => { const { getGroupDir, getDefaultGitnexusDir } = await import('../core/group/storage.js'); const { loadGroupConfig } = await import('../core/group/config-parser.js'); - const { syncGroup } = await import('../core/group/sync.js'); + const { syncGroup, formatGroupSyncAmbiguousError } = await import('../core/group/sync.js'); const { GroupSyncLockError } = await import('../core/group/group-lock.js'); + const { RegistryAmbiguousTargetError } = await import('../storage/repo-manager.js'); const groupDir = getGroupDir(getDefaultGitnexusDir(), name); const config = await loadGroupConfig(groupDir); @@ -235,6 +236,11 @@ export function registerGroupCommands(program: Command): void { exactOnly: Boolean(opts.exactOnly), }); } catch (err) { + if (err instanceof RegistryAmbiguousTargetError) { + logger.error(`⚠️ Did not sync group "${name}": ${formatGroupSyncAmbiguousError(err)}`); + process.exitCode = 1; + return; + } // A sync that could not take the group's lock did NOT run and wrote // nothing (R9 fails closed). That is an operator-actionable outcome, not // a crash, so report it as a failed command rather than letting it diff --git a/gitnexus/src/core/group/config-parser.ts b/gitnexus/src/core/group/config-parser.ts index b831c6706..373e5e5c4 100644 --- a/gitnexus/src/core/group/config-parser.ts +++ b/gitnexus/src/core/group/config-parser.ts @@ -60,7 +60,16 @@ export function parseGroupConfig(yamlContent: string): GroupConfig { throw new Error('repos is required in group.yaml (must be a mapping)'); } - const repos = raw.repos as Record; + const reposRaw = raw.repos as Record; + const repos: Record = {}; + for (const [memberPath, registryName] of Object.entries(reposRaw)) { + if (typeof registryName !== 'string' || registryName.trim() === '') { + throw new Error( + `repos["${memberPath}"] must be a non-empty registry name string, not ${typeof registryName}`, + ); + } + repos[memberPath] = registryName.trim(); + } const repoPaths = new Set(Object.keys(repos)); const rawLinks = (raw.links as unknown[]) || []; diff --git a/gitnexus/src/core/group/cross-impact.ts b/gitnexus/src/core/group/cross-impact.ts index 485b1ee8c..fd2ae0779 100644 --- a/gitnexus/src/core/group/cross-impact.ts +++ b/gitnexus/src/core/group/cross-impact.ts @@ -244,6 +244,17 @@ async function resolveGroupRepo( ): Promise { const registryName = config.repos[repoPath]; if (!registryName) { + const matchingMemberPaths = Object.entries(config.repos) + .filter(([, alias]) => alias.toLowerCase() === repoPath.toLowerCase()) + .map(([memberPath]) => memberPath); + if (matchingMemberPaths.length > 0) { + return { + error: + `Unknown repo path "${repoPath}" in this group. ` + + `That value is a registry alias for member path(s): ${matchingMemberPaths.join(', ')}. ` + + `Pass the group.yaml key to --repo, not the alias.`, + }; + } return { error: `Unknown repo path "${repoPath}" in this group.` }; } try { diff --git a/gitnexus/src/core/group/service.ts b/gitnexus/src/core/group/service.ts index af1f1385a..d0aa4882c 100644 --- a/gitnexus/src/core/group/service.ts +++ b/gitnexus/src/core/group/service.ts @@ -479,8 +479,9 @@ export class GroupService { // group tools never need it — so deferring it here keeps that closure off // MCP server startup entirely and off every non-sync group call. The CLI // already does exactly this at `cli/group.ts`'s sync command. - const { syncGroup } = await import('./sync.js'); + const { syncGroup, formatGroupSyncAmbiguousError } = await import('./sync.js'); const { GroupSyncLockError } = await import('./group-lock.js'); + const { RegistryAmbiguousTargetError } = await import('../../storage/repo-manager.js'); let result: Awaited>; try { result = await syncGroup(config, { @@ -492,6 +493,9 @@ export class GroupService { // expects. `SyncOptions.verbose` stays for the CLI, which can see them. }); } catch (err) { + if (err instanceof RegistryAmbiguousTargetError) { + return { error: formatGroupSyncAmbiguousError(err) }; + } // Fails closed (R9): this sync could not be protected against a concurrent // one, so it did not run and wrote nothing. Return it through the same // error channel a missing group uses — NEVER as a success payload of zeroes, diff --git a/gitnexus/src/core/group/sync.ts b/gitnexus/src/core/group/sync.ts index d79cd4de5..7981a3d0c 100644 --- a/gitnexus/src/core/group/sync.ts +++ b/gitnexus/src/core/group/sync.ts @@ -8,8 +8,12 @@ import { getMaxResidentRepos, } from '../lbug/pool-adapter.js'; import { + findRegistryEntryByName, + canonicalizePath, + registryPathEquals, readRegistry, readRegistryStrict, + RegistryAmbiguousTargetError, type RegistryEntry, } from '../../storage/repo-manager.js'; import type { @@ -128,9 +132,19 @@ export function stableRepoPoolId(entry: RegistryEntry, allEntries: RegistryEntry return base; } +/** Operator copy for group sync — unique `--name`, not a path in yaml. */ +export function formatGroupSyncAmbiguousError(err: RegistryAmbiguousTargetError): string { + const listing = err.matches.map((m) => ` - ${m.path}`).join('\n'); + return ( + `Multiple registered repos are named "${err.target}":\n${listing}\n` + + `Give each clone a unique registry name with \`gitnexus analyze --name\`, then re-sync. ` + + `Do not put a filesystem path in group.yaml.` + ); +} + function defaultResolveHandle(allEntries: RegistryEntry[]) { return async (registryName: string, groupPath: string): Promise => { - const e = allEntries.find((en) => en.name === registryName); + const e = findRegistryEntryByName(allEntries, registryName); if (!e) return null; const poolId = stableRepoPoolId(e, allEntries); return { @@ -277,7 +291,10 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis // Group-path → pool identity for repos that successfully initialized. Drives // windowed manifest resolution below (re-init + lease per window). Keyed by // group path because manifest links reference repos by group path. - const repoHandles = new Map(); + const repoHandles = new Map(); + // Keep resolved disk paths even when extraction fails and removes the + // corresponding handle; workspace discovery does not need a readable index. + const resolvedRepoPaths = new Map(); // Every eviction lease this sync holds. Window loops release their own leases // (bounding residency); this set is the defensive outer-finally sweep — // release disposers are idempotent, so double-release is a safe no-op. @@ -295,6 +312,11 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis registryEntries = await readRegistryStrict(); const entries = registryEntries; const resolve = opts?.resolveRepoHandle ?? defaultResolveHandle(entries); + if (!opts?.resolveRepoHandle) { + for (const regName of Object.values(config.repos)) { + findRegistryEntryByName(entries, regName); + } + } const httpEx = new HttpRouteExtractor(); const graphqlEx = new GraphqlExtractor(); const grpcEx = new GrpcExtractor(); @@ -308,6 +330,7 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis missingRepos.push(groupPath); continue; } + resolvedRepoPaths.set(groupPath, handle.repoPath); const poolId = handle.id; const lbugPath = path.join(handle.storagePath, 'lbug'); @@ -327,7 +350,7 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis // resolution no longer reuses these executors — it re-inits + leases // each repo per window (see windowed resolution below, issue #2189). // Record the pool identity so windowed resolution can re-init. - repoHandles.set(groupPath, { poolId, lbugPath }); + repoHandles.set(groupPath, { poolId, lbugPath, repoPath: handle.repoPath }); const executor: CypherExecutor = (query, params) => executeParameterized(poolId, query, params ?? {}); @@ -409,7 +432,10 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis lastCommit: m.lastCommit || '', }; } catch { - const e = entries.find((en) => en.name === regName); + const resolvedHandlePath = canonicalizePath(handle.repoPath); + const e = entries.find((en) => + registryPathEquals(canonicalizePath(en.path), resolvedHandlePath), + ); repoSnapshots[groupPath] = { indexedAt: e?.indexedAt || '', lastCommit: e?.lastCommit || '', @@ -431,6 +457,7 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis // read. The loop bounds the append by memory instead. for (const contract of repoContracts) autoContracts.push(contract); } catch (err) { + if (err instanceof RegistryAmbiguousTargetError) throw err; // This spans initLbug plus all contract extraction for the repo. The // error used to be discarded entirely, so the only trace of (say) a // storage-version mismatch was an empty contracts.json and a later @@ -465,7 +492,13 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis const repoPaths = new Map(); if (!registryEntries) registryEntries = await readRegistry(); for (const [groupPath, regName] of Object.entries(config.repos)) { - const e = registryEntries.find((en) => en.name === regName); + const resolvedPath = resolvedRepoPaths.get(groupPath); + if (resolvedPath) { + repoPaths.set(groupPath, resolvedPath); + continue; + } + if (opts?.resolveRepoHandle) continue; + const e = findRegistryEntryByName(registryEntries, regName); if (e) repoPaths.set(groupPath, e.path); } diff --git a/gitnexus/src/core/run-analyze.ts b/gitnexus/src/core/run-analyze.ts index 018efc42a..ad9841406 100644 --- a/gitnexus/src/core/run-analyze.ts +++ b/gitnexus/src/core/run-analyze.ts @@ -1308,6 +1308,13 @@ async function runFullAnalysisInner( } progress('fts', 90, 'Search indexes ready'); progress('done', 100, 'Done'); + if (options.registryName) { + await registerRepo(repoPath, existingMeta, { + name: options.registryName, + allowDuplicateName: options.allowDuplicateName, + branch: placement.branch, + }); + } return { repoName: options.registryName ?? @@ -1687,6 +1694,35 @@ async function runFullAnalysisInner( // later read on a host where it loads — which is a legitimate, common // state, and the invariant `analyzer-identity-cli.test.ts` pins. if (!dirty && !healUnregistered) { + if (options.registryName) { + await registerRepo(repoPath, existingMeta, { + name: options.registryName, + allowDuplicateName: options.allowDuplicateName, + branch: placement.branch, + }); + if (!placement.branch) { + try { + await generateAIContextFiles( + repoPath, + storagePath, + options.registryName, + existingMeta.stats ?? {}, + undefined, + { + skipAgentsMd: options.skipAgentsMd, + skipSkills: options.skipSkills, + noStats: options.noStats, + defaultBranch: options.defaultBranch, + // Fast path does not re-run PDG. Using `options.pdg` would + // strip PDG bullets from AGENTS.md on a rename-only analyze. + hasPdg: existingMeta.pdg != null, + }, + ); + } catch { + /* best-effort — never fail the fast path over a context refresh */ + } + } + } // ── #2354: restamp the workspace label on a same-commit branch flip ── // The flat slot follows the checked-out working tree; a branch switch // at the SAME commit with a clean tree changes nothing the pipeline diff --git a/gitnexus/src/storage/repo-manager.ts b/gitnexus/src/storage/repo-manager.ts index b223d5f69..3f68df80a 100644 --- a/gitnexus/src/storage/repo-manager.ts +++ b/gitnexus/src/storage/repo-manager.ts @@ -909,7 +909,10 @@ const registerRepoUnlocked = async ( // falling back to `path.resolve` when the path doesn't exist. const canonicalInput = canonicalizePath(repoPath); - const entries = await readRegistry(); + // Mutating writes must not treat an unreadable/truncated registry as empty + // (#3094): lenient `readRegistry()` returns `[]` on parse failure and would + // replace the machine-wide file with only this entry. ENOENT stays empty. + const entries = await readRegistryStrict(); const existingIdx = entries.findIndex((e) => { // Canonicalise the STORED entry too so pre-canonicalisation // registries (written by older versions, or paths passed in a @@ -1024,7 +1027,7 @@ const registerRepoUnlocked = async ( // R9): re-derive THIS run's delta against the FRESHEST snapshot so a // concurrent change to the OTHER axis (a branch upsert vs a primary refresh) // survives instead of being clobbered by a stale entry-time view. - const fresh = await readRegistry(); + const fresh = await readRegistryStrict(); const freshIdx = fresh.findIndex((e) => { const a = canonicalizePath(e.path); return registryPathEquals(a, canonicalInput); @@ -1469,6 +1472,27 @@ export const resolveRegistryEntry = (entries: RegistryEntry[], target: string): throw new RegistryNotFoundError(target, availableNames); }; +/** + * Name-only registry match (the name tier of {@link resolveRegistryEntry}, + * without path matching). Used by `group.yaml` member *values*, which are + * registry aliases, not filesystem paths. + * + * Zero matches → `undefined` (caller treats as missing). One match → that + * entry. Two or more → {@link RegistryAmbiguousTargetError}. + */ +export const findRegistryEntryByName = ( + entries: RegistryEntry[], + name: string, +): RegistryEntry | undefined => { + const targetLower = name.toLowerCase(); + const nameMatches = entries.filter((e) => e.name.toLowerCase() === targetLower); + if (nameMatches.length === 1) return nameMatches[0]; + if (nameMatches.length > 1) { + throw new RegistryAmbiguousTargetError(name, nameMatches); + } + return undefined; +}; + /** * List all registered repos from the global registry. * diff --git a/gitnexus/test/integration/group/group-cli.test.ts b/gitnexus/test/integration/group/group-cli.test.ts index a9c1840cf..45dda9e7a 100644 --- a/gitnexus/test/integration/group/group-cli.test.ts +++ b/gitnexus/test/integration/group/group-cli.test.ts @@ -55,6 +55,51 @@ describe('group CLI', () => { expect(l.stdout).toContain('acme'); }); + it('sync exits nonzero with formatted copy when a member name is ambiguous', () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-group-cli-amb-')); + try { + fs.mkdirSync(path.join(home, 'groups', 'g1'), { recursive: true }); + fs.writeFileSync( + path.join(home, 'groups', 'g1', 'group.yaml'), + `version: 1 +name: g1 +repos: + demo/api: demo-api +`, + ); + const cloneA = path.join(home, 'clone-a'); + const cloneB = path.join(home, 'clone-b'); + fs.mkdirSync(path.join(cloneA, '.gitnexus'), { recursive: true }); + fs.mkdirSync(path.join(cloneB, '.gitnexus'), { recursive: true }); + fs.writeFileSync( + path.join(home, 'registry.json'), + JSON.stringify([ + { + name: 'demo-api', + path: cloneA, + storagePath: path.join(cloneA, '.gitnexus'), + indexedAt: '2026-01-01T00:00:00.000Z', + lastCommit: 'aaa', + }, + { + name: 'demo-api', + path: cloneB, + storagePath: path.join(cloneB, '.gitnexus'), + indexedAt: '2026-01-01T00:00:00.000Z', + lastCommit: 'bbb', + }, + ]), + ); + const r = runGroupIn(home, ['sync', 'g1']); + expect(r.status).not.toBe(0); + // CLI logs JSON (pino): quotes around the group name are escaped in the byte stream. + expect(`${r.stderr}${r.stdout}`).toMatch(/Did not sync group \\"g1\\"/); + expect(`${r.stderr}${r.stdout}`).toContain('demo-api'); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } + }); + it('test_create_with_invalid_name_fails', () => { const result = runGroup(['create', '../../evil']); expect(result.status).not.toBe(0); diff --git a/gitnexus/test/unit/group-service-not-found.test.ts b/gitnexus/test/unit/group-service-not-found.test.ts index b16f9074f..f3961b9ab 100644 --- a/gitnexus/test/unit/group-service-not-found.test.ts +++ b/gitnexus/test/unit/group-service-not-found.test.ts @@ -21,7 +21,10 @@ vi.mock('../../src/core/group/storage.js', () => ({ listGroups: listGroupsMock, })); -vi.mock('../../src/core/group/sync.js', () => ({ syncGroup: syncGroupMock })); +vi.mock('../../src/core/group/sync.js', () => ({ + syncGroup: syncGroupMock, + formatGroupSyncAmbiguousError: (err: Error) => err.message, +})); vi.mock('../../src/core/git-staleness.js', () => ({ checkStaleness: vi.fn() })); describe('GroupService — missing group error handling', () => { diff --git a/gitnexus/test/unit/group/config-parser.test.ts b/gitnexus/test/unit/group/config-parser.test.ts index 9644b304b..a17ebe148 100644 --- a/gitnexus/test/unit/group/config-parser.test.ts +++ b/gitnexus/test/unit/group/config-parser.test.ts @@ -245,6 +245,25 @@ links: expect(() => parseGroupConfig('version: 1\nname: test')).toThrow(/repos.*required/i); }); + it('throws when a repos value is not a string (YAML number/boolean)', () => { + expect(() => + parseGroupConfig(`version: 1 +name: test +repos: + app: 12 +`), + ).toThrow(/non-empty registry name string/); + }); + + it('trims padded registry aliases so they match the registry name', () => { + const config = parseGroupConfig(`version: 1 +name: test +repos: + app: " my-app " +`); + expect(config.repos.app).toBe('my-app'); + }); + it('allows empty repos object (fresh group before first add)', () => { const yaml = `version: 1 name: new-group diff --git a/gitnexus/test/unit/group/cross-impact.test.ts b/gitnexus/test/unit/group/cross-impact.test.ts index bceaa9195..9d718a0bf 100644 --- a/gitnexus/test/unit/group/cross-impact.test.ts +++ b/gitnexus/test/unit/group/cross-impact.test.ts @@ -444,4 +444,102 @@ describe('cross-impact', () => { cleanup(); } }); + + it('hints the yaml member path when --repo is the registry alias', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-ci-alias-')); + const groupDir = path.join(tmpDir, 'groups', 'g1'); + fs.mkdirSync(groupDir, { recursive: true }); + fs.writeFileSync( + path.join(groupDir, 'group.yaml'), + `version: 1 +name: g1 +repos: + demo/api: demo-api + demo/web: demo-web +`, + ); + vi.stubEnv('GITNEXUS_HOME', tmpDir); + try { + const port: GroupToolPort = { + resolveRepo: vi.fn(), + impact: vi.fn(), + query: vi.fn(), + impactByUid: vi.fn(), + context: vi.fn(), + }; + const r = await runGroupImpact( + { port, gitnexusDir: tmpDir }, + { + name: 'g1', + repo: 'demo-api', + target: 'Sym', + direction: 'upstream', + }, + ); + expect('error' in r).toBe(true); + if ('error' in r) { + expect(r.error).toContain('demo/api'); + expect(r.error).toMatch(/registry alias/i); + expect(r.error).not.toContain('demo/web'); + } + const mixedCase = await runGroupImpact( + { port, gitnexusDir: tmpDir }, + { + name: 'g1', + repo: 'Demo-API', + target: 'Sym', + direction: 'upstream', + }, + ); + expect('error' in mixedCase).toBe(true); + if ('error' in mixedCase) { + expect(mixedCase.error).toContain('demo/api'); + } + } finally { + vi.unstubAllEnvs(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('lists every member path that shares the same registry alias', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-ci-alias-dup-')); + const groupDir = path.join(tmpDir, 'groups', 'g1'); + fs.mkdirSync(groupDir, { recursive: true }); + fs.writeFileSync( + path.join(groupDir, 'group.yaml'), + `version: 1 +name: g1 +repos: + demo/api: shared + demo/other: shared +`, + ); + vi.stubEnv('GITNEXUS_HOME', tmpDir); + try { + const port: GroupToolPort = { + resolveRepo: vi.fn(), + impact: vi.fn(), + query: vi.fn(), + impactByUid: vi.fn(), + context: vi.fn(), + }; + const r = await runGroupImpact( + { port, gitnexusDir: tmpDir }, + { + name: 'g1', + repo: 'shared', + target: 'Sym', + direction: 'upstream', + }, + ); + expect('error' in r).toBe(true); + if ('error' in r) { + expect(r.error).toContain('demo/api'); + expect(r.error).toContain('demo/other'); + } + } finally { + vi.unstubAllEnvs(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); }); diff --git a/gitnexus/test/unit/group/resolve-bridge-neighbors.test.ts b/gitnexus/test/unit/group/resolve-bridge-neighbors.test.ts index 73ffa5b7e..13876c897 100644 --- a/gitnexus/test/unit/group/resolve-bridge-neighbors.test.ts +++ b/gitnexus/test/unit/group/resolve-bridge-neighbors.test.ts @@ -127,4 +127,60 @@ describe('resolveBridgeNeighbors', () => { expect(rows).toEqual([]); await closeBridgeDb(handle!); }); + + itLbugReopen( + 'registry alias as localRepo does not join contracts stamped with the member path', + async () => { + const consumer = makeContract({ + repo: 'demo/api', + role: 'consumer', + symbolUid: 'consumer-uid', + symbolRef: { filePath: 'src/api.ts', name: 'fetchUsers' }, + symbolName: 'fetchUsers', + contractId: 'http::GET::/api/users', + confidence: 0.5, + }); + const provider = makeContract({ + repo: 'demo/api', + role: 'provider', + symbolUid: 'provider-uid', + symbolRef: { filePath: 'src/routes.ts', name: 'getUsers' }, + symbolName: 'getUsers', + contractId: 'http::GET::/api/users', + confidence: 0.9, + }); + const link: CrossLink = { + from: { repo: 'web', symbolUid: 'web-uid', symbolRef: consumer.symbolRef }, + to: { repo: 'demo/api', symbolUid: 'provider-uid', symbolRef: provider.symbolRef }, + type: 'http', + contractId: 'http::GET::/api/users', + matchType: 'manifest', + confidence: 0.9, + }; + await writeBridge(tmpDir, { + contracts: [{ ...consumer, repo: 'web' }, provider], + crossLinks: [link], + repoSnapshots: {}, + missingRepos: [], + }); + const handle = await openBridgeDbReadOnly(tmpDir); + const aliasMiss = await resolveBridgeNeighbors(handle!, { + localRepo: 'demo-api', + uids: ['provider-uid'], + direction: 'upstream', + }); + expect(aliasMiss).toEqual([]); + const pathHit = await resolveBridgeNeighbors(handle!, { + localRepo: 'demo/api', + uids: ['provider-uid'], + direction: 'upstream', + }); + expect(pathHit).toHaveLength(1); + expect(pathHit[0]).toMatchObject({ + neighborRepo: 'web', + matchType: 'manifest', + }); + await closeBridgeDb(handle!); + }, + ); }); diff --git a/gitnexus/test/unit/group/service-group-sync-payload.test.ts b/gitnexus/test/unit/group/service-group-sync-payload.test.ts index ddfa32d71..ea2792b7d 100644 --- a/gitnexus/test/unit/group/service-group-sync-payload.test.ts +++ b/gitnexus/test/unit/group/service-group-sync-payload.test.ts @@ -50,6 +50,7 @@ const syncGroupMock = vi.fn<() => Promise>(); vi.mock('../../../src/core/group/sync.js', () => ({ syncGroup: (...args: unknown[]) => syncGroupMock(...(args as [])), + formatGroupSyncAmbiguousError: (err: Error) => err.message, })); const { GroupService } = await import('../../../src/core/group/service.js'); diff --git a/gitnexus/test/unit/group/sync-partial-extraction.test.ts b/gitnexus/test/unit/group/sync-partial-extraction.test.ts index 06b3bf450..0e7d41228 100644 --- a/gitnexus/test/unit/group/sync-partial-extraction.test.ts +++ b/gitnexus/test/unit/group/sync-partial-extraction.test.ts @@ -54,10 +54,14 @@ vi.mock('../../../src/core/lbug/pool-adapter.js', () => ({ getMaxResidentRepos: vi.fn(() => 5), })); -vi.mock('../../../src/storage/repo-manager.js', () => ({ - readRegistry: vi.fn(async () => []), - readRegistryStrict: vi.fn(async () => []), -})); +vi.mock('../../../src/storage/repo-manager.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readRegistry: vi.fn(async () => []), + readRegistryStrict: vi.fn(async () => []), + }; +}); vi.mock('../../../src/core/group/extractors/http-route-extractor.js', () => ({ HttpRouteExtractor: class { diff --git a/gitnexus/test/unit/group/sync-registry-identity.test.ts b/gitnexus/test/unit/group/sync-registry-identity.test.ts new file mode 100644 index 000000000..309a2c0fd --- /dev/null +++ b/gitnexus/test/unit/group/sync-registry-identity.test.ts @@ -0,0 +1,273 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import fs from 'node:fs/promises'; +import { mkdirSync } from 'node:fs'; +import path from 'node:path'; +import { syncGroup } from '../../../src/core/group/sync.js'; +import { RegistryAmbiguousTargetError } from '../../../src/storage/repo-manager.js'; +import { createTempDir } from '../../helpers/test-db.js'; +import type { GroupConfig } from '../../../src/core/group/types.js'; +import { GroupService } from '../../../src/core/group/service.js'; +import type { GroupToolPort } from '../../../src/core/group/service.js'; + +const initLbugMock = vi.fn(async () => {}); + +vi.mock('../../../src/core/lbug/pool-adapter.js', () => ({ + initLbug: (...args: unknown[]) => initLbugMock(...args), + executeParameterized: vi.fn(async () => []), + pinRepo: vi.fn(() => () => {}), + getMaxResidentRepos: vi.fn(() => 5), +})); + +const makeConfig = (repos: Record, extra?: Partial): GroupConfig => ({ + version: 1, + name: 'test', + description: '', + repos, + links: [], + packages: {}, + detect: { + http: false, + graphql: false, + grpc: false, + thrift: false, + topics: false, + includes: false, + workspace_deps: false, + }, + matching: {}, + ...extra, +}); + +const row = ( + tmpHome: string, + name: string, + clone: string, +): { + name: string; + path: string; + storagePath: string; + indexedAt: string; + lastCommit: string; +} => { + mkdirSync(path.join(tmpHome, 'repos', clone), { recursive: true }); + return { + name, + path: path.join(tmpHome, 'repos', clone), + storagePath: path.join(tmpHome, 'repos', clone, '.gitnexus'), + indexedAt: '2026-01-01T00:00:00.000Z', + lastCommit: 'abc123', + }; +}; + +describe('syncGroup registry name identity', () => { + let tmpHome: Awaited>; + let savedGitnexusHome: string | undefined; + let registryPath: string; + + beforeEach(async () => { + initLbugMock.mockReset(); + initLbugMock.mockResolvedValue(undefined); + tmpHome = await createTempDir('gitnexus-sync-registry-id-'); + savedGitnexusHome = process.env.GITNEXUS_HOME; + process.env.GITNEXUS_HOME = tmpHome.dbPath; + registryPath = path.join(tmpHome.dbPath, 'registry.json'); + }); + + afterEach(async () => { + if (savedGitnexusHome === undefined) delete process.env.GITNEXUS_HOME; + else process.env.GITNEXUS_HOME = savedGitnexusHome; + await tmpHome.cleanup(); + }); + + it('throws RegistryAmbiguousTargetError and does not rewrite group dir files', async () => { + const a = row(tmpHome.dbPath, 'demo-api', 'clone-a'); + const b = row(tmpHome.dbPath, 'demo-api', 'clone-b'); + await fs.writeFile(registryPath, JSON.stringify([a, b])); + + const groupDir = path.join(tmpHome.dbPath, 'groups', 'g'); + await fs.mkdir(groupDir, { recursive: true }); + const contractsPath = path.join(groupDir, 'contracts.json'); + const prior = '{"contracts":[],"crossLinks":[],"marker":"keep"}\n'; + await fs.writeFile(contractsPath, prior); + + await expect(syncGroup(makeConfig({ 'demo/api': 'demo-api' }), { groupDir })).rejects.toSatisfy( + (err: unknown) => { + expect(err).toBeInstanceOf(RegistryAmbiguousTargetError); + const amb = err as RegistryAmbiguousTargetError; + expect(amb.matches).toHaveLength(2); + expect(amb.matches.map((m) => m.path).sort()).toEqual([a.path, b.path].sort()); + return true; + }, + ); + + expect(await fs.readFile(contractsPath, 'utf-8')).toBe(prior); + await expect(fs.access(path.join(groupDir, 'bridge.lbug'))).rejects.toThrow(); + }); + + it('records an unknown yaml value as missing and still extracts other members', async () => { + const known = row(tmpHome.dbPath, 'backend-repo', 'backend'); + await fs.writeFile(registryPath, JSON.stringify([known])); + + const result = await syncGroup( + makeConfig({ 'app/backend': 'backend-repo', 'app/ghost': 'ghost' }), + { skipWrite: true }, + ); + + expect(result.missingRepos).toEqual(['app/ghost']); + expect(result.unreadableRepos).toEqual([]); + expect(result.repoSnapshots['app/backend']).toEqual({ + indexedAt: known.indexedAt, + lastCommit: known.lastCommit, + }); + }); + + it('treats mixed missing and ambiguous names as a terminal ambiguity with no write', async () => { + const a = row(tmpHome.dbPath, 'demo-api', 'clone-a'); + const b = row(tmpHome.dbPath, 'demo-api', 'clone-b'); + await fs.writeFile(registryPath, JSON.stringify([a, b])); + + const groupDir = path.join(tmpHome.dbPath, 'groups', 'g'); + await fs.mkdir(groupDir, { recursive: true }); + const contractsPath = path.join(groupDir, 'contracts.json'); + await fs.writeFile(contractsPath, '{"keep":true}'); + + await expect( + syncGroup(makeConfig({ 'demo/api': 'demo-api', 'app/ghost': 'ghost' }), { groupDir }), + ).rejects.toBeInstanceOf(RegistryAmbiguousTargetError); + + expect(await fs.readFile(contractsPath, 'utf-8')).toBe('{"keep":true}'); + }); + + it('injected resolveRepoHandle still bypasses default name matching', async () => { + const a = row(tmpHome.dbPath, 'demo-api', 'clone-a'); + const b = row(tmpHome.dbPath, 'demo-api', 'clone-b'); + await fs.writeFile(registryPath, JSON.stringify([a, b])); + + const result = await syncGroup(makeConfig({ 'demo/api': 'demo-api' }), { + skipWrite: true, + resolveRepoHandle: async (_name, groupPath) => ({ + id: 'injected', + path: groupPath, + repoPath: a.path, + storagePath: a.storagePath, + }), + }); + + expect(result.missingRepos).toEqual([]); + expect(result.unreadableRepos).toEqual([]); + }); + + it('does not treat a filesystem path yaml value as a registry hit', async () => { + const known = row(tmpHome.dbPath, 'backend-repo', 'backend'); + await fs.writeFile(registryPath, JSON.stringify([known])); + + const result = await syncGroup(makeConfig({ 'app/backend': known.path }), { skipWrite: true }); + + expect(result.missingRepos).toEqual(['app/backend']); + expect(result.repoSnapshots['app/backend']).toBeUndefined(); + }); + + it('injected resolveRepoHandle plus workspace_deps does not throw on duplicate names', async () => { + const a = row(tmpHome.dbPath, 'demo-api', 'clone-a'); + const b = row(tmpHome.dbPath, 'demo-api', 'clone-b'); + await fs.writeFile(registryPath, JSON.stringify([a, b])); + + const result = await syncGroup( + makeConfig( + { 'demo/api': 'demo-api' }, + { + detect: { + http: false, + graphql: false, + grpc: false, + thrift: false, + topics: false, + includes: false, + workspace_deps: true, + }, + }, + ), + { + skipWrite: true, + resolveRepoHandle: async (_name, groupPath) => ({ + id: 'injected', + path: groupPath, + repoPath: a.path, + storagePath: a.storagePath, + }), + }, + ); + + expect(result.missingRepos).toEqual([]); + }); + + it('injected resolveRepoHandle plus workspace_deps still bypasses name lookup after extraction failure', async () => { + const a = row(tmpHome.dbPath, 'demo-api', 'clone-a'); + const b = row(tmpHome.dbPath, 'demo-api', 'clone-b'); + await fs.writeFile(registryPath, JSON.stringify([a, b])); + initLbugMock.mockRejectedValueOnce(new Error('init failed')); + + const result = await syncGroup( + makeConfig( + { 'demo/api': 'demo-api' }, + { + detect: { + http: false, + graphql: false, + grpc: false, + thrift: false, + topics: false, + includes: false, + workspace_deps: true, + }, + }, + ), + { + skipWrite: true, + resolveRepoHandle: async (_name, groupPath) => ({ + id: 'injected', + path: groupPath, + repoPath: a.path, + storagePath: a.storagePath, + }), + }, + ); + + expect(result.missingRepos).toEqual([]); + expect(result.unreadableRepos).toEqual(['demo/api']); + }); + + it('MCP groupSync returns { error } for an ambiguous registry name', async () => { + const a = row(tmpHome.dbPath, 'demo-api', 'clone-a'); + const b = row(tmpHome.dbPath, 'demo-api', 'clone-b'); + await fs.writeFile(registryPath, JSON.stringify([a, b])); + + const groupDir = path.join(tmpHome.dbPath, 'groups', 'g1'); + await fs.mkdir(groupDir, { recursive: true }); + await fs.writeFile( + path.join(groupDir, 'group.yaml'), + `version: 1 +name: g1 +repos: + demo/api: demo-api +`, + ); + + const port: GroupToolPort = { + resolveRepo: vi.fn(), + impact: vi.fn(), + query: vi.fn(), + impactByUid: vi.fn(), + context: vi.fn(), + }; + const svc = new GroupService(port); + const payload = (await svc.groupSync({ name: 'g1' })) as { error?: string }; + + expect(payload.error).toBeDefined(); + expect(payload.error).toContain('demo-api'); + expect(payload.error).toContain(a.path); + expect(payload.error).toContain(b.path); + expect(payload.error).toMatch(/unique registry name/i); + expect(payload.error).not.toMatch(/Pass the absolute path/); + }); +}); diff --git a/gitnexus/test/unit/group/sync-unreadable-repos.test.ts b/gitnexus/test/unit/group/sync-unreadable-repos.test.ts index 12a85439c..5f565d443 100644 --- a/gitnexus/test/unit/group/sync-unreadable-repos.test.ts +++ b/gitnexus/test/unit/group/sync-unreadable-repos.test.ts @@ -66,10 +66,14 @@ vi.mock('../../../src/core/lbug/pool-adapter.js', () => ({ getMaxResidentRepos: vi.fn(() => 5), })); -vi.mock('../../../src/storage/repo-manager.js', () => ({ - readRegistry: (...args: unknown[]) => readRegistryLenientMock(...args), - readRegistryStrict: (...args: unknown[]) => readRegistryStrictMock(...args), -})); +vi.mock('../../../src/storage/repo-manager.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readRegistry: (...args: unknown[]) => readRegistryLenientMock(...args), + readRegistryStrict: (...args: unknown[]) => readRegistryStrictMock(...args), + }; +}); /** * Armed by the bridge-write-failure suite at the bottom of this file, `null` diff --git a/gitnexus/test/unit/group/sync-windowed-resolution.test.ts b/gitnexus/test/unit/group/sync-windowed-resolution.test.ts index aaf361b87..d96eeec26 100644 --- a/gitnexus/test/unit/group/sync-windowed-resolution.test.ts +++ b/gitnexus/test/unit/group/sync-windowed-resolution.test.ts @@ -156,10 +156,14 @@ vi.mock('../../../src/core/lbug/sidecar-recovery.js', () => ({ // The registry read happens in syncGroup's else branch; resolveRepoHandle is // supplied, so an empty registry is fine (only the meta.json fallback reads it). -vi.mock('../../../src/storage/repo-manager.js', () => ({ - readRegistry: vi.fn().mockResolvedValue([]), - readRegistryStrict: vi.fn().mockResolvedValue([]), -})); +vi.mock('../../../src/storage/repo-manager.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readRegistry: vi.fn().mockResolvedValue([]), + readRegistryStrict: vi.fn().mockResolvedValue([]), + }; +}); const { syncGroup } = await import('../../../src/core/group/sync.js'); const { closeLbug, getMaxResidentRepos } = await import('../../../src/core/lbug/pool-adapter.js'); diff --git a/gitnexus/test/unit/repo-manager-registry-strict-read.test.ts b/gitnexus/test/unit/repo-manager-registry-strict-read.test.ts index 76d79e295..23241517d 100644 --- a/gitnexus/test/unit/repo-manager-registry-strict-read.test.ts +++ b/gitnexus/test/unit/repo-manager-registry-strict-read.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import fs from 'node:fs/promises'; import path from 'node:path'; import { inspect } from 'node:util'; -import { readRegistry, readRegistryStrict } from '../../src/storage/repo-manager.js'; +import { readRegistry, readRegistryStrict, registerRepo } from '../../src/storage/repo-manager.js'; import { _captureLogger, type LoggerCapture } from '../../src/core/logger.js'; import { createTempDir } from '../helpers/test-db.js'; import { syncGroup } from '../../src/core/group/sync.js'; @@ -116,6 +116,20 @@ describe('readRegistryStrict', () => { await expect(readRegistryStrict()).rejects.toThrow(); }); + it('registerRepo refuses to overwrite a truncated registry with a single entry', async () => { + const prior = '{"truncated": '; + await fs.writeFile(registryPath, prior); + await expect( + registerRepo('/repos/one', { + repoPath: '/repos/one', + lastCommit: 'abc', + indexedAt: '2026-01-01T00:00:00.000Z', + stats: {}, + }), + ).rejects.toThrow('registry is corrupt'); + expect(await fs.readFile(registryPath, 'utf-8')).toBe(prior); + }); + it('throws when a row is missing the fields the resolver needs', async () => { // `[{}]` is a JSON array, so an array-shape check alone waved it through. // Every configured repo then failed to resolve and landed in missingRepos; diff --git a/gitnexus/test/unit/repo-manager.test.ts b/gitnexus/test/unit/repo-manager.test.ts index 2080e2f45..468096a61 100644 --- a/gitnexus/test/unit/repo-manager.test.ts +++ b/gitnexus/test/unit/repo-manager.test.ts @@ -28,6 +28,7 @@ import { adoptFlatBranchLabel, listRegisteredRepos, resolveRegistryEntry, + findRegistryEntryByName, canonicalizePath, registryPathEquals, cloneDirBelongsToEntry, @@ -1535,6 +1536,13 @@ describe('resolveRegistryEntry (#664)', () => { expect(resolveRegistryEntry(entries, 'Website')).toBe(entries[2]); }); + it('findRegistryEntryByName is name-only: a filesystem path is a miss, not a path-tier hit', () => { + expect(findRegistryEntryByName(entries, pathA)).toBeUndefined(); + expect(findRegistryEntryByName(entries, 'website')).toBe(entries[2]); + expect(findRegistryEntryByName(entries, 'WEBSITE')).toBe(entries[2]); + expect(() => findRegistryEntryByName(entries, 'app')).toThrow(RegistryAmbiguousTargetError); + }); + it('path match is case-insensitive on Windows only', () => { if (process.platform !== 'win32') { // On POSIX, a differently-cased path must NOT match. Verify by diff --git a/gitnexus/test/unit/run-analyze-fts-repair.test.ts b/gitnexus/test/unit/run-analyze-fts-repair.test.ts index 24d03844f..091c7f915 100644 --- a/gitnexus/test/unit/run-analyze-fts-repair.test.ts +++ b/gitnexus/test/unit/run-analyze-fts-repair.test.ts @@ -359,6 +359,52 @@ describe('runFullAnalysis FTS repair and verification failure paths', () => { } }); + it('--repair-fts applies analyze --name without a full re-index', async () => { + vi.doMock('../../src/core/lbug/lbug-adapter.js', () => mockRepairSuccessLbugAdapter()); + vi.doMock('../../src/core/search/fts-indexes.js', () => ({ + initialiseSearchFTSStemmer: vi.fn(() => 'porter'), + createSearchFTSIndexes: vi.fn(async () => []), + verifySearchFTSIndexes: vi.fn(async () => []), + })); + vi.doMock('../../src/storage/repo-manager.js', async (importActual) => ({ + ...(await importActual()), + ensureGitNexusIgnored: vi.fn(async () => undefined), + })); + + const tmpRepo = await createTempDir('gitnexus-run-analyze-repair-name-'); + const tmpHome = await createTempDir('gitnexus-run-analyze-repair-name-home-'); + const savedHome = process.env.GITNEXUS_HOME; + process.env.GITNEXUS_HOME = tmpHome.dbPath; + try { + const { storagePath, lbugPath } = getStoragePaths(tmpRepo.dbPath); + await fs.mkdir(storagePath, { recursive: true }); + const seeded: RepoMeta = { + repoPath: tmpRepo.dbPath, + lastCommit: 'abc123', + indexedAt: new Date().toISOString(), + stats: { files: 1, nodes: 1, edges: 1 }, + }; + await saveMeta(storagePath, seeded); + const { registerRepo, readRegistry } = await import('../../src/storage/repo-manager.js'); + await registerRepo(tmpRepo.dbPath, seeded, { name: 'old' }); + await createPlaceholderGraphStore(lbugPath); + + const { runFullAnalysis } = await import('../../src/core/run-analyze.js'); + const result = await runFullAnalysis( + tmpRepo.dbPath, + { repairFts: true, registryName: 'new' }, + { onProgress: () => {} }, + ); + expect(result.ftsRepairedOnly).toBe(true); + expect((await readRegistry())[0].name).toBe('new'); + } finally { + if (savedHome === undefined) delete process.env.GITNEXUS_HOME; + else process.env.GITNEXUS_HOME = savedHome; + await tmpHome.cleanup(); + await tmpRepo.cleanup(); + } + }); + it('--repair-fts backfills a full capabilities object when the existing meta predates the field entirely (#2767)', async () => { vi.doMock('../../src/core/lbug/lbug-adapter.js', () => mockRepairSuccessLbugAdapter()); vi.doMock('../../src/core/search/fts-indexes.js', () => ({ diff --git a/gitnexus/test/unit/run-analyze.test.ts b/gitnexus/test/unit/run-analyze.test.ts index aef31907c..ea4525b61 100644 --- a/gitnexus/test/unit/run-analyze.test.ts +++ b/gitnexus/test/unit/run-analyze.test.ts @@ -14,6 +14,8 @@ import { loadMeta, registerRepo, saveMeta, + readRegistry, + RegistryNameCollisionError, type RepoMeta, } from '../../src/storage/repo-manager.js'; import { SCHEMA_FINGERPRINT } from '../../src/core/lbug/schema.js'; @@ -90,6 +92,197 @@ describe('run-analyze module', () => { } }); + it('applies analyze --name on the already-up-to-date path without --force', async () => { + const tmpRepo = await createTempDir('gitnexus-run-analyze-fast-name-'); + const tmpHome = await createTempDir('gitnexus-run-analyze-fast-name-home-'); + const savedHome = process.env.GITNEXUS_HOME; + process.env.GITNEXUS_HOME = tmpHome.dbPath; + try { + execSync('git init', { cwd: tmpRepo.dbPath, stdio: 'pipe' }); + execSync('git -c user.name=t -c user.email=t@t commit --allow-empty -m init', { + cwd: tmpRepo.dbPath, + stdio: 'pipe', + }); + const currentCommit = execSync('git rev-parse HEAD', { + cwd: tmpRepo.dbPath, + encoding: 'utf-8', + }).trim(); + const { storagePath } = getStoragePaths(tmpRepo.dbPath); + const meta: RepoMeta = { + repoPath: tmpRepo.dbPath, + lastCommit: currentCommit, + indexedAt: new Date().toISOString(), + schemaFingerprint: SCHEMA_FINGERPRINT, + analysisFeatures: CURRENT_ANALYSIS_FEATURES, + runnerIdentity: currentRunnerIdentity(), + }; + await saveMeta(storagePath, meta); + await registerRepo(tmpRepo.dbPath, meta, { name: 'old' }); + + const { runFullAnalysis } = await import('../../src/core/run-analyze.js'); + const result = await runFullAnalysis( + tmpRepo.dbPath, + { registryName: 'new' }, + { onProgress: () => {} }, + ); + + expect(result.alreadyUpToDate).toBe(true); + expect(result.repoName).toBe('new'); + const entries = await readRegistry(); + expect(entries).toHaveLength(1); + expect(entries[0].name).toBe('new'); + const agents = await fs.readFile(path.join(tmpRepo.dbPath, 'AGENTS.md'), 'utf-8'); + expect(agents).toContain('**new**'); + } finally { + if (savedHome === undefined) delete process.env.GITNEXUS_HOME; + else process.env.GITNEXUS_HOME = savedHome; + await tmpHome.cleanup(); + await tmpRepo.cleanup(); + } + }); + + it('repeating the same --name on the fast path is a no-op, not an error', async () => { + const tmpRepo = await createTempDir('gitnexus-run-analyze-fast-name-repeat-'); + const tmpHome = await createTempDir('gitnexus-run-analyze-fast-name-repeat-home-'); + const savedHome = process.env.GITNEXUS_HOME; + process.env.GITNEXUS_HOME = tmpHome.dbPath; + try { + execSync('git init', { cwd: tmpRepo.dbPath, stdio: 'pipe' }); + execSync('git -c user.name=t -c user.email=t@t commit --allow-empty -m init', { + cwd: tmpRepo.dbPath, + stdio: 'pipe', + }); + const currentCommit = execSync('git rev-parse HEAD', { + cwd: tmpRepo.dbPath, + encoding: 'utf-8', + }).trim(); + const { storagePath } = getStoragePaths(tmpRepo.dbPath); + const meta: RepoMeta = { + repoPath: tmpRepo.dbPath, + lastCommit: currentCommit, + indexedAt: new Date().toISOString(), + schemaFingerprint: SCHEMA_FINGERPRINT, + analysisFeatures: CURRENT_ANALYSIS_FEATURES, + runnerIdentity: currentRunnerIdentity(), + }; + await saveMeta(storagePath, meta); + await registerRepo(tmpRepo.dbPath, meta, { name: 'kept' }); + + const { runFullAnalysis } = await import('../../src/core/run-analyze.js'); + const result = await runFullAnalysis( + tmpRepo.dbPath, + { registryName: 'kept' }, + { onProgress: () => {} }, + ); + + expect(result.alreadyUpToDate).toBe(true); + expect((await readRegistry())[0].name).toBe('kept'); + } finally { + if (savedHome === undefined) delete process.env.GITNEXUS_HOME; + else process.env.GITNEXUS_HOME = savedHome; + await tmpHome.cleanup(); + await tmpRepo.cleanup(); + } + }); + + it('fast-path --name still collides when another path already owns the alias', async () => { + const tmpA = await createTempDir('gitnexus-run-analyze-fast-name-col-a-'); + const tmpB = await createTempDir('gitnexus-run-analyze-fast-name-col-b-'); + const tmpHome = await createTempDir('gitnexus-run-analyze-fast-name-col-home-'); + const savedHome = process.env.GITNEXUS_HOME; + process.env.GITNEXUS_HOME = tmpHome.dbPath; + try { + for (const tmp of [tmpA, tmpB]) { + execSync('git init', { cwd: tmp.dbPath, stdio: 'pipe' }); + execSync('git -c user.name=t -c user.email=t@t commit --allow-empty -m init', { + cwd: tmp.dbPath, + stdio: 'pipe', + }); + } + const commitB = execSync('git rev-parse HEAD', { + cwd: tmpB.dbPath, + encoding: 'utf-8', + }).trim(); + const metaA: RepoMeta = { + repoPath: tmpA.dbPath, + lastCommit: 'aaaa', + indexedAt: new Date().toISOString(), + schemaFingerprint: SCHEMA_FINGERPRINT, + analysisFeatures: CURRENT_ANALYSIS_FEATURES, + runnerIdentity: currentRunnerIdentity(), + }; + await registerRepo(tmpA.dbPath, metaA, { name: 'new' }); + + const { storagePath } = getStoragePaths(tmpB.dbPath); + const metaB: RepoMeta = { + repoPath: tmpB.dbPath, + lastCommit: commitB, + indexedAt: new Date().toISOString(), + schemaFingerprint: SCHEMA_FINGERPRINT, + analysisFeatures: CURRENT_ANALYSIS_FEATURES, + runnerIdentity: currentRunnerIdentity(), + }; + await saveMeta(storagePath, metaB); + await registerRepo(tmpB.dbPath, metaB, { name: 'old' }); + + const { runFullAnalysis } = await import('../../src/core/run-analyze.js'); + await expect( + runFullAnalysis(tmpB.dbPath, { registryName: 'new' }, { onProgress: () => {} }), + ).rejects.toBeInstanceOf(RegistryNameCollisionError); + } finally { + if (savedHome === undefined) delete process.env.GITNEXUS_HOME; + else process.env.GITNEXUS_HOME = savedHome; + await tmpHome.cleanup(); + await tmpA.cleanup(); + await tmpB.cleanup(); + } + }); + + it('plain fast path does not call registerRepo when --name is absent', async () => { + const tmpRepo = await createTempDir('gitnexus-run-analyze-fast-no-name-'); + const tmpHome = await createTempDir('gitnexus-run-analyze-fast-no-name-home-'); + const savedHome = process.env.GITNEXUS_HOME; + process.env.GITNEXUS_HOME = tmpHome.dbPath; + try { + execSync('git init', { cwd: tmpRepo.dbPath, stdio: 'pipe' }); + execSync('git -c user.name=t -c user.email=t@t commit --allow-empty -m init', { + cwd: tmpRepo.dbPath, + stdio: 'pipe', + }); + const currentCommit = execSync('git rev-parse HEAD', { + cwd: tmpRepo.dbPath, + encoding: 'utf-8', + }).trim(); + const { storagePath } = getStoragePaths(tmpRepo.dbPath); + const meta: RepoMeta = { + repoPath: tmpRepo.dbPath, + lastCommit: currentCommit, + indexedAt: new Date().toISOString(), + schemaFingerprint: SCHEMA_FINGERPRINT, + analysisFeatures: CURRENT_ANALYSIS_FEATURES, + runnerIdentity: currentRunnerIdentity(), + }; + await saveMeta(storagePath, meta); + await registerRepo(tmpRepo.dbPath, meta, { name: 'original' }); + + const registerSpy = vi.spyOn( + await import('../../src/storage/repo-manager.js'), + 'registerRepo', + ); + const { runFullAnalysis } = await import('../../src/core/run-analyze.js'); + const result = await runFullAnalysis(tmpRepo.dbPath, {}, { onProgress: () => {} }); + expect(result.alreadyUpToDate).toBe(true); + expect(registerSpy).not.toHaveBeenCalled(); + expect((await readRegistry())[0].name).toBe('original'); + registerSpy.mockRestore(); + } finally { + if (savedHome === undefined) delete process.env.GITNEXUS_HOME; + else process.env.GITNEXUS_HOME = savedHome; + await tmpHome.cleanup(); + await tmpRepo.cleanup(); + } + }); + it('resumes a matching embedding checkpoint instead of taking the clean fast path', async () => { const tmpRepo = await createTempDir('gitnexus-run-analyze-embedding-checkpoint-'); const tmpHome = await createTempDir('gitnexus-run-analyze-embedding-checkpoint-home-'); From 4e51213c82249d5c235f8535d8292bd56de8c01a Mon Sep 17 00:00:00 2001 From: svector Date: Sun, 30 Aug 2026 09:03:05 +0100 Subject: [PATCH 2/2] fix(deps): bump transitive packages to patch disclosed CVEs (#3095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(deps): bump transitive packages to patch disclosed CVEs Automated dependency bumps addressing already-disclosed advisories in gitnexus and gitnexus-web lockfiles. - undici 6.24.0 → 6.28.0 (@vercel/node override) — GHSA-vxpw-j846-p89q / CVE-2026-12151 - undici 7.25.0 → 7.29.0 (jsdom override) — GHSA-4cwx-7wf7-3272, GHSA-hm92-r4w5-c3mj, GHSA-vmh5-mc38-953g, GHSA-vxpw-j846-p89q - js-yaml 4.1.1 → 4.3.1 — GHSA-52cp-r559-cp3m / CVE-2026-59869, GHSA-5p4m-2wfm-xmqj - nanoid 3.3.16 → 3.3.18 — GHSA-2v37-7h3g-55p8 / CVE-2026-67213 - tar 7.5.20 → 7.5.22 — GHSA-r292-9mhp-454m / CVE-2026-73566 - protobufjs 7.6.4 → 7.6.6 (gitnexus override) — GHSA-j3f2-48v5-ccww / CVE-2026-59877 No application code changes. npm audit and osv-scanner report 0 remaining vulnerabilities on these two lockfiles after the bump. * fix(deps): prefer root bump and lockfile refresh --------- Co-authored-by: Svector-anu Co-authored-by: Gergő Magyar --- gitnexus-web/package-lock.json | 186 ++++----------------------------- gitnexus-web/package.json | 4 +- gitnexus/package-lock.json | 6 +- 3 files changed, 25 insertions(+), 171 deletions(-) diff --git a/gitnexus-web/package-lock.json b/gitnexus-web/package-lock.json index 82f082d15..53e1b5c69 100644 --- a/gitnexus-web/package-lock.json +++ b/gitnexus-web/package-lock.json @@ -57,7 +57,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.4", "@types/react-syntax-highlighter": "^15.5.13", - "@vercel/node": "^5.10.1", + "@vercel/node": "^5.10.2", "@vitejs/plugin-react": "^6.0.5", "@vitest/coverage-v8": "^4.1.9", "jsdom": "^29.1.1", @@ -307,13 +307,6 @@ "specificity": "bin/cli.js" } }, - "node_modules/@bytecodealliance/preview2-shim": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.17.6.tgz", - "integrity": "sha512-n3cM88gTen5980UOBAD6xDcNNL3ocTK8keab21bpx1ONdA+ARj7uD1qoFxOWCyKlkpSi195FH+GeAut7Oc6zZw==", - "dev": true, - "license": "(Apache-2.0 WITH LLVM-exception)" - }, "node_modules/@cfworker/json-schema": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", @@ -1419,17 +1412,6 @@ "node": ">=20" } }, - "node_modules/@renovatebot/pep440": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@renovatebot/pep440/-/pep440-4.2.1.tgz", - "integrity": "sha512-2FK1hF93Fuf1laSdfiEmJvSJPVIDHEUTz68D3Fi9s0IZrrpaEcj6pTFBTbYvsgC5du4ogrtf5re7yMMvrKNgkw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.9.0 || ^22.11.0 || ^24", - "pnpm": "^10.0.0" - } - }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", @@ -1517,9 +1499,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1536,9 +1515,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1555,9 +1531,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1574,9 +1547,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1593,9 +1563,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1612,9 +1579,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1865,9 +1829,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1884,9 +1845,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1903,9 +1861,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1922,9 +1877,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2641,13 +2593,12 @@ } }, "node_modules/@vercel/build-utils": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-14.1.1.tgz", - "integrity": "sha512-kW9CeW0aokEBvX1rSgNyOKg90VyIQOmT0wBl7KXneM3Qs1+x4Puakqp97BdIgttWEtmN96UvdhQVG2bCA5JsPA==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-14.2.0.tgz", + "integrity": "sha512-GwmtB31tBXQEzFw11grr8BKFCBdUORmYeooB0ZtonaCXZMZaPCHLBFTMFKsvaV6ZciQORPInRwXShbFvmnjqtg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@vercel/python-analysis": "0.13.2", "cjs-module-lexer": "1.2.3", "es-module-lexer": "1.5.0" } @@ -2694,9 +2645,9 @@ } }, "node_modules/@vercel/node": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.10.1.tgz", - "integrity": "sha512-muj+t8sZ2XHQDkWcHxkql2rbvr/HhZOqYdZBG7pw8F5RLasL3o0gjHLXJKwHAEHp2I3fd3AgbMud2oz+hzeV0g==", + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.10.2.tgz", + "integrity": "sha512-YBXcoQVOh5O2ySXvzE+POhPEQEPMJJo4ctlMMdp5why/NIoa8m6gotv14j8Uo6D5qyZsnc+0+++JgUiV4mYB6w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2704,7 +2655,7 @@ "@edge-runtime/primitives": "4.1.0", "@edge-runtime/vm": "3.2.0", "@types/node": "20.11.0", - "@vercel/build-utils": "14.1.1", + "@vercel/build-utils": "14.2.0", "@vercel/error-utils": "2.2.1", "@vercel/nft": "1.10.0", "@vercel/static-config": "3.4.1", @@ -2741,32 +2692,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@vercel/python-analysis": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@vercel/python-analysis/-/python-analysis-0.13.2.tgz", - "integrity": "sha512-IEr5K2gvX143NBoQc1W4BWrdDWjZwxnIT6UrL5Y1dnyH7Cqc4AV00FIAddB1YpnIZBJwT4ZhE8QbgqBeO6C9Zw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@bytecodealliance/preview2-shim": "0.17.6", - "@renovatebot/pep440": "4.2.1", - "fs-extra": "11.1.1", - "js-yaml": "4.1.1", - "minimatch": "10.1.1", - "smol-toml": "1.5.2", - "zod": "3.22.4" - } - }, - "node_modules/@vercel/python-analysis/node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/@vercel/static-config": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@vercel/static-config/-/static-config-3.4.1.tgz", @@ -3054,13 +2979,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -4514,21 +4432,6 @@ "node": ">=0.4.x" } }, - "node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5200,19 +5103,6 @@ "license": "MIT", "peer": true }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/jsdom": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", @@ -5268,9 +5158,9 @@ } }, "node_modules/jsdom/node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", "engines": { @@ -5322,19 +5212,6 @@ "dev": true, "license": "MIT" }, - "node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/katex": { "version": "0.16.47", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", @@ -6887,9 +6764,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -7808,19 +7685,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/smol-toml": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", - "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 18" - }, - "funding": { - "url": "https://github.com/sponsors/cyyynthia" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7955,9 +7819,9 @@ } }, "node_modules/tar": { - "version": "7.5.20", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.20.tgz", - "integrity": "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -8193,9 +8057,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.24.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.0.tgz", - "integrity": "sha512-lVLNosgqo5EkGqh5XUDhGfsMSoO8K0BAN0TyJLvwNRSl4xWGZlCVYsAIpa/OpA3TvmnM01GWcoKmc3ZWo5wKKA==", + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", "dev": true, "license": "MIT", "engines": { @@ -8296,16 +8160,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", diff --git a/gitnexus-web/package.json b/gitnexus-web/package.json index 440bf4a54..09ac0b640 100644 --- a/gitnexus-web/package.json +++ b/gitnexus-web/package.json @@ -67,7 +67,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.4", "@types/react-syntax-highlighter": "^15.5.13", - "@vercel/node": "^5.10.1", + "@vercel/node": "^5.10.2", "@vitejs/plugin-react": "^6.0.5", "@vitest/coverage-v8": "^4.1.9", "jsdom": "^29.1.1", @@ -83,7 +83,7 @@ }, "@vercel/node": { "path-to-regexp": "6.3.0", - "undici": "6.24.0" + "undici": "6.28.0" }, "@vercel/python-analysis": { "minimatch": "10.2.3", diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index b8b4c8b32..bd3ebd088 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -4521,9 +4521,9 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", - "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", "hasInstallScript": true, "license": "BSD-3-Clause", "optional": true,