fix(group)!: remove the matching cascade that was advertised but never built

`gitnexus group create` wrote `matching.bm25_threshold` and
`matching.embedding_threshold` into every generated group.yaml, and no matcher
ever read either one. That was not the whole of it — an entire feature surface
described a BM25/embedding cascade that does not exist:

- `matching.bm25_threshold` / `matching.embedding_threshold` — parsed, persisted,
  unread
- `detect.embedding_fallback` — defaulted and templated, unread
- `MatchType` declared `'bm25' | 'embedding'`; both variants unreachable
- `SyncOptions.skipEmbeddings` — declared in sync.ts and never read
- `gitnexus group sync --skip-embeddings` — accepted, threaded through
  GroupService, ignored
- CLI help in en and zh-CN promised "Exact + BM25 only (no embedding fallback)"
- the MCP `group_sync` schema exposed `skipEmbeddings`, described as
  "Exact + BM25 only (Demo PR: same as default exact path)"

`sync.ts` imports exactly `buildProviderIndex`, `runExactMatch` and
`runWildcardMatch`, and the printed cascade has one stage. An operator whose
links do not match reaches for those thresholds first, and turning either knob
changes nothing — config that silently does nothing is how people conclude a
feature is broken.

Evidence that the cascade should be deleted rather than implemented, from a real
backend/frontend pair: of 165 consumer contracts, 149 link exactly and 16 do not.
Nine of the sixteen are third-party APIs (Google OAuth, Apple public keys,
PostHog, image annotation) with no in-group provider by construction — similarity
matching cannot recover them, it can only invent false links. Two are verb
mismatches: the frontend calls `POST /links` and `GET /links/check-exists` while
the backend declares `GET /links` and eleven other `/links/*` routes but neither
of those, so a fuzzy path match would link a POST consumer to a GET provider. The
rest are path-extraction artifacts. Roughly none of the sixteen would be
correctly recovered, and several would be actively mis-linked.

BREAKING CHANGE: `gitnexus group sync --skip-embeddings` and the MCP `group_sync`
`skipEmbeddings` parameter are removed. Both were accepted and ignored, so no
behavior changes — but a script passing the flag now fails with `unknown option`
instead of being silently misled. Existing group.yaml files keep loading: the
removed keys are simply no longer part of the schema, and a regression test pins
that a legacy config carrying all three still parses.

Closes #3006

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ReidenXerx 2026-08-20 17:16:52 +03:00
parent aac7515d2a
commit 90425a4554
11 changed files with 27 additions and 21 deletions

View file

@ -149,7 +149,6 @@ export function registerGroupCommands(program: Command): void {
group
.command('sync <name>')
.description('Sync Contract Registry — extract contracts and build cross-links')
.option('--skip-embeddings', 'Exact + BM25 only (no embedding fallback)')
.option('--exact-only', 'Exact match only')
.option('--allow-stale', 'Skip stale index warnings')
.option('--verbose', 'Show each cross-link detail')
@ -168,7 +167,6 @@ export function registerGroupCommands(program: Command): void {
groupDir,
allowStale: Boolean(opts.allowStale),
verbose: Boolean(opts.verbose),
skipEmbeddings: Boolean(opts.skipEmbeddings),
exactOnly: Boolean(opts.exactOnly),
});

View file

