mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-25 01:01:28 +00:00
Restore types.ts, config-parser.ts, matching.ts, sync.ts, and manifest-extractor.ts to upstream/main versions. The original commit accidentally removed fields (thrift, workspace_deps, exclude_links_paths, exclude_links_param_only_paths) from DetectConfig/MatchingConfig/ContractType which are still referenced by matching.test.ts, config-parser.test.ts, sync.test.ts and other integration tests. This PR's scope is C++ scope-resolution parity only — group module type definitions and logic should remain unchanged.
135 lines
4.5 KiB
TypeScript
135 lines
4.5 KiB
TypeScript
import { createRequire } from 'node:module';
|
|
import type { GroupConfig, GroupManifestLink, ContractType, ContractRole } from './types.js';
|
|
|
|
const _require = createRequire(import.meta.url);
|
|
const yaml = _require('js-yaml') as typeof import('js-yaml');
|
|
|
|
const VALID_CONTRACT_TYPES: ContractType[] = [
|
|
'http',
|
|
'grpc',
|
|
'thrift',
|
|
'topic',
|
|
'lib',
|
|
'custom',
|
|
'include',
|
|
];
|
|
const VALID_ROLES: ContractRole[] = ['provider', 'consumer'];
|
|
|
|
// Defaults matter for backward compatibility: any group.yaml that omits a
|
|
// `detect.<type>` key inherits its value from this constant. Adding a new
|
|
// extractor that defaults to `true` silently changes the behavior of every
|
|
// existing group on the next sync. New extractors must default to `false`
|
|
// (opt-in) so operators consciously enable them via group.yaml.
|
|
//
|
|
// `includes`: opt-in. The C/C++ IncludeExtractor (PR #1156) ships disabled by
|
|
// default; enable with `detect.includes: true` for groups containing C/C++
|
|
// repos that need cross-repo header tracking.
|
|
const DEFAULT_DETECT = {
|
|
http: true,
|
|
grpc: true,
|
|
thrift: true,
|
|
topics: true,
|
|
shared_libs: true,
|
|
embedding_fallback: true,
|
|
includes: false,
|
|
workspace_deps: false,
|
|
};
|
|
|
|
const DEFAULT_MATCHING = {
|
|
bm25_threshold: 0.7,
|
|
embedding_threshold: 0.65,
|
|
max_candidates_per_step: 3,
|
|
exclude_links_paths: [] as string[],
|
|
exclude_links_param_only_paths: false,
|
|
};
|
|
|
|
export function parseGroupConfig(yamlContent: string): GroupConfig {
|
|
const raw = yaml.load(yamlContent, { schema: yaml.JSON_SCHEMA }) as Record<string, unknown>;
|
|
|
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
throw new Error('Invalid YAML: expected an object');
|
|
}
|
|
|
|
if (raw.version === undefined) throw new Error('version is required in group.yaml');
|
|
if (raw.version !== 1) {
|
|
throw new Error(`Unsupported group.yaml version: ${raw.version}. Expected 1.`);
|
|
}
|
|
if (!raw.name || typeof raw.name !== 'string') throw new Error('name is required in group.yaml');
|
|
if (!raw.repos || typeof raw.repos !== 'object' || Array.isArray(raw.repos)) {
|
|
throw new Error('repos is required in group.yaml (must be a mapping)');
|
|
}
|
|
|
|
const repos = raw.repos as Record<string, string>;
|
|
const repoPaths = new Set(Object.keys(repos));
|
|
|
|
const rawLinks = (raw.links as unknown[]) || [];
|
|
const links: GroupManifestLink[] = rawLinks.map((l: unknown, i: number) => {
|
|
const link = l as Record<string, unknown>;
|
|
if (!link.from || !repoPaths.has(link.from as string)) {
|
|
throw new Error(`links[${i}].from "${link.from}" does not match any repo path in group`);
|
|
}
|
|
if (!link.to || !repoPaths.has(link.to as string)) {
|
|
throw new Error(`links[${i}].to "${link.to}" does not match any repo path in group`);
|
|
}
|
|
if (!VALID_CONTRACT_TYPES.includes(link.type as ContractType)) {
|
|
throw new Error(
|
|
`links[${i}].type "${link.type}" is invalid. Expected: ${VALID_CONTRACT_TYPES.join(', ')}`,
|
|
);
|
|
}
|
|
if (!VALID_ROLES.includes(link.role as ContractRole)) {
|
|
throw new Error(`links[${i}].role "${link.role}" is invalid. Expected: provider | consumer`);
|
|
}
|
|
if (
|
|
link.contract === undefined ||
|
|
link.contract === null ||
|
|
String(link.contract).trim() === ''
|
|
) {
|
|
throw new Error(`links[${i}].contract is required`);
|
|
}
|
|
return {
|
|
from: link.from as string,
|
|
to: link.to as string,
|
|
type: link.type as ContractType,
|
|
contract: String(link.contract),
|
|
role: link.role as ContractRole,
|
|
};
|
|
});
|
|
|
|
const detect = { ...DEFAULT_DETECT, ...((raw.detect as object) || {}) };
|
|
const matching = { ...DEFAULT_MATCHING, ...((raw.matching as object) || {}) };
|
|
const packages = (raw.packages as Record<string, Record<string, string>>) || {};
|
|
|
|
return {
|
|
version: 1,
|
|
name: raw.name as string,
|
|
description: (raw.description as string) || '',
|
|
repos,
|
|
links,
|
|
packages,
|
|
detect,
|
|
matching,
|
|
};
|
|
}
|
|
|
|
export class GroupNotFoundError extends Error {
|
|
constructor(public readonly groupName: string) {
|
|
super(`Group "${groupName}" not found`);
|
|
this.name = 'GroupNotFoundError';
|
|
}
|
|
}
|
|
|
|
export async function loadGroupConfig(groupDir: string): Promise<GroupConfig> {
|
|
const fsp = await import('node:fs/promises');
|
|
const path = await import('node:path');
|
|
const yamlPath = path.join(groupDir, 'group.yaml');
|
|
let content: string;
|
|
try {
|
|
content = await fsp.readFile(yamlPath, 'utf-8');
|
|
} catch (err) {
|
|
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
throw new GroupNotFoundError(path.basename(groupDir));
|
|
}
|
|
throw err;
|
|
}
|
|
return parseGroupConfig(content);
|
|
}
|