gitnexus 1.6.6-rc.95 → 1.6.6-rc.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/cli/help-i18n.js +3 -0
- package/dist/cli/i18n/en.d.ts +3 -1
- package/dist/cli/i18n/en.js +3 -1
- package/dist/cli/i18n/resources.d.ts +5 -1
- package/dist/cli/i18n/zh-CN.d.ts +2 -0
- package/dist/cli/i18n/zh-CN.js +3 -1
- package/dist/cli/index.js +4 -1
- package/dist/cli/tool.d.ts +4 -1
- package/dist/cli/tool.js +27 -4
- package/dist/core/ingestion/csharp-namespace-gate.d.ts +25 -0
- package/dist/core/ingestion/csharp-namespace-gate.js +129 -0
- package/dist/core/ingestion/import-resolvers/configs/csharp.js +25 -10
- package/dist/core/ingestion/import-resolvers/csharp.d.ts +6 -2
- package/dist/core/ingestion/import-resolvers/csharp.js +11 -2
- package/dist/core/ingestion/import-resolvers/types.d.ts +3 -1
- package/dist/core/ingestion/language-config.d.ts +40 -3
- package/dist/core/ingestion/language-config.js +232 -35
- package/dist/core/ingestion/languages/csharp/import-target.d.ts +8 -4
- package/dist/core/ingestion/languages/csharp/import-target.js +128 -78
- package/dist/core/ingestion/languages/csharp/namespace-siblings.d.ts +22 -0
- package/dist/core/ingestion/languages/csharp/namespace-siblings.js +68 -33
- package/dist/core/ingestion/languages/csharp/resolution-config.d.ts +14 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.js +15 -0
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +10 -2
- package/dist/mcp/local/local-backend.js +17 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -170,6 +170,13 @@ gitnexus clean --all --force # Delete all indexes
|
|
|
170
170
|
gitnexus wiki [path] # Generate LLM-powered docs from knowledge graph
|
|
171
171
|
gitnexus wiki --model <model> # Wiki with custom LLM model (default: gpt-4o-mini)
|
|
172
172
|
|
|
173
|
+
# Direct graph queries — the same tools the MCP server exposes, no MCP daemon needed
|
|
174
|
+
gitnexus query "<concept>" # Process-grouped hybrid search
|
|
175
|
+
gitnexus context <symbol> [--uid <uid> | --file <path>] # 360° symbol view; flags disambiguate a shared name
|
|
176
|
+
gitnexus impact <symbol> [--uid <uid> | --file <path> | --kind <kind>] # Blast radius; flags disambiguate a shared name
|
|
177
|
+
gitnexus detect-changes # Map the working-tree diff to affected symbols and execution flows
|
|
178
|
+
gitnexus cypher "<query>" # Run a raw Cypher query against the knowledge graph
|
|
179
|
+
|
|
173
180
|
# Repository groups (multi-repo / monorepo service tracking)
|
|
174
181
|
gitnexus group create <name> # Create a repository group
|
|
175
182
|
gitnexus group add <group> <groupPath> <registryName> # Add a repo to a group. <groupPath> is a hierarchy path (e.g. hr/hiring/backend); <registryName> is the repo's name from the registry (see `gitnexus list`)
|
package/dist/cli/help-i18n.js
CHANGED
|
@@ -97,6 +97,9 @@ const OPTION_DESCRIPTION_KEYS = {
|
|
|
97
97
|
'context|--content': 'help.option.content',
|
|
98
98
|
'impact|-d, --direction <dir>': 'help.option.impact.direction',
|
|
99
99
|
'impact|-r, --repo <name>': 'help.option.repo.target',
|
|
100
|
+
'impact|-u, --uid <uid>': 'help.option.context.uid',
|
|
101
|
+
'impact|-f, --file <path>': 'help.option.context.file',
|
|
102
|
+
'impact|--kind <kind>': 'help.option.impact.kind',
|
|
100
103
|
'impact|--depth <n>': 'help.option.impact.depth',
|
|
101
104
|
'impact|--include-tests': 'help.option.impact.includeTests',
|
|
102
105
|
'impact|--limit <n>': 'help.option.impact.limit',
|
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -41,8 +41,9 @@ export declare const en: {
|
|
|
41
41
|
readonly 'tool.noIndexed': "GitNexus: No indexed repositories found. Run: gitnexus analyze";
|
|
42
42
|
readonly 'tool.usage.query': "Usage: gitnexus query <search_query>";
|
|
43
43
|
readonly 'tool.usage.context': "Usage: gitnexus context <symbol_name> [--uid <uid>] [--file <path>]";
|
|
44
|
-
readonly 'tool.usage.impact': "Usage: gitnexus impact <symbol_name> [--direction upstream|downstream]";
|
|
44
|
+
readonly 'tool.usage.impact': "Usage: gitnexus impact <symbol_name> [--uid <uid>] [--file <path>] [--kind <kind>] [--direction upstream|downstream]";
|
|
45
45
|
readonly 'tool.usage.cypher': "Usage: gitnexus cypher <cypher_query>";
|
|
46
|
+
readonly 'tool.warn.unknownKind': "--kind '{{kind}}' is not a known symbol kind (e.g. Function, Class, Method); it will not narrow the result.";
|
|
46
47
|
readonly 'tool.detectChanges.noChanges': "No changes detected.";
|
|
47
48
|
readonly 'tool.detectChanges.changesSummary': "Changes: {{files}} files, {{symbols}} symbols";
|
|
48
49
|
readonly 'tool.detectChanges.affectedProcesses': "Affected processes: {{count}}";
|
|
@@ -177,6 +178,7 @@ export declare const en: {
|
|
|
177
178
|
readonly 'help.option.repo.target': "Target repository";
|
|
178
179
|
readonly 'help.option.context.uid': "Direct symbol UID (zero-ambiguity lookup)";
|
|
179
180
|
readonly 'help.option.context.file': "File path to disambiguate common names";
|
|
181
|
+
readonly 'help.option.impact.kind': "Kind filter to disambiguate common names (e.g. Function, Class, Method)";
|
|
180
182
|
readonly 'help.option.impact.direction': "upstream (dependants) or downstream (dependencies)";
|
|
181
183
|
readonly 'help.option.impact.depth': "Max relationship depth (default: 3)";
|
|
182
184
|
readonly 'help.option.impact.includeTests': "Include test files in results";
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -41,8 +41,9 @@ export const en = {
|
|
|
41
41
|
'tool.noIndexed': 'GitNexus: No indexed repositories found. Run: gitnexus analyze',
|
|
42
42
|
'tool.usage.query': 'Usage: gitnexus query <search_query>',
|
|
43
43
|
'tool.usage.context': 'Usage: gitnexus context <symbol_name> [--uid <uid>] [--file <path>]',
|
|
44
|
-
'tool.usage.impact': 'Usage: gitnexus impact <symbol_name> [--direction upstream|downstream]',
|
|
44
|
+
'tool.usage.impact': 'Usage: gitnexus impact <symbol_name> [--uid <uid>] [--file <path>] [--kind <kind>] [--direction upstream|downstream]',
|
|
45
45
|
'tool.usage.cypher': 'Usage: gitnexus cypher <cypher_query>',
|
|
46
|
+
'tool.warn.unknownKind': "--kind '{{kind}}' is not a known symbol kind (e.g. Function, Class, Method); it will not narrow the result.",
|
|
46
47
|
'tool.detectChanges.noChanges': 'No changes detected.',
|
|
47
48
|
'tool.detectChanges.changesSummary': 'Changes: {{files}} files, {{symbols}} symbols',
|
|
48
49
|
'tool.detectChanges.affectedProcesses': 'Affected processes: {{count}}',
|
|
@@ -177,6 +178,7 @@ export const en = {
|
|
|
177
178
|
'help.option.repo.target': 'Target repository',
|
|
178
179
|
'help.option.context.uid': 'Direct symbol UID (zero-ambiguity lookup)',
|
|
179
180
|
'help.option.context.file': 'File path to disambiguate common names',
|
|
181
|
+
'help.option.impact.kind': 'Kind filter to disambiguate common names (e.g. Function, Class, Method)',
|
|
180
182
|
'help.option.impact.direction': 'upstream (dependants) or downstream (dependencies)',
|
|
181
183
|
'help.option.impact.depth': 'Max relationship depth (default: 3)',
|
|
182
184
|
'help.option.impact.includeTests': 'Include test files in results',
|
|
@@ -42,8 +42,9 @@ export declare const cliResources: {
|
|
|
42
42
|
readonly 'tool.noIndexed': "GitNexus: No indexed repositories found. Run: gitnexus analyze";
|
|
43
43
|
readonly 'tool.usage.query': "Usage: gitnexus query <search_query>";
|
|
44
44
|
readonly 'tool.usage.context': "Usage: gitnexus context <symbol_name> [--uid <uid>] [--file <path>]";
|
|
45
|
-
readonly 'tool.usage.impact': "Usage: gitnexus impact <symbol_name> [--direction upstream|downstream]";
|
|
45
|
+
readonly 'tool.usage.impact': "Usage: gitnexus impact <symbol_name> [--uid <uid>] [--file <path>] [--kind <kind>] [--direction upstream|downstream]";
|
|
46
46
|
readonly 'tool.usage.cypher': "Usage: gitnexus cypher <cypher_query>";
|
|
47
|
+
readonly 'tool.warn.unknownKind': "--kind '{{kind}}' is not a known symbol kind (e.g. Function, Class, Method); it will not narrow the result.";
|
|
47
48
|
readonly 'tool.detectChanges.noChanges': "No changes detected.";
|
|
48
49
|
readonly 'tool.detectChanges.changesSummary': "Changes: {{files}} files, {{symbols}} symbols";
|
|
49
50
|
readonly 'tool.detectChanges.affectedProcesses': "Affected processes: {{count}}";
|
|
@@ -178,6 +179,7 @@ export declare const cliResources: {
|
|
|
178
179
|
readonly 'help.option.repo.target': "Target repository";
|
|
179
180
|
readonly 'help.option.context.uid': "Direct symbol UID (zero-ambiguity lookup)";
|
|
180
181
|
readonly 'help.option.context.file': "File path to disambiguate common names";
|
|
182
|
+
readonly 'help.option.impact.kind': "Kind filter to disambiguate common names (e.g. Function, Class, Method)";
|
|
181
183
|
readonly 'help.option.impact.direction': "upstream (dependants) or downstream (dependencies)";
|
|
182
184
|
readonly 'help.option.impact.depth': "Max relationship depth (default: 3)";
|
|
183
185
|
readonly 'help.option.impact.includeTests': "Include test files in results";
|
|
@@ -253,6 +255,7 @@ export declare const cliResources: {
|
|
|
253
255
|
'tool.usage.context': string;
|
|
254
256
|
'tool.usage.impact': string;
|
|
255
257
|
'tool.usage.cypher': string;
|
|
258
|
+
'tool.warn.unknownKind': string;
|
|
256
259
|
'tool.detectChanges.noChanges': string;
|
|
257
260
|
'tool.detectChanges.changesSummary': string;
|
|
258
261
|
'tool.detectChanges.affectedProcesses': string;
|
|
@@ -387,6 +390,7 @@ export declare const cliResources: {
|
|
|
387
390
|
'help.option.repo.target': string;
|
|
388
391
|
'help.option.context.uid': string;
|
|
389
392
|
'help.option.context.file': string;
|
|
393
|
+
'help.option.impact.kind': string;
|
|
390
394
|
'help.option.impact.direction': string;
|
|
391
395
|
'help.option.impact.depth': string;
|
|
392
396
|
'help.option.impact.includeTests': string;
|
package/dist/cli/i18n/zh-CN.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ export declare const zhCN: {
|
|
|
43
43
|
'tool.usage.context': string;
|
|
44
44
|
'tool.usage.impact': string;
|
|
45
45
|
'tool.usage.cypher': string;
|
|
46
|
+
'tool.warn.unknownKind': string;
|
|
46
47
|
'tool.detectChanges.noChanges': string;
|
|
47
48
|
'tool.detectChanges.changesSummary': string;
|
|
48
49
|
'tool.detectChanges.affectedProcesses': string;
|
|
@@ -177,6 +178,7 @@ export declare const zhCN: {
|
|
|
177
178
|
'help.option.repo.target': string;
|
|
178
179
|
'help.option.context.uid': string;
|
|
179
180
|
'help.option.context.file': string;
|
|
181
|
+
'help.option.impact.kind': string;
|
|
180
182
|
'help.option.impact.direction': string;
|
|
181
183
|
'help.option.impact.depth': string;
|
|
182
184
|
'help.option.impact.includeTests': string;
|
package/dist/cli/i18n/zh-CN.js
CHANGED
|
@@ -41,8 +41,9 @@ export const zhCN = {
|
|
|
41
41
|
'tool.noIndexed': 'GitNexus:未找到已索引仓库。请运行:gitnexus analyze',
|
|
42
42
|
'tool.usage.query': '用法:gitnexus query <搜索词>',
|
|
43
43
|
'tool.usage.context': '用法:gitnexus context <符号名> [--uid <uid>] [--file <路径>]',
|
|
44
|
-
'tool.usage.impact': '用法:gitnexus impact <符号名> [--direction upstream|downstream]',
|
|
44
|
+
'tool.usage.impact': '用法:gitnexus impact <符号名> [--uid <uid>] [--file <路径>] [--kind <类型>] [--direction upstream|downstream]',
|
|
45
45
|
'tool.usage.cypher': '用法:gitnexus cypher <Cypher 查询>',
|
|
46
|
+
'tool.warn.unknownKind': "--kind '{{kind}}' 不是已知的符号类型(如 Function、Class、Method),不会用于缩小结果范围。",
|
|
46
47
|
'tool.detectChanges.noChanges': '未检测到变更。',
|
|
47
48
|
'tool.detectChanges.changesSummary': '变更:{{files}} 个文件,{{symbols}} 个符号',
|
|
48
49
|
'tool.detectChanges.affectedProcesses': '受影响流程:{{count}}',
|
|
@@ -177,6 +178,7 @@ export const zhCN = {
|
|
|
177
178
|
'help.option.repo.target': '目标仓库',
|
|
178
179
|
'help.option.context.uid': '直接符号 UID(零歧义查找)',
|
|
179
180
|
'help.option.context.file': '用于消除常见名称歧义的文件路径',
|
|
181
|
+
'help.option.impact.kind': '用于消除常见名称歧义的类型过滤(如 Function、Class、Method)',
|
|
180
182
|
'help.option.impact.direction': 'upstream(依赖它的项)或 downstream(它依赖的项)',
|
|
181
183
|
'help.option.impact.depth': '最大关系遍历深度(默认:3)',
|
|
182
184
|
'help.option.impact.includeTests': '在结果中包含测试文件',
|
package/dist/cli/index.js
CHANGED
|
@@ -142,10 +142,13 @@ program
|
|
|
142
142
|
.option('--content', 'Include full symbol source code')
|
|
143
143
|
.action(createLbugLazyAction(() => import('./tool.js'), 'contextCommand'));
|
|
144
144
|
program
|
|
145
|
-
.command('impact
|
|
145
|
+
.command('impact [target]')
|
|
146
146
|
.description('Blast radius analysis: what breaks if you change a symbol')
|
|
147
147
|
.option('-d, --direction <dir>', 'upstream (dependants) or downstream (dependencies)', 'upstream')
|
|
148
148
|
.option('-r, --repo <name>', 'Target repository')
|
|
149
|
+
.option('-u, --uid <uid>', 'Direct symbol UID (zero-ambiguity lookup)')
|
|
150
|
+
.option('-f, --file <path>', 'File path to disambiguate common names')
|
|
151
|
+
.option('--kind <kind>', 'Kind filter to disambiguate common names (e.g. Function, Class, Method)')
|
|
149
152
|
.option('--depth <n>', 'Max relationship depth (default: 3)')
|
|
150
153
|
.option('--include-tests', 'Include test files in results')
|
|
151
154
|
.option('--limit <n>', 'Max symbols per depth level (default: 100)')
|
package/dist/cli/tool.d.ts
CHANGED
|
@@ -27,9 +27,12 @@ export declare function contextCommand(name: string, options?: {
|
|
|
27
27
|
uid?: string;
|
|
28
28
|
content?: boolean;
|
|
29
29
|
}): Promise<void>;
|
|
30
|
-
export declare function impactCommand(target
|
|
30
|
+
export declare function impactCommand(target?: string, options?: {
|
|
31
31
|
direction?: string;
|
|
32
32
|
repo?: string;
|
|
33
|
+
uid?: string;
|
|
34
|
+
file?: string;
|
|
35
|
+
kind?: string;
|
|
33
36
|
depth?: string;
|
|
34
37
|
includeTests?: boolean;
|
|
35
38
|
limit?: string;
|
package/dist/cli/tool.js
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* See the output() function for details (#324).
|
|
16
16
|
*/
|
|
17
17
|
import { writeSync } from 'node:fs';
|
|
18
|
-
import { LocalBackend } from '../mcp/local/local-backend.js';
|
|
19
|
-
import { cliErrorKey } from './cli-message.js';
|
|
18
|
+
import { LocalBackend, VALID_NODE_LABELS } from '../mcp/local/local-backend.js';
|
|
19
|
+
import { cliErrorKey, cliWarnKey } from './cli-message.js';
|
|
20
20
|
import { formatDetectChangesResult } from './detect-changes-format.js';
|
|
21
21
|
let _backend = null;
|
|
22
22
|
async function getBackend() {
|
|
@@ -72,6 +72,11 @@ export async function queryCommand(queryText, options) {
|
|
|
72
72
|
output(result);
|
|
73
73
|
}
|
|
74
74
|
export async function contextCommand(name, options) {
|
|
75
|
+
// Reject a `--`-prefixed uid swallowed from a following flag (see impactCommand).
|
|
76
|
+
if (options?.uid?.startsWith('--')) {
|
|
77
|
+
cliErrorKey('tool.usage.context');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
75
80
|
if (!name?.trim() && !options?.uid) {
|
|
76
81
|
cliErrorKey('tool.usage.context');
|
|
77
82
|
process.exit(1);
|
|
@@ -87,10 +92,25 @@ export async function contextCommand(name, options) {
|
|
|
87
92
|
output(result);
|
|
88
93
|
}
|
|
89
94
|
export async function impactCommand(target, options) {
|
|
90
|
-
|
|
95
|
+
// A `--`-prefixed uid means Commander swallowed a following flag as the uid
|
|
96
|
+
// value (e.g. `impact --uid --file x` → uid === '--file'). Reject it rather
|
|
97
|
+
// than forwarding a garbage uid that would silently resolve to not-found.
|
|
98
|
+
if (options?.uid?.startsWith('--')) {
|
|
91
99
|
cliErrorKey('tool.usage.impact');
|
|
92
100
|
process.exit(1);
|
|
93
101
|
}
|
|
102
|
+
// Target is an optional positional: a uid alone is enough to resolve (parity
|
|
103
|
+
// with `context [name]`). Only error when neither a target nor a uid is given.
|
|
104
|
+
if (!target?.trim() && !options?.uid) {
|
|
105
|
+
cliErrorKey('tool.usage.impact');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
// Soft-validate --kind: an unknown kind is a no-op hint (the backend scores
|
|
109
|
+
// it but it matches nothing), so warn and proceed rather than rejecting —
|
|
110
|
+
// parity with the lenient MCP surface and forward-compatible with new labels.
|
|
111
|
+
if (options?.kind && !VALID_NODE_LABELS.has(options.kind)) {
|
|
112
|
+
cliWarnKey('tool.warn.unknownKind', { kind: options.kind });
|
|
113
|
+
}
|
|
94
114
|
try {
|
|
95
115
|
const backend = await getBackend();
|
|
96
116
|
const rawLimit = parseInt(options?.limit ?? '', 10);
|
|
@@ -98,7 +118,10 @@ export async function impactCommand(target, options) {
|
|
|
98
118
|
const parsedLimit = Number.isFinite(rawLimit) ? rawLimit : undefined;
|
|
99
119
|
const parsedOffset = Number.isFinite(rawOffset) ? rawOffset : undefined;
|
|
100
120
|
const result = await backend.callTool('impact', {
|
|
101
|
-
target,
|
|
121
|
+
target: target || undefined,
|
|
122
|
+
target_uid: options?.uid,
|
|
123
|
+
file_path: options?.file,
|
|
124
|
+
kind: options?.kind,
|
|
102
125
|
direction: options?.direction || 'upstream',
|
|
103
126
|
maxDepth: options?.depth ? parseInt(options.depth, 10) : undefined,
|
|
104
127
|
includeTests: options?.includeTests ?? false,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure predicates gating C# `using` suffix-fallback resolution so BCL usings
|
|
3
|
+
* (e.g. `System.Threading.Tasks`) can't match a coincidentally-named local
|
|
4
|
+
* file (#1881).
|
|
5
|
+
*
|
|
6
|
+
* Lives in the shared `ingestion/` layer — NOT under `languages/csharp/` — so
|
|
7
|
+
* BOTH the registry-primary scope resolver (`languages/csharp/import-target.ts`)
|
|
8
|
+
* and the legacy DAG resolver (`import-resolvers/csharp.ts`) can import it
|
|
9
|
+
* without an `import-resolvers/ -> languages/` dependency inversion (#5).
|
|
10
|
+
*/
|
|
11
|
+
import type { CSharpNamespaceEvidence } from './language-config.js';
|
|
12
|
+
/**
|
|
13
|
+
* Whether the unanchored suffix fallback may run for `targetRaw`.
|
|
14
|
+
*
|
|
15
|
+
* Fails OPEN when the namespace scan was truncated (large repos must not
|
|
16
|
+
* silently lose legitimate edges, #1881 #11) and when no evidence was
|
|
17
|
+
* threaded at all (preserves legacy permissive behavior). The truncation
|
|
18
|
+
* fail-open is carved out for clearly-external roots (BCL / well-known
|
|
19
|
+
* packages) that the repo does not declare, so one incomplete scan can't
|
|
20
|
+
* re-open the #1881 hole repo-wide. Otherwise defers to
|
|
21
|
+
* {@link importAlignsWithDeclaredNamespaces}.
|
|
22
|
+
*/
|
|
23
|
+
export declare function csharpSuffixFallbackAllowed(targetRaw: string, evidence: CSharpNamespaceEvidence | undefined): boolean;
|
|
24
|
+
/** True when `targetRaw` plausibly refers to a namespace declared in-repo. */
|
|
25
|
+
export declare function importAlignsWithDeclaredNamespaces(targetRaw: string, declaredNamespaces: ReadonlySet<string> | undefined, rootNamespaces?: ReadonlySet<string>): boolean;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure predicates gating C# `using` suffix-fallback resolution so BCL usings
|
|
3
|
+
* (e.g. `System.Threading.Tasks`) can't match a coincidentally-named local
|
|
4
|
+
* file (#1881).
|
|
5
|
+
*
|
|
6
|
+
* Lives in the shared `ingestion/` layer — NOT under `languages/csharp/` — so
|
|
7
|
+
* BOTH the registry-primary scope resolver (`languages/csharp/import-target.ts`)
|
|
8
|
+
* and the legacy DAG resolver (`import-resolvers/csharp.ts`) can import it
|
|
9
|
+
* without an `import-resolvers/ -> languages/` dependency inversion (#5).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Top-level namespace segments that clearly belong to the BCL / runtime / a
|
|
13
|
+
* ubiquitous third-party package — i.e. roots a normal repo does NOT declare.
|
|
14
|
+
* These stay gated even when the namespace scan is truncated, so a single
|
|
15
|
+
* unreadable file / capped subtree can't silently re-enable BCL→local suffix
|
|
16
|
+
* matches repo-wide (#1881). A repo that legitimately declares one of these
|
|
17
|
+
* roots is still allowed via the alignment escape hatch below.
|
|
18
|
+
*/
|
|
19
|
+
const CSHARP_EXTERNAL_ROOTS = new Set([
|
|
20
|
+
// .NET BCL / runtime
|
|
21
|
+
'System',
|
|
22
|
+
'Microsoft',
|
|
23
|
+
'Windows',
|
|
24
|
+
'Mono',
|
|
25
|
+
// ubiquitous third-party NuGet roots
|
|
26
|
+
'Newtonsoft',
|
|
27
|
+
'Serilog',
|
|
28
|
+
'AutoMapper',
|
|
29
|
+
'MediatR',
|
|
30
|
+
'Polly',
|
|
31
|
+
'FluentValidation',
|
|
32
|
+
'Grpc',
|
|
33
|
+
'Google',
|
|
34
|
+
'Azure',
|
|
35
|
+
'Amazon',
|
|
36
|
+
'AWSSDK',
|
|
37
|
+
// common test frameworks
|
|
38
|
+
'Xunit',
|
|
39
|
+
'NUnit',
|
|
40
|
+
'Moq',
|
|
41
|
+
'FluentAssertions',
|
|
42
|
+
'NSubstitute',
|
|
43
|
+
'Shouldly',
|
|
44
|
+
]);
|
|
45
|
+
/** Whether `targetRaw`'s top-level segment is a clearly-external root. */
|
|
46
|
+
function isExternalRoot(targetRaw) {
|
|
47
|
+
const dot = targetRaw.indexOf('.');
|
|
48
|
+
const top = dot === -1 ? targetRaw : targetRaw.slice(0, dot);
|
|
49
|
+
return CSHARP_EXTERNAL_ROOTS.has(top);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Whether the unanchored suffix fallback may run for `targetRaw`.
|
|
53
|
+
*
|
|
54
|
+
* Fails OPEN when the namespace scan was truncated (large repos must not
|
|
55
|
+
* silently lose legitimate edges, #1881 #11) and when no evidence was
|
|
56
|
+
* threaded at all (preserves legacy permissive behavior). The truncation
|
|
57
|
+
* fail-open is carved out for clearly-external roots (BCL / well-known
|
|
58
|
+
* packages) that the repo does not declare, so one incomplete scan can't
|
|
59
|
+
* re-open the #1881 hole repo-wide. Otherwise defers to
|
|
60
|
+
* {@link importAlignsWithDeclaredNamespaces}.
|
|
61
|
+
*/
|
|
62
|
+
export function csharpSuffixFallbackAllowed(targetRaw, evidence) {
|
|
63
|
+
if (evidence === undefined)
|
|
64
|
+
return true;
|
|
65
|
+
if (evidence.truncated) {
|
|
66
|
+
// Keep clearly-external roots blocked through truncation UNLESS the repo
|
|
67
|
+
// actually declares an aligning namespace (the alignment check is the
|
|
68
|
+
// escape hatch — a repo that declares `namespace System;` still resolves).
|
|
69
|
+
if (isExternalRoot(targetRaw) &&
|
|
70
|
+
!importAlignsWithDeclaredNamespaces(targetRaw, evidence.declaredNamespaces, evidence.rootNamespaces)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return importAlignsWithDeclaredNamespaces(targetRaw, evidence.declaredNamespaces, evidence.rootNamespaces);
|
|
76
|
+
}
|
|
77
|
+
/** True when `targetRaw` plausibly refers to a namespace declared in-repo. */
|
|
78
|
+
export function importAlignsWithDeclaredNamespaces(targetRaw, declaredNamespaces, rootNamespaces) {
|
|
79
|
+
if (declaredNamespaces === undefined || declaredNamespaces.size === 0)
|
|
80
|
+
return false;
|
|
81
|
+
// Exact: the import IS a declared in-repo namespace.
|
|
82
|
+
if (declaredNamespaces.has(targetRaw))
|
|
83
|
+
return true;
|
|
84
|
+
// Child-of: the import's IMMEDIATE parent namespace is declared in-repo.
|
|
85
|
+
// Anchoring on the direct parent — not "any declared prefix" — is what stops
|
|
86
|
+
// a declared BCL prefix from green-lighting an unrelated BCL using: a repo
|
|
87
|
+
// that declares `namespace System;` must NOT make `using
|
|
88
|
+
// System.Threading.Tasks;` resolve to a coincidental local `Tasks.cs`,
|
|
89
|
+
// because the import's parent `System.Threading` is not itself declared
|
|
90
|
+
// (#1881). The case this still allows is a type / `using static` import under
|
|
91
|
+
// a declared namespace laid out without its full path on disk, e.g.
|
|
92
|
+
// `using static MyApp.Utils.Logger;` when `MyApp.Utils` is declared.
|
|
93
|
+
const lastDot = targetRaw.lastIndexOf('.');
|
|
94
|
+
if (lastDot > 0 && declaredNamespaces.has(targetRaw.slice(0, lastDot)))
|
|
95
|
+
return true;
|
|
96
|
+
// Ancestor-of: the import is a strict prefix of some declared namespace
|
|
97
|
+
// (e.g. `using MyApp;` when `MyApp.Models` is declared). Only honored when
|
|
98
|
+
// the import also sits at or above an in-repo root namespace, so a BCL prefix
|
|
99
|
+
// can't qualify merely because a file declares something deeper under it
|
|
100
|
+
// (e.g. `System.Threading.Tasks.Extensions`) (#1881).
|
|
101
|
+
const childPrefix = targetRaw + '.';
|
|
102
|
+
for (const ns of declaredNamespaces) {
|
|
103
|
+
if (ns.startsWith(childPrefix)) {
|
|
104
|
+
return isAtOrAboveInRepoRoot(targetRaw, declaredNamespaces, rootNamespaces);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
function isAtOrAboveInRepoRoot(targetRaw, declaredNamespaces, rootNamespaces) {
|
|
110
|
+
const descendantPrefix = targetRaw + '.';
|
|
111
|
+
if (rootNamespaces !== undefined && rootNamespaces.size > 0) {
|
|
112
|
+
for (const root of rootNamespaces) {
|
|
113
|
+
// targetRaw equals a root, or is an ancestor of one (e.g. `using MyApp;`
|
|
114
|
+
// for csproj RootNamespace `MyApp.Core`).
|
|
115
|
+
if (root === targetRaw || root.startsWith(descendantPrefix))
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
// No explicit roots (e.g. no csproj): treat the top-level segment of each
|
|
121
|
+
// declared namespace as the implied root.
|
|
122
|
+
for (const ns of declaredNamespaces) {
|
|
123
|
+
const dot = ns.indexOf('.');
|
|
124
|
+
const top = dot === -1 ? ns : ns.slice(0, dot);
|
|
125
|
+
if (top === targetRaw)
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
@@ -5,21 +5,36 @@
|
|
|
5
5
|
import { SupportedLanguages } from '../../../../_shared/index.js';
|
|
6
6
|
import { createStandardStrategy } from '../standard.js';
|
|
7
7
|
import { resolveCSharpImportInternal, resolveCSharpNamespaceDir } from '../csharp.js';
|
|
8
|
+
import { csharpSuffixFallbackAllowed } from '../../csharp-namespace-gate.js';
|
|
8
9
|
/** C# namespace-based resolution strategy via .csproj configs. */
|
|
9
10
|
export const csharpNamespaceStrategy = (rawImportPath, _filePath, ctx) => {
|
|
10
11
|
const csharpConfigs = ctx.configs.csharpConfigs;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
const evidence = ctx.configs.csharpNamespaces;
|
|
13
|
+
if (csharpConfigs.length === 0) {
|
|
14
|
+
// No csproj → there's no namespace→directory mapping to apply, so the
|
|
15
|
+
// generic strategy would normally take over. But that generic suffix match
|
|
16
|
+
// is UNGATED: it re-introduces the BCL→local spurious match the #1881 gate
|
|
17
|
+
// exists to stop. Mirror the registry leg's no-csproj path — defer to the
|
|
18
|
+
// generic strategy ONLY for imports that align with an in-repo declared
|
|
19
|
+
// namespace; for everything else (BCL usings) return an authoritative empty
|
|
20
|
+
// result that STOPS the chain (#2 parity). With no evidence threaded the
|
|
21
|
+
// gate fails open, so behavior is unchanged when the scan didn't run.
|
|
22
|
+
if (!csharpSuffixFallbackAllowed(rawImportPath, evidence)) {
|
|
23
|
+
return { kind: 'files', files: [] };
|
|
18
24
|
}
|
|
19
|
-
|
|
20
|
-
return { kind: 'files', files: resolvedFiles };
|
|
25
|
+
return null;
|
|
21
26
|
}
|
|
22
|
-
|
|
27
|
+
const resolvedFiles = resolveCSharpImportInternal(rawImportPath, csharpConfigs, ctx.normalizedFileList, ctx.allFileList, ctx.index, evidence);
|
|
28
|
+
if (resolvedFiles.length > 1) {
|
|
29
|
+
const dirSuffix = resolveCSharpNamespaceDir(rawImportPath, csharpConfigs);
|
|
30
|
+
if (dirSuffix) {
|
|
31
|
+
return { kind: 'package', files: resolvedFiles, dirSuffix };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Authoritative once csproj configs exist: return even an empty result to
|
|
35
|
+
// STOP the chain, so the generic suffix fallback can't re-introduce the
|
|
36
|
+
// gated BCL→local match this resolver just suppressed (#1881).
|
|
37
|
+
return { kind: 'files', files: resolvedFiles };
|
|
23
38
|
};
|
|
24
39
|
export const csharpImportConfig = {
|
|
25
40
|
language: SupportedLanguages.CSharp,
|
|
@@ -5,12 +5,16 @@
|
|
|
5
5
|
* This file contains shared helpers for namespace-based resolution.
|
|
6
6
|
*/
|
|
7
7
|
import type { SuffixIndex } from './utils.js';
|
|
8
|
-
import type { CSharpProjectConfig } from '../language-config.js';
|
|
8
|
+
import type { CSharpProjectConfig, CSharpNamespaceEvidence } from '../language-config.js';
|
|
9
9
|
/**
|
|
10
10
|
* Resolve a C# using-directive import path to matching .cs files (low-level helper).
|
|
11
11
|
* Tries single-file match first, then directory match for namespace imports.
|
|
12
|
+
*
|
|
13
|
+
* The final unanchored suffix fallback is gated on `evidence` so BCL usings
|
|
14
|
+
* (e.g. `System.Threading.Tasks`) can't match a coincidentally-named local
|
|
15
|
+
* file (#1881). When `evidence` is omitted the fallback stays permissive.
|
|
12
16
|
*/
|
|
13
|
-
export declare function resolveCSharpImportInternal(importPath: string, csharpConfigs: CSharpProjectConfig[], normalizedFileList: string[], allFileList: string[], index?: SuffixIndex): string[];
|
|
17
|
+
export declare function resolveCSharpImportInternal(importPath: string, csharpConfigs: CSharpProjectConfig[], normalizedFileList: string[], allFileList: string[], index?: SuffixIndex, evidence?: CSharpNamespaceEvidence): string[];
|
|
14
18
|
/**
|
|
15
19
|
* Compute the directory suffix for a C# namespace import (for PackageMap).
|
|
16
20
|
* Returns a suffix like "/ProjectDir/Models/" or null if no config matches.
|
|
@@ -5,11 +5,16 @@
|
|
|
5
5
|
* This file contains shared helpers for namespace-based resolution.
|
|
6
6
|
*/
|
|
7
7
|
import { suffixResolve } from './utils.js';
|
|
8
|
+
import { csharpSuffixFallbackAllowed } from '../csharp-namespace-gate.js';
|
|
8
9
|
/**
|
|
9
10
|
* Resolve a C# using-directive import path to matching .cs files (low-level helper).
|
|
10
11
|
* Tries single-file match first, then directory match for namespace imports.
|
|
12
|
+
*
|
|
13
|
+
* The final unanchored suffix fallback is gated on `evidence` so BCL usings
|
|
14
|
+
* (e.g. `System.Threading.Tasks`) can't match a coincidentally-named local
|
|
15
|
+
* file (#1881). When `evidence` is omitted the fallback stays permissive.
|
|
11
16
|
*/
|
|
12
|
-
export function resolveCSharpImportInternal(importPath, csharpConfigs, normalizedFileList, allFileList, index) {
|
|
17
|
+
export function resolveCSharpImportInternal(importPath, csharpConfigs, normalizedFileList, allFileList, index, evidence) {
|
|
13
18
|
const namespacePath = importPath.replace(/\./g, '/');
|
|
14
19
|
const results = [];
|
|
15
20
|
for (const config of csharpConfigs) {
|
|
@@ -79,7 +84,11 @@ export function resolveCSharpImportInternal(importPath, csharpConfigs, normalize
|
|
|
79
84
|
return results;
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
|
-
// Fallback: suffix matching without namespace stripping (single file)
|
|
87
|
+
// Fallback: suffix matching without namespace stripping (single file).
|
|
88
|
+
// Gated on in-repo declared-namespace evidence (#1881).
|
|
89
|
+
if (!csharpSuffixFallbackAllowed(importPath, evidence)) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
83
92
|
const pathParts = namespacePath.split('/').filter(Boolean);
|
|
84
93
|
const fallback = suffixResolve(pathParts, normalizedFileList, allFileList, index);
|
|
85
94
|
return fallback ? [fallback] : [];
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extracted from import-resolution.ts to co-locate types with their consumers.
|
|
5
5
|
*/
|
|
6
|
-
import type { TsconfigPaths, GoModuleConfig, CSharpProjectConfig, ComposerConfig } from '../language-config.js';
|
|
6
|
+
import type { TsconfigPaths, GoModuleConfig, CSharpProjectConfig, CSharpNamespaceEvidence, ComposerConfig } from '../language-config.js';
|
|
7
7
|
import type { SwiftPackageConfig } from '../language-config.js';
|
|
8
8
|
import type { SuffixIndex } from './utils.js';
|
|
9
9
|
import type { SupportedLanguages } from '../../../_shared/index.js';
|
|
@@ -28,6 +28,8 @@ export interface ImportConfigs {
|
|
|
28
28
|
composerConfig: ComposerConfig | null;
|
|
29
29
|
swiftPackageConfig: SwiftPackageConfig | null;
|
|
30
30
|
csharpConfigs: CSharpProjectConfig[];
|
|
31
|
+
/** In-repo namespace evidence gating C# suffix-fallback resolution (#1881). */
|
|
32
|
+
csharpNamespaces?: CSharpNamespaceEvidence;
|
|
31
33
|
}
|
|
32
34
|
/** Pre-built lookup structures for import resolution. Build once, reuse across chunks. */
|
|
33
35
|
export interface ImportResolutionContext {
|
|
@@ -26,6 +26,35 @@ export interface CSharpProjectConfig {
|
|
|
26
26
|
/** Directory containing the .csproj file */
|
|
27
27
|
projectDir: string;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Declared-namespace evidence used to gate C# suffix-fallback resolution so
|
|
31
|
+
* BCL usings (e.g. `System.Threading.Tasks`) can't match a coincidentally-
|
|
32
|
+
* named local file (#1881).
|
|
33
|
+
*/
|
|
34
|
+
export interface CSharpNamespaceEvidence {
|
|
35
|
+
/** Every `namespace X.Y` declared in-repo (scan may be capped — see `truncated`). */
|
|
36
|
+
readonly declaredNamespaces?: ReadonlySet<string>;
|
|
37
|
+
/** csproj RootNamespace values plus the top-level segment of each declared
|
|
38
|
+
* namespace — the anchor set for the parent-namespace gate direction. */
|
|
39
|
+
readonly rootNamespaces?: ReadonlySet<string>;
|
|
40
|
+
/** True when the BFS hit its dir/depth cap, so the namespace set may be
|
|
41
|
+
* incomplete; the gate fails open (allows) in that case. */
|
|
42
|
+
readonly truncated?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** Result of a single BFS over a repo collecting both csproj configs and
|
|
45
|
+
* declared `.cs` namespaces (one disk traversal — see `scanCSharpProject`). */
|
|
46
|
+
export interface CSharpProjectScan {
|
|
47
|
+
readonly configs: CSharpProjectConfig[];
|
|
48
|
+
readonly declaredNamespaces: ReadonlySet<string>;
|
|
49
|
+
readonly rootNamespaces: ReadonlySet<string>;
|
|
50
|
+
readonly truncated: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** Project the one-pass {@link CSharpProjectScan} into the
|
|
53
|
+
* {@link CSharpNamespaceEvidence} both import-resolution legs thread to the
|
|
54
|
+
* #1881 gate — one shape, two carriers (`ImportConfigs.csharpNamespaces` for
|
|
55
|
+
* the legacy DAG, `CsharpResolutionConfig.namespaces` for the scope resolver).
|
|
56
|
+
* Keeps the field mapping in one place so the two carriers can't drift. */
|
|
57
|
+
export declare function csharpScanToEvidence(scan: CSharpProjectScan): CSharpNamespaceEvidence;
|
|
29
58
|
/** Swift Package Manager module config */
|
|
30
59
|
export interface SwiftPackageConfig {
|
|
31
60
|
/** Map of target name -> source directory path (e.g., "SiuperModel" -> "Package/Sources/SiuperModel") */
|
|
@@ -43,10 +72,18 @@ export declare function loadGoModulePath(repoRoot: string): Promise<GoModuleConf
|
|
|
43
72
|
/** Parse composer.json to extract PSR-4 autoload mappings (including autoload-dev). */
|
|
44
73
|
export declare function loadComposerConfig(repoRoot: string): Promise<ComposerConfig | null>;
|
|
45
74
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
75
|
+
* Single BFS over a repo that collects BOTH .csproj configs and the set of
|
|
76
|
+
* `namespace` declarations from `.cs` files.
|
|
77
|
+
*
|
|
78
|
+
* The csproj walk is cheap (a handful of project files); the namespace scan
|
|
79
|
+
* is NOT — it opens and reads every `.cs` file in the repo to collect its
|
|
80
|
+
* `namespace` declarations. That `.cs` read cost is the price of the #1881
|
|
81
|
+
* gate, not a saving: collapsing the csproj and namespace walks into one BFS
|
|
82
|
+
* avoids a second directory traversal, but the per-file `.cs` reads are new
|
|
83
|
+
* work this scan introduces. Reads within a directory are issued in bounded
|
|
84
|
+
* windows (see below); directories are still visited breadth-first.
|
|
48
85
|
*/
|
|
49
|
-
export declare function
|
|
86
|
+
export declare function scanCSharpProject(repoRoot: string): Promise<CSharpProjectScan>;
|
|
50
87
|
export declare function loadSwiftPackageConfig(repoRoot: string): Promise<SwiftPackageConfig | null>;
|
|
51
88
|
/** Load all language-specific configs once for an ingestion run. */
|
|
52
89
|
export declare function loadImportConfigs(repoRoot: string): Promise<ImportConfigs>;
|