From aa4a8482e5e7849b874972a940f89818bb1d2a45 Mon Sep 17 00:00:00 2001 From: shibf Date: Tue, 2 Jun 2026 06:33:55 +0000 Subject: [PATCH] feat: add SOFA framework extractor for RPC and MQ auto-detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add native support for Alibaba SOFA framework contract extraction: - New SofaExtractor: discovers SOFA RPC providers and consumers from XML declarations (/ with ) - Extend Java topic extractor: detect SOFAMQ (OpenMessaging API) patterns - consumer.subscribe(topic, tag, listener) → topic consumer - new Message(topic, tag, body) → topic provider - Add 'sofamq' broker type to topic pattern types - Add 'sofa' opt-in detect flag to group config (default: false) SOFA RPC uses XML namespace prefix (sofa:), making regex extraction reliable without a full XML tree-sitter grammar. Note: XML sofa:consumer/sofa:channel/sofa:binding.msg_broker declarations are intentionally NOT extracted as they represent distributed scheduled task dispatching (e.g. TP_F_SC channel), not real message pub/sub. --- gitnexus/src/core/group/config-parser.ts | 1 + .../core/group/extractors/sofa-extractor.ts | 136 ++++++++++++++++++ .../group/extractors/topic-patterns/java.ts | 32 ++++- .../group/extractors/topic-patterns/types.ts | 2 +- gitnexus/src/core/group/sync.ts | 13 ++ gitnexus/src/core/group/types.ts | 2 + 6 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 gitnexus/src/core/group/extractors/sofa-extractor.ts diff --git a/gitnexus/src/core/group/config-parser.ts b/gitnexus/src/core/group/config-parser.ts index 29c868171..ebf0723b5 100644 --- a/gitnexus/src/core/group/config-parser.ts +++ b/gitnexus/src/core/group/config-parser.ts @@ -33,6 +33,7 @@ const DEFAULT_DETECT = { embedding_fallback: true, includes: false, workspace_deps: false, + sofa: false, }; const DEFAULT_MATCHING = { diff --git a/gitnexus/src/core/group/extractors/sofa-extractor.ts b/gitnexus/src/core/group/extractors/sofa-extractor.ts new file mode 100644 index 000000000..0c7d2da1b --- /dev/null +++ b/gitnexus/src/core/group/extractors/sofa-extractor.ts @@ -0,0 +1,136 @@ +import { glob } from 'glob'; +import { createIgnoreFilter } from '../../../config/ignore-service.js'; +import type { ContractExtractor, CypherExecutor } from '../contract-extractor.js'; +import type { ExtractedContract, RepoHandle } from '../types.js'; +import { readSafe } from './fs-utils.js'; + +/** + * SOFA framework extractor for RPC contract discovery from XML files. + * + * SOFA RPC (Alibaba SOFA framework) uses XML declarations: + * - `` + `` → provider + * - `` + `` → consumer + * + * SOFAMQ topic extraction is handled by the Java tree-sitter topic extractor + * (topic-patterns/java.ts), which detects OpenMessaging API calls: + * - `consumer.subscribe("topic", "tag", listener)` → topic consumer + * - `new Message("topic", "tag", body)` → topic provider + * + * XML `sofa:consumer` + `sofa:channel` + `sofa:binding.msg_broker` declarations + * are NOT extracted because they represent distributed scheduled task dispatching + * (e.g. `TP_F_SC` channel), not real message pub/sub relationships. + * + * Because SOFA XML uses a distinctive namespace prefix, regex extraction is + * reliable and avoids the need for a full XML tree-sitter grammar. + */ + +const SOFA_XML_GLOB = '**/*.xml'; + +// ─── Regex patterns for SOFA XML elements ─────────────────────────── + +/** + * Match … blocks. + * Captures the interface attribute and the full block content. + * Note: self-closing is not matched; SOFA XML always + * uses the closing tag because it contains binding children. + */ +const SOFA_SERVICE_RE = /]*interface="([^"]+)"[^>]*>([\s\S]*?)<\/sofa:service>/g; + +/** + * Match (may be self-closing or have body). + * Captures the interface attribute. + */ +const SOFA_REFERENCE_RE = /]*interface="([^"]+)"[^>]*\/?>/g; + +/** + * Detect TR (bolt) binding inside a sofa:service block. + */ +const BINDING_TR_RE = /sofa:binding\.tr/; + +// ─── Helper: build an ExtractedContract ────────────────────────────── + +function makeRpcContract( + interfaceName: string, + role: 'provider' | 'consumer', + filePath: string, +): ExtractedContract { + return { + contractId: `custom::${interfaceName}`, + type: 'custom', + role, + symbolUid: '', + symbolRef: { filePath: filePath.replace(/\\/g, '/'), name: interfaceName }, + symbolName: interfaceName, + confidence: 0.85, + meta: { + framework: 'sofa-rpc', + interface: interfaceName, + extractionStrategy: 'regex_xml', + }, + }; +} + +// ─── Extractor class ───────────────────────────────────────────────── + +export class SofaExtractor implements ContractExtractor { + type = 'custom' as const; + + async canExtract(_repo: RepoHandle): Promise { + return true; + } + + async extract( + _dbExecutor: CypherExecutor | null, + repoPath: string, + _repo: RepoHandle, + ): Promise { + const baseFilter = await createIgnoreFilter(repoPath); + const files = await glob(SOFA_XML_GLOB, { + cwd: repoPath, + ignore: baseFilter, + nodir: true, + }); + + const out: ExtractedContract[] = []; + + for (const rel of files) { + const content = readSafe(repoPath, rel); + if (!content) continue; + + // Skip files that don't contain any SOFA namespace declarations + if (!content.includes('sofa:')) continue; + + // ── SOFA RPC: sofa:service (provider) ── + let m: RegExpExecArray | null; + SOFA_SERVICE_RE.lastIndex = 0; + while ((m = SOFA_SERVICE_RE.exec(content)) !== null) { + const interfaceName = m[1]; + const body = m[2]; + if (BINDING_TR_RE.test(body)) { + out.push(makeRpcContract(interfaceName, 'provider', rel)); + } + } + + // ── SOFA RPC: sofa:reference (consumer) ── + SOFA_REFERENCE_RE.lastIndex = 0; + while ((m = SOFA_REFERENCE_RE.exec(content)) !== null) { + const interfaceName = m[1]; + out.push(makeRpcContract(interfaceName, 'consumer', rel)); + } + } + + return this.dedupe(out); + } + + private dedupe(items: ExtractedContract[]): ExtractedContract[] { + const seen = new Set(); + const out: ExtractedContract[] = []; + for (const c of items) { + const k = `${c.contractId}|${c.role}|${c.symbolRef.filePath}`; + if (seen.has(k)) continue; + seen.add(k); + out.push(c); + } + return out; + } +} diff --git a/gitnexus/src/core/group/extractors/topic-patterns/java.ts b/gitnexus/src/core/group/extractors/topic-patterns/java.ts index d126f25ce..be02904a2 100644 --- a/gitnexus/src/core/group/extractors/topic-patterns/java.ts +++ b/gitnexus/src/core/group/extractors/topic-patterns/java.ts @@ -5,11 +5,14 @@ import type { TopicMeta } from './types.js'; /** * Java topic extraction patterns. * - * Detects Kafka and RabbitMQ (Spring conventions) producer/consumer APIs: + * Detects Kafka, RabbitMQ (Spring conventions) and SOFAMQ (OpenMessaging API) + * producer/consumer APIs: * - `@KafkaListener(topics = "xxx")` * - `@RabbitListener(queues = "xxx")` * - `kafkaTemplate.send("xxx", ...)` * - `rabbitTemplate.convertAndSend("xxx", ...)` + * - `consumer.subscribe("topic", "tag", listener)` (SOFAMQ consumer) + * - `new Message("topic", "tag", body)` (SOFAMQ producer) * * Every query MUST bind `@value` to the topic literal node. */ @@ -77,6 +80,33 @@ const JAVA_TOPIC_SPEC: LanguagePatterns = { arguments: (argument_list . (string_literal) @value)) `, }, + { + meta: { + role: 'consumer', + broker: 'sofamq', + confidence: 0.7, + symbolName: 'openMessaging.Consumer.subscribe', + }, + query: ` + (method_invocation + object: (identifier) @obj (#eq? @obj "consumer") + name: (identifier) @method (#eq? @method "subscribe") + arguments: (argument_list . (string_literal) @value)) + `, + }, + { + meta: { + role: 'provider', + broker: 'sofamq', + confidence: 0.7, + symbolName: 'openMessaging.Message.constructor', + }, + query: ` + (object_creation_expression + type: (type_identifier) @cls (#eq? @cls "Message") + arguments: (argument_list . (string_literal) @value)) + `, + }, ], }; diff --git a/gitnexus/src/core/group/extractors/topic-patterns/types.ts b/gitnexus/src/core/group/extractors/topic-patterns/types.ts index 3a27f21d3..b529fd037 100644 --- a/gitnexus/src/core/group/extractors/topic-patterns/types.ts +++ b/gitnexus/src/core/group/extractors/topic-patterns/types.ts @@ -8,7 +8,7 @@ * query text directly — that's the whole point of the split. */ -export type Broker = 'kafka' | 'rabbitmq' | 'nats'; +export type Broker = 'kafka' | 'rabbitmq' | 'nats' | 'sofamq'; /** * Per-pattern payload every topic plugin attaches to its query. Whatever diff --git a/gitnexus/src/core/group/sync.ts b/gitnexus/src/core/group/sync.ts index 5c535f29f..7e7a5533e 100644 --- a/gitnexus/src/core/group/sync.ts +++ b/gitnexus/src/core/group/sync.ts @@ -8,6 +8,7 @@ import { HttpRouteExtractor } from './extractors/http-route-extractor.js'; import { GrpcExtractor } from './extractors/grpc-extractor.js'; import { ThriftExtractor } from './extractors/thrift-extractor.js'; import { TopicExtractor } from './extractors/topic-extractor.js'; +import { SofaExtractor } from './extractors/sofa-extractor.js'; import { IncludeExtractor } from './extractors/include-extractor.js'; import { ManifestExtractor } from './extractors/manifest-extractor.js'; import { discoverWorkspaceLinks } from './extractors/workspace-extractor.js'; @@ -105,6 +106,7 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis const grpcEx = new GrpcExtractor(); const thriftEx = new ThriftExtractor(); const topicEx = new TopicExtractor(); + const sofaEx = new SofaExtractor(); const includeEx = new IncludeExtractor(); dbExecutors = new Map(); @@ -172,6 +174,17 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis } } + if (config.detect.sofa) { + const extracted = await sofaEx.extract(executor, handle.repoPath, handle); + for (const c of extracted) { + autoContracts.push({ + ...c, + repo: groupPath, + service: assignService(c.symbolRef.filePath, boundaries), + }); + } + } + if (config.detect.includes) { const extracted = await includeEx.extract(executor, handle.repoPath, handle); for (const c of extracted) { diff --git a/gitnexus/src/core/group/types.ts b/gitnexus/src/core/group/types.ts index 8e43ff78f..c19ff083b 100644 --- a/gitnexus/src/core/group/types.ts +++ b/gitnexus/src/core/group/types.ts @@ -30,6 +30,8 @@ export interface DetectConfig { embedding_fallback: boolean; includes: boolean; workspace_deps: boolean; + /** SOFA framework (SOFA RPC + SOFAMQ) XML contract extraction. Opt-in. */ + sofa: boolean; } export interface MatchingConfig {