@ -155,7 +155,6 @@ const OPTION_DESCRIPTION_KEYS = {
'embeddings install|--cuda': 'help.option.embeddings.install.cuda',
'embeddings install|--force': 'help.option.embeddings.install.force',
'group create|--force': 'help.option.group.create.force',
'group sync|--skip-embeddings': 'help.option.group.sync.skipEmbeddings',
'group sync|--exact-only': 'help.option.group.sync.exactOnly',
'group sync|--allow-stale': 'help.option.group.sync.allowStale',
'group sync|--verbose': 'help.option.group.sync.verbose',

View file

@ -293,7 +293,6 @@ export const en = {
'help.option.embeddings.install.force':
'Install into the runtime prefix even when the stack already resolves',
'help.option.group.create.force': 'Overwrite existing group',
'help.option.group.sync.skipEmbeddings': 'Exact + BM25 only (no embedding fallback)',
'help.option.group.sync.exactOnly': 'Exact match only',
'help.option.group.sync.allowStale': 'Skip stale index warnings',
'help.option.group.sync.verbose': 'Show each cross-link detail',

View file

@ -273,7 +273,6 @@ export const zhCN = {
'同时下载 CUDA GPU 二进制文件(运行 onnxruntime-node 的 NuGet postinstall;代理后请设置 GLOBAL_AGENT_HTTPS_PROXY)',
'help.option.embeddings.install.force': '即使嵌入组件已可解析,也强制安装到运行时目录',
'help.option.group.create.force': '覆盖现有仓库组',
'help.option.group.sync.skipEmbeddings': '仅使用 exact + BM25(不使用嵌入回退)',
'help.option.group.sync.exactOnly': '仅精确匹配',
'help.option.group.sync.allowStale': '跳过过期索引警告',
'help.option.group.sync.verbose': '显示每条跨仓库链接详情',

View file

@ -30,14 +30,11 @@ const DEFAULT_DETECT = {
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,

View file

@ -350,7 +350,6 @@ export class GroupService {
const result = await syncGroup(config, {
groupDir,
exactOnly: Boolean(params.exactOnly),
skipEmbeddings: Boolean(params.skipEmbeddings),
allowStale: Boolean(params.allowStale),
verbose: Boolean(params.verbose),
});

View file

@ -94,11 +94,8 @@ detect:
grpc: true
topics: true
shared_libs: true
embedding_fallback: true
matching:
bm25_threshold: 0.7
embedding_threshold: 0.65
max_candidates_per_step: 3
# exclude_links_paths: [/ping, /health, /healthcheck]
# exclude_links_param_only_paths: false

Binary file not shown.

View file

@ -1,5 +1,5 @@
export type ContractType = 'http' | 'grpc' | 'thrift' | 'topic' | 'lib' | 'custom' | 'include';
export type MatchType = 'exact' | 'manifest' | 'wildcard' | 'bm25' | 'embedding';
export type MatchType = 'exact' | 'manifest' | 'wildcard';
export type ContractRole = 'provider' | 'consumer';
export interface GroupConfig {
@ -27,14 +27,11 @@ export interface DetectConfig {
thrift: boolean;
topics: boolean;
shared_libs: boolean;
embedding_fallback: boolean;
includes: boolean;
workspace_deps: boolean;
}
export interface MatchingConfig {
bm25_threshold: number;
embedding_threshold: number;
max_candidates_per_step: number;
/**
* HTTP paths to exclude from cross-link matching. Contracts at these paths

View file

@ -859,10 +859,6 @@ WHEN TO USE: After changing group.yaml or re-indexing member repos.`,
type: 'object',
properties: {
name: { type: 'string', description: 'Group name' },
skipEmbeddings: {
type: 'boolean',
description: 'Exact + BM25 only (Demo PR: same as default exact path)',
},
exactOnly: { type: 'boolean', description: 'Exact match only in cascade' },
},
required: ['name'],

View file

@ -59,11 +59,36 @@ repos:
expect(config.links).toEqual([]);
expect(config.packages).toEqual({});
expect(config.detect.http).toBe(true);
expect(config.matching.bm25_threshold).toBe(0.7);
expect(config.matching.max_candidates_per_step).toBe(3);
expect(config.matching.exclude_links_paths).toEqual([]);
expect(config.matching.exclude_links_param_only_paths).toBe(false);
});
it('still parses a legacy config carrying the removed matching knobs', () => {
// `bm25_threshold`, `embedding_threshold` and `detect.embedding_fallback`
// were written into every generated group.yaml but read by no matcher, so
// they are gone from the schema and the template. Every group.yaml already
// on disk still has them, and must keep loading without complaint.
const legacy = `
version: 1
name: test
repos:
app: my-app
detect:
http: true
embedding_fallback: true
matching:
bm25_threshold: 0.7
embedding_threshold: 0.65
max_candidates_per_step: 3
`;
const config = parseGroupConfig(legacy);
expect(config.name).toBe('test');
expect(config.repos).toEqual({ app: 'my-app' });
expect(config.detect.http).toBe(true);
expect(config.matching.max_candidates_per_step).toBe(3);
});
it('defaults thrift detection to true', () => {
const minimal = `
version: 